• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 != 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