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.permissioncontroller.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.length() > 0) { 104 result.add(reason); 105 } 106 } 107 } 108 } 109 110 /** 111 * Read images from the IncidentMinimal. 112 * 113 * @throws 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 final int imageCount = set.getImagesCount(); 126 for (int j = 0; j < imageCount; j++) { 127 // Hard cap on number of images, as a guardrail. 128 totalImageCount++; 129 if (totalImageCount > totalImageCountLimit) { 130 throw new ParseException("Image count is greater than the limit of " 131 + totalImageCountLimit); 132 } 133 134 final RestrictedImageProto image = set.getImages(j); 135 final String mimeType = image.getMimeType(); 136 if (!("image/jpeg".equals(mimeType) 137 || "image/png".equals(mimeType))) { 138 throw new ParseException("Unsupported image type " + mimeType); 139 } 140 final ByteString bytes = image.getImageData(); 141 final byte[] buf = bytes.toByteArray(); 142 if (buf.length == 0) { 143 continue; 144 } 145 146 // This will attempt to uncompress the image. If it's gigantic, 147 // this could fail with OutOfMemoryError, which will be caught 148 // by the caller, and turned into a report rejection. 149 final Drawable drawable = new android.graphics.drawable.BitmapDrawable( 150 res, new ByteArrayInputStream(buf)); 151 152 // TODO: Scale bitmap to correct thumbnail size to save memory. 153 154 result.add(drawable); 155 } 156 } 157 } 158 } 159 160 /** 161 * The "reason" field from any incident report headers, which could contain 162 * explanitory text for why the incident report was taken. 163 */ getReasons()164 public ArrayList<String> getReasons() { 165 return mReasons; 166 } 167 168 /** 169 * Images that must be approved by the user. 170 */ getImages()171 public ArrayList<Drawable> getImages() { 172 return mImages; 173 } 174 } 175