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