summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJoe Onorato <joeo@google.com>2019-05-22 03:16:44 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2019-05-22 03:16:44 +0000
commit0ebde2fb1509c307601197f8a5b14e9254cc66bf (patch)
tree2abbc4128dfec8d4691a5298afbf3a03601bb287 /src
parentc506eaaaec152bdb8f1f0dc33965a4bbd9dd8b62 (diff)
parent2ea4955fcdc3698216c04519724b3cab50fd33ab (diff)
downloadandroid_packages_apps_PackageInstaller-0ebde2fb1509c307601197f8a5b14e9254cc66bf.tar.gz
android_packages_apps_PackageInstaller-0ebde2fb1509c307601197f8a5b14e9254cc66bf.tar.bz2
android_packages_apps_PackageInstaller-0ebde2fb1509c307601197f8a5b14e9254cc66bf.zip
Merge "Show reason text from incident report in incident confirmation dialog." into qt-dev
Diffstat (limited to 'src')
-rw-r--r--src/com/android/packageinstaller/incident/ConfirmationActivity.java220
-rw-r--r--src/com/android/packageinstaller/incident/ReportDetails.java183
-rw-r--r--src/com/android/packageinstaller/incident/incident_minimal.proto25
3 files changed, 267 insertions, 161 deletions
diff --git a/src/com/android/packageinstaller/incident/ConfirmationActivity.java b/src/com/android/packageinstaller/incident/ConfirmationActivity.java
index e4504ec7..dcfe88a5 100644
--- a/src/com/android/packageinstaller/incident/ConfirmationActivity.java
+++ b/src/com/android/packageinstaller/incident/ConfirmationActivity.java
@@ -26,6 +26,9 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.IncidentManager;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.BulletSpan;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
@@ -34,11 +37,6 @@ import android.widget.TextView;
import com.android.permissioncontroller.R;
-import com.google.protobuf.ByteString;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
import java.util.ArrayList;
/**
@@ -95,13 +93,15 @@ public class ConfirmationActivity extends Activity implements OnClickListener, O
final String appLabel = formatting.getAppLabel(pending.getRequestingPackage());
final Resources res = getResources();
- final ArrayList<Drawable> images = getImages(uri, res);
- if (images == null) {
- // Null result from getImages means that there was an error in the input,
- // and we will just summarily reject the upload, since we can't get proper
- // approval.
- // Zero-length list means that we will proceed with the imageless consent
- // dialog.
+
+ ReportDetails details;
+ try {
+ details = ReportDetails.parseIncidentReport(this, uri);
+ } catch (ReportDetails.ParseException ex) {
+ Log.w("Rejecting report because it couldn't be parsed", ex);
+ // If there was an error in the input we will just summarily reject the upload,
+ // since we can't get proper approval. (Zero-length images or reasons means that
+ // we will proceed with the imageless consent dialog).
final IncidentManager incidentManager = getSystemService(IncidentManager.class);
incidentManager.denyReport(getIntent().getData());
@@ -118,26 +118,58 @@ public class ConfirmationActivity extends Activity implements OnClickListener, O
.setOnDismissListener(this)
.show();
return;
+
}
+ final View content = getLayoutInflater().inflate(R.layout.incident_confirmation,
+ null);
+
+ final ArrayList<String> reasons = details.getReasons();
+ final int reasonsSize = reasons.size();
+ if (reasonsSize > 0) {
+ content.findViewById(R.id.reasonIntro).setVisibility(View.VISIBLE);
+
+ final TextView reasonTextView = (TextView) content.findViewById(R.id.reasons);
+ reasonTextView.setVisibility(View.VISIBLE);
+
+ final int bulletSize =
+ (int) (res.getDimension(R.dimen.incident_reason_bullet_size) + 0.5f);
+ final int bulletIndent =
+ (int) (res.getDimension(R.dimen.incident_reason_bullet_indent) + 0.5f);
+ final int bulletColor =
+ getColor(R.color.incident_reason_bullet_color);
+
+ final StringBuilder text = new StringBuilder();
+ for (int i = 0; i < reasonsSize; i++) {
+ text.append(reasons.get(i));
+ if (i != reasonsSize - 1) {
+ text.append("\n");
+ }
+ }
+ final SpannableString spannable = new SpannableString(text.toString());
+ int spanStart = 0;
+ for (int i = 0; i < reasonsSize; i++) {
+ final int length = reasons.get(i).length();
+ spannable.setSpan(new BulletSpan(bulletIndent, bulletColor, bulletSize),
+ spanStart, spanStart + length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spanStart += length + 1;
+ }
- final AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.incident_report_dialog_title)
- .setPositiveButton(R.string.incident_report_dialog_allow_label, this)
- .setNegativeButton(R.string.incident_report_dialog_deny_label, this)
- .setOnDismissListener(this);
+ reasonTextView.setText(spannable);
+ }
final String message = getString(R.string.incident_report_dialog_text,
appLabel,
formatting.getDate(pending.getTimestamp()),
formatting.getTime(pending.getTimestamp()),
appLabel);
+ ((TextView) content.findViewById(R.id.message)).setText(message);
-
+ final ArrayList<Drawable> images = details.getImages();
final int imagesSize = images.size();
if (imagesSize > 0) {
- final View content = getLayoutInflater().inflate(R.layout.incident_image_confirmation,
- null);
+ content.findViewById(R.id.imageScrollView).setVisibility(View.VISIBLE);
+
final LinearLayout imageList = (LinearLayout) content.findViewById(R.id.imageList);
final int width = res.getDimensionPixelSize(R.dimen.incident_image_width);
@@ -151,15 +183,15 @@ public class ConfirmationActivity extends Activity implements OnClickListener, O
imageList.addView(imageView, new LinearLayout.LayoutParams(width, height));
}
-
- ((TextView) content.findViewById(R.id.message)).setText(message);
-
- builder.setView(content);
- } else {
- builder.setMessage(message);
}
- builder.show();
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.incident_report_dialog_title)
+ .setPositiveButton(R.string.incident_report_dialog_allow_label, this)
+ .setNegativeButton(R.string.incident_report_dialog_deny_label, this)
+ .setOnDismissListener(this)
+ .setView(content)
+ .show();
}
/**
@@ -209,139 +241,5 @@ public class ConfirmationActivity extends Activity implements OnClickListener, O
}
finish();
}
-
- ArrayList<Drawable> getImages(final Uri uri, Resources res) {
- try {
- final IncidentManager incidentManager = getSystemService(IncidentManager.class);
- final IncidentManager.IncidentReport report = incidentManager.getIncidentReport(uri);
- if (report == null) {
- // There is no incident report, so no images to show, so return empty list.
- // Other errors below are invalid images, which we reject, because they're there
- // but we can't let the user confirm it.
- return new ArrayList();
- }
-
- final InputStream stream = report.getInputStream();
- if (stream != null) {
- return getImages(stream, res);
- }
- } catch (IOException ex) {
- Log.w(TAG, "Error while reading stream. The report will be rejected.", ex);
- return null;
- } catch (OutOfMemoryError ex) {
- Log.w(TAG, "Out of memory while creating confirmation images. The report will"
- + " be rejected.", ex);
- return null;
- }
- return new ArrayList();
- }
-
- ArrayList<Drawable> getImages(InputStream stream, Resources res) throws IOException {
- final IncidentMinimal incident = IncidentMinimal.parseFrom(stream);
- if (incident != null) {
- return getImages(incident, res);
- } else {
- return new ArrayList();
- }
- }
-
- ArrayList<Drawable> getImages(IncidentMinimal incident, Resources res) {
- final ArrayList<Drawable> drawables = new ArrayList();
-
- final int totalImageCountLimit = 200;
- int totalImageCount = 0;
-
- if (incident.hasRestrictedImagesSection()) {
- final RestrictedImagesDumpProto section = incident.getRestrictedImagesSection();
- final int setsCount = section.getSetsCount();
- for (int i = 0; i < setsCount; i++) {
- final RestrictedImageSetProto set = section.getSets(i);
- if (set == null) {
- continue;
- }
- final int imageCount = set.getImagesCount();
- for (int j = 0; j < imageCount; j++) {
- // Hard cap on number of images, as a guardrail.
- totalImageCount++;
- if (totalImageCount > totalImageCountLimit) {
- Log.w(TAG, "Image count is greater than the limit of "
- + totalImageCountLimit + ". The report will be rejected.");
- return null;
- }
-
- final RestrictedImageProto image = set.getImages(j);
- if (image == null) {
- continue;
- }
- final String mimeType = image.getMimeType();
- if (!("image/jpeg".equals(mimeType)
- || "image/png".equals(mimeType))) {
- Log.w(TAG, "Unsupported image type " + mimeType
- + ". The report will be rejected.");
- return null;
- }
- final ByteString bytes = image.getImageData();
- if (bytes == null) {
- continue;
- }
- final byte[] buf = bytes.toByteArray();
- if (buf.length == 0) {
- continue;
- }
-
- // This will attempt to uncompress the image. If it's gigantic,
- // this could fail with OutOfMemoryError, which will be caught
- // by the caller, and turned into a report rejection.
- final Drawable drawable = new android.graphics.drawable.BitmapDrawable(
- res, new ByteArrayInputStream(buf));
-
- // TODO: Scale bitmap to correct thumbnail size to save memory.
-
- drawables.add(drawable);
- }
- }
- }
-
- // Test data
- if (false) {
- drawables.add(new android.graphics.drawable.BitmapDrawable(res,
- new ByteArrayInputStream(new byte[] {
- // png image data
- -119, 80, 78, 71, 13, 10, 26, 10,
- 0, 0, 0, 13, 73, 72, 68, 82,
- 0, 0, 0, 100, 0, 0, 0, 100,
- 1, 3, 0, 0, 0, 74, 44, 7,
- 23, 0, 0, 0, 4, 103, 65, 77,
- 65, 0, 0, -79, -113, 11, -4, 97,
- 5, 0, 0, 0, 1, 115, 82, 71,
- 66, 0, -82, -50, 28, -23, 0, 0,
- 0, 6, 80, 76, 84, 69, -1, -1,
- -1, 0, 0, 0, 85, -62, -45, 126,
- 0, 0, 0, -115, 73, 68, 65, 84,
- 56, -53, -19, -46, -79, 17, -128, 32,
- 12, 5, -48, 120, 22, -106, -116, -32,
- 40, -84, 101, -121, -93, 57, 10, 35,
- 88, 82, 112, 126, 3, -60, 104, 6,
- -112, 70, 127, -59, -69, -53, 29, 33,
- -127, -24, 79, -49, -52, -15, 41, 36,
- 34, -105, 85, 124, -14, 88, 27, 6,
- 28, 68, 1, 82, 62, 22, -95, -108,
- 55, -95, 40, -9, -110, -12, 98, -107,
- 76, -41, -105, -62, -50, 111, -60, 46,
- -14, -4, 24, -89, 42, -103, 16, 63,
- -72, -11, -15, 48, -62, 102, -44, 102,
- -73, -56, 56, -21, -128, 92, -70, -124,
- 117, -46, -67, -77, 82, 80, 121, -44,
- -56, 116, 93, -45, -90, -5, -29, -24,
- -83, -75, 52, -34, 55, -22, 102, -21,
- -105, -124, -23, 71, 87, -7, -25, -59,
- -100, -73, -92, -122, -7, -109, -49, -80,
- -89, 0, 0, 0, 0, 73, 69, 78,
- 68, -82, 66, 96, -126
- })));
- }
-
- return drawables;
- }
}
diff --git a/src/com/android/packageinstaller/incident/ReportDetails.java b/src/com/android/packageinstaller/incident/ReportDetails.java
new file mode 100644
index 00000000..a6d40193
--- /dev/null
+++ b/src/com/android/packageinstaller/incident/ReportDetails.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.incident;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.IncidentManager;
+
+import com.google.protobuf.ByteString;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * The pieces of an incident report that should be confirmed by the user.
+ */
+public class ReportDetails {
+ private static final String TAG = "ReportDetails";
+
+ private ArrayList<String> mReasons = new ArrayList<String>();
+ private ArrayList<Drawable> mImages = new ArrayList<Drawable>();
+
+ /**
+ * Thrown when there is an error parsing the incident report. Incident reports
+ * that can't be parsed can not be properly shown to the user and are summarily
+ * rejected.
+ */
+ public static class ParseException extends Exception {
+ public ParseException(String message) {
+ super(message);
+ }
+
+ public ParseException(String message, Throwable ex) {
+ super(message, ex);
+ }
+ }
+
+ private ReportDetails() {
+ }
+
+ /**
+ * Parse an incident report into a ReportDetails object. This function drops most
+ * of the fields in an incident report
+ */
+ public static ReportDetails parseIncidentReport(final Context context, final Uri uri)
+ throws ParseException {
+ final ReportDetails details = new ReportDetails();
+ try {
+ final IncidentManager incidentManager = context.getSystemService(IncidentManager.class);
+ final IncidentManager.IncidentReport report = incidentManager.getIncidentReport(uri);
+ if (report == null) {
+ // There is no incident report, so nothing to show, so return empty object.
+ // Other errors below are invalid images, which we reject, because they're there
+ // but we can't let the user confirm it, but nothing to show is okay. This is
+ // also the dumpstate / bugreport case.
+ return details;
+ }
+
+ final InputStream stream = report.getInputStream();
+ if (stream != null) {
+ final IncidentMinimal incident = IncidentMinimal.parseFrom(stream);
+ if (incident != null) {
+ parseImages(details.mImages, incident, context.getResources());
+ parseReasons(details.mReasons, incident);
+ }
+ }
+ } catch (IOException ex) {
+ throw new ParseException("Error while reading stream.", ex);
+ } catch (OutOfMemoryError ex) {
+ throw new ParseException("Out of memory while loading incident report.", ex);
+ }
+ return details;
+ }
+
+ /**
+ * Reads the reasons from the incident headers. Does not throw any exceptions
+ * about validity, because the headers are optional.
+ */
+ private static void parseReasons(ArrayList<String> result, IncidentMinimal incident) {
+ final int headerSize = incident.getHeaderCount();
+ for (int i = 0; i < headerSize; i++) {
+ final IncidentHeaderProto header = incident.getHeader(i);
+ if (header.hasReason()) {
+ final String reason = header.getReason();
+ if (reason != null && reason.length() > 0) {
+ result.add(reason);
+ }
+ }
+ }
+ }
+
+ /**
+ * Read images from the IncidentMinimal.
+ *
+ * @throw ParseException if there was an error reading them.
+ */
+ private static void parseImages(ArrayList<Drawable> result, IncidentMinimal incident,
+ Resources res) throws ParseException {
+ final int totalImageCountLimit = 200;
+ int totalImageCount = 0;
+
+ if (incident.hasRestrictedImagesSection()) {
+ final RestrictedImagesDumpProto section = incident.getRestrictedImagesSection();
+ final int setsCount = section.getSetsCount();
+ for (int i = 0; i < setsCount; i++) {
+ final RestrictedImageSetProto set = section.getSets(i);
+ if (set == null) {
+ continue;
+ }
+ final int imageCount = set.getImagesCount();
+ for (int j = 0; j < imageCount; j++) {
+ // Hard cap on number of images, as a guardrail.
+ totalImageCount++;
+ if (totalImageCount > totalImageCountLimit) {
+ throw new ParseException("Image count is greater than the limit of "
+ + totalImageCountLimit);
+ }
+
+ final RestrictedImageProto image = set.getImages(j);
+ if (image == null) {
+ continue;
+ }
+ final String mimeType = image.getMimeType();
+ if (!("image/jpeg".equals(mimeType)
+ || "image/png".equals(mimeType))) {
+ throw new ParseException("Unsupported image type " + mimeType);
+ }
+ final ByteString bytes = image.getImageData();
+ if (bytes == null) {
+ continue;
+ }
+ final byte[] buf = bytes.toByteArray();
+ if (buf.length == 0) {
+ continue;
+ }
+
+ // This will attempt to uncompress the image. If it's gigantic,
+ // this could fail with OutOfMemoryError, which will be caught
+ // by the caller, and turned into a report rejection.
+ final Drawable drawable = new android.graphics.drawable.BitmapDrawable(
+ res, new ByteArrayInputStream(buf));
+
+ // TODO: Scale bitmap to correct thumbnail size to save memory.
+
+ result.add(drawable);
+ }
+ }
+ }
+ }
+
+ /**
+ * The "reason" field from any incident report headers, which could contain
+ * explanitory text for why the incident report was taken.
+ */
+ public ArrayList<String> getReasons() {
+ return mReasons;
+ }
+
+ /**
+ * Images that must be approved by the user.
+ */
+ public ArrayList<Drawable> getImages() {
+ return mImages;
+ }
+}
diff --git a/src/com/android/packageinstaller/incident/incident_minimal.proto b/src/com/android/packageinstaller/incident/incident_minimal.proto
index 29088f84..150e8d8a 100644
--- a/src/com/android/packageinstaller/incident/incident_minimal.proto
+++ b/src/com/android/packageinstaller/incident/incident_minimal.proto
@@ -19,19 +19,41 @@ package com.android.packageinstaller.incident;
option java_multiple_files = true;
+
+/*
+ * Everything in this file is a subset of the platform incident.proto.
+ */
+
/**
* This message has the same fields in an incident report that we care about
* but none of the ones we don't. So when we receive one, we attempt to parse
* it using this proto, which will result in the rest of the fields being dropped.
+ *
+ * From frameworks/base/core/proto/android/os/incident.proto
*/
message IncidentMinimal {
+ repeated IncidentHeaderProto header = 1;
optional RestrictedImagesDumpProto restricted_images_section = 3025;
}
+/**
+ * From frameworks/base/core/proto/android/os/header.proto
+ */
+message IncidentHeaderProto {
+ // A human readable reason why an incident report is requested.
+ optional string reason = 2;
+}
+
+/**
+ * From frameworks/base/core/proto/android/service/restricted_image.proto
+ */
message RestrictedImagesDumpProto {
repeated RestrictedImageSetProto sets = 1;
}
+/**
+ * From frameworks/base/core/proto/android/service/restricted_image.proto
+ */
message RestrictedImageSetProto {
// Name of the service producing the data.
optional string category = 1;
@@ -43,6 +65,9 @@ message RestrictedImageSetProto {
optional bytes metadata = 3;
}
+/**
+ * From frameworks/base/core/proto/android/service/restricted_image.proto
+ */
message RestrictedImageProto {
// Type of image data
optional string mime_type = 1;