diff options
author | Joe Onorato <joeo@google.com> | 2019-05-22 03:16:44 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2019-05-22 03:16:44 +0000 |
commit | 0ebde2fb1509c307601197f8a5b14e9254cc66bf (patch) | |
tree | 2abbc4128dfec8d4691a5298afbf3a03601bb287 /src | |
parent | c506eaaaec152bdb8f1f0dc33965a4bbd9dd8b62 (diff) | |
parent | 2ea4955fcdc3698216c04519724b3cab50fd33ab (diff) | |
download | android_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')
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; |