1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.packageinstaller.incident; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.drawable.Drawable; 22 import android.net.Uri; 23 import android.os.IncidentManager; 24 25 import com.google.protobuf.ByteString; 26 27 import java.io.ByteArrayInputStream; 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.util.ArrayList; 31 32 /** 33 * The pieces of an incident report that should be confirmed by the user. 34 */ 35 public class ReportDetails { 36 private static final String TAG = "ReportDetails"; 37 38 private ArrayList<String> mReasons = new ArrayList<String>(); 39 private ArrayList<Drawable> mImages = new ArrayList<Drawable>(); 40 41 /** 42 * Thrown when there is an error parsing the incident report. Incident reports 43 * that can't be parsed can not be properly shown to the user and are summarily 44 * rejected. 45 */ 46 public static class ParseException extends Exception { ParseException(String message)47 public ParseException(String message) { 48 super(message); 49 } 50 ParseException(String message, Throwable ex)51 public ParseException(String message, Throwable ex) { 52 super(message, ex); 53 } 54 } 55 ReportDetails()56 private ReportDetails() { 57 } 58 59 /** 60 * Parse an incident report into a ReportDetails object. This function drops most 61 * of the fields in an incident report 62 */ parseIncidentReport(final Context context, final Uri uri)63 public static ReportDetails parseIncidentReport(final Context context, final Uri uri) 64 throws ParseException { 65 final ReportDetails details = new ReportDetails(); 66 try { 67 final IncidentManager incidentManager = context.getSystemService(IncidentManager.class); 68 final IncidentManager.IncidentReport report = incidentManager.getIncidentReport(uri); 69 if (report == null) { 70 // There is no incident report, so nothing to show, so return empty object. 71 // Other errors below are invalid images, which we reject, because they're there 72 // but we can't let the user confirm it, but nothing to show is okay. This is 73 // also the dumpstate / bugreport case. 74 return details; 75 } 76 77 final InputStream stream = report.getInputStream(); 78 if (stream != null) { 79 final IncidentMinimal incident = IncidentMinimal.parseFrom(stream); 80 if (incident != null) { 81 parseImages(details.mImages, incident, context.getResources()); 82 parseReasons(details.mReasons, incident); 83 } 84 } 85 } catch (IOException ex) { 86 throw new ParseException("Error while reading stream.", ex); 87 } catch (OutOfMemoryError ex) { 88 throw new ParseException("Out of memory while loading incident report.", ex); 89 } 90 return details; 91 } 92 93 /** 94 * Reads the reasons from the incident headers. Does not throw any exceptions 95 * about validity, because the headers are optional. 96 */ parseReasons(ArrayList<String> result, IncidentMinimal incident)97 private static void parseReasons(ArrayList<String> result, IncidentMinimal incident) { 98 final int headerSize = incident.getHeaderCount(); 99 for (int i = 0; i < headerSize; i++) { 100 final IncidentHeaderProto header = incident.getHeader(i); 101 if (header.hasReason()) { 102 final String reason = header.getReason(); 103 if (reason != null && reason.length() > 0) { 104 result.add(reason); 105 } 106 } 107 } 108 } 109 110 /** 111 * Read images from the IncidentMinimal. 112 * 113 * @throw ParseException if there was an error reading them. 114 */ parseImages(ArrayList<Drawable> result, IncidentMinimal incident, Resources res)115 private static void parseImages(ArrayList<Drawable> result, IncidentMinimal incident, 116 Resources res) throws ParseException { 117 final int totalImageCountLimit = 200; 118 int totalImageCount = 0; 119 120 if (incident.hasRestrictedImagesSection()) { 121 final RestrictedImagesDumpProto section = incident.getRestrictedImagesSection(); 122 final int setsCount = section.getSetsCount(); 123 for (int i = 0; i < setsCount; i++) { 124 final RestrictedImageSetProto set = section.getSets(i); 125 if (set == null) { 126 continue; 127 } 128 final int imageCount = set.getImagesCount(); 129 for (int j = 0; j < imageCount; j++) { 130 // Hard cap on number of images, as a guardrail. 131 totalImageCount++; 132 if (totalImageCount > totalImageCountLimit) { 133 throw new ParseException("Image count is greater than the limit of " 134 + totalImageCountLimit); 135 } 136 137 final RestrictedImageProto image = set.getImages(j); 138 if (image == null) { 139 continue; 140 } 141 final String mimeType = image.getMimeType(); 142 if (!("image/jpeg".equals(mimeType) 143 || "image/png".equals(mimeType))) { 144 throw new ParseException("Unsupported image type " + mimeType); 145 } 146 final ByteString bytes = image.getImageData(); 147 if (bytes == null) { 148 continue; 149 } 150 final byte[] buf = bytes.toByteArray(); 151 if (buf.length == 0) { 152 continue; 153 } 154 155 // This will attempt to uncompress the image. If it's gigantic, 156 // this could fail with OutOfMemoryError, which will be caught 157 // by the caller, and turned into a report rejection. 158 final Drawable drawable = new android.graphics.drawable.BitmapDrawable( 159 res, new ByteArrayInputStream(buf)); 160 161 // TODO: Scale bitmap to correct thumbnail size to save memory. 162 163 result.add(drawable); 164 } 165 } 166 } 167 } 168 169 /** 170 * The "reason" field from any incident report headers, which could contain 171 * explanitory text for why the incident report was taken. 172 */ getReasons()173 public ArrayList<String> getReasons() { 174 return mReasons; 175 } 176 177 /** 178 * Images that must be approved by the user. 179 */ getImages()180 public ArrayList<Drawable> getImages() { 181 return mImages; 182 } 183 } 184