1 /*
2  * Copyright (C) 2018 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.app.AlertDialog;
20 import android.content.DialogInterface;
21 import android.content.DialogInterface.OnClickListener;
22 import android.content.DialogInterface.OnDismissListener;
23 import android.content.res.Resources;
24 import android.graphics.drawable.Drawable;
25 import android.net.Uri;
26 import android.os.Bundle;
27 import android.os.IncidentManager;
28 import android.provider.Settings;
29 import android.text.Spannable;
30 import android.text.SpannableString;
31 import android.text.style.BulletSpan;
32 import android.util.Log;
33 import android.view.View;
34 import android.view.Window;
35 import android.view.WindowManager;
36 import android.widget.ImageView;
37 import android.widget.LinearLayout;
38 import android.widget.TextView;
39 
40 import androidx.activity.ComponentActivity;
41 
42 import com.android.modules.utils.build.SdkLevel;
43 import com.android.permissioncontroller.DeviceUtils;
44 import com.android.permissioncontroller.R;
45 import com.android.permissioncontroller.incident.wear.ConfirmationActivityWearViewHandler;
46 
47 import java.util.ArrayList;
48 
49 /**
50  * Confirmation dialog for approving an incident or bug report for sharing off the device.
51  */
52 public class ConfirmationActivity extends ComponentActivity implements OnClickListener,
53         OnDismissListener {
54     private static final String TAG = "ConfirmationActivity";
55 
56     /**
57      * Currently displaying activity.
58      */
59     private static ConfirmationActivity sCurrentActivity;
60 
61     /**
62      * Currently displaying uri.
63      */
64     private static Uri sCurrentUri;
65 
66     /**
67      * If this activity is running in the current process, call finish() on it.
68      */
finishCurrent()69     public static void finishCurrent() {
70         if (sCurrentActivity != null) {
71             sCurrentActivity.finish();
72         }
73     }
74 
75     /**
76      * If the activity is in the resumed state, then record the Uri for the current
77      * one, so PendingList can skip re-showing the same one.
78      */
getCurrentUri()79     public static Uri getCurrentUri() {
80         return sCurrentUri;
81     }
82 
83     /**
84      * Create the activity.
85      */
86     @Override
onCreate(Bundle savedInstanceState)87     protected void onCreate(Bundle savedInstanceState) {
88         super.onCreate(savedInstanceState);
89 
90         final Formatting formatting = new Formatting(this);
91 
92         final Uri uri = getIntent().getData();
93         Log.d(TAG, "uri=" + uri);
94         if (uri == null) {
95             Log.w(TAG, "No uri in intent: " + getIntent());
96             finish();
97             return;
98         }
99 
100         final IncidentManager.PendingReport pending = new IncidentManager.PendingReport(uri);
101         final String appLabel = formatting.getAppLabel(pending.getRequestingPackage());
102 
103         final Resources res = getResources();
104 
105         ReportDetails details;
106         try {
107             details = ReportDetails.parseIncidentReport(this, uri);
108         } catch (ReportDetails.ParseException ex) {
109             Log.w("Rejecting report because it couldn't be parsed", ex);
110             // If there was an error in the input we will just summarily reject the upload,
111             // since we can't get proper approval. (Zero-length images or reasons means that
112             // we will proceed with the imageless consent dialog).
113             final IncidentManager incidentManager = getSystemService(IncidentManager.class);
114             incidentManager.denyReport(uri);
115 
116             if (DeviceUtils.isWear(this)) {
117                 ConfirmationActivityWearViewHandler viewHandler =
118                         new ConfirmationActivityWearViewHandler(this, incidentManager);
119                 setContentView(viewHandler.createView());
120                 viewHandler.updateViewModel(true, getString(R.string.incident_report_dialog_title),
121                         getString(R.string.incident_report_error_dialog_text, appLabel), uri);
122                 return;
123             }
124 
125             // Show a message to the user saying... nevermind.
126             new AlertDialog.Builder(this)
127                 .setTitle(R.string.incident_report_dialog_title)
128                 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
129                             @Override
130                             public void onClick(DialogInterface dialog, int which) {
131                                 finish();
132                             }
133                         })
134                 .setMessage(getString(R.string.incident_report_error_dialog_text, appLabel))
135                 .setOnDismissListener(this)
136                 .show();
137             return;
138 
139         }
140 
141         final String message = getString(R.string.incident_report_dialog_text,
142                 appLabel,
143                 formatting.getDate(pending.getTimestamp()),
144                 formatting.getTime(pending.getTimestamp()),
145                 appLabel);
146 
147         if (DeviceUtils.isWear(this)) {
148             ConfirmationActivityWearViewHandler viewHandler =
149                     new ConfirmationActivityWearViewHandler(this,
150                             getSystemService(IncidentManager.class));
151             setContentView(viewHandler.createView());
152             viewHandler.updateViewModel(false, getString(R.string.incident_report_dialog_title),
153                     message, uri);
154             return;
155         }
156 
157         final View content = getLayoutInflater().inflate(R.layout.incident_confirmation,
158                 null);
159 
160         final ArrayList<String> reasons = details.getReasons();
161         final int reasonsSize = reasons.size();
162         if (reasonsSize > 0) {
163             content.findViewById(R.id.reasonIntro).setVisibility(View.VISIBLE);
164 
165             final TextView reasonTextView = (TextView) content.findViewById(R.id.reasons);
166             reasonTextView.setVisibility(View.VISIBLE);
167 
168             final int bulletSize =
169                     (int) (res.getDimension(R.dimen.incident_reason_bullet_size) + 0.5f);
170             final int bulletIndent =
171                     (int) (res.getDimension(R.dimen.incident_reason_bullet_indent) + 0.5f);
172             final int bulletColor =
173                     getColor(R.color.incident_reason_bullet_color);
174 
175             final StringBuilder text = new StringBuilder();
176             for (int i = 0; i < reasonsSize; i++) {
177                 text.append(reasons.get(i));
178                 if (i != reasonsSize - 1) {
179                     text.append("\n");
180                 }
181             }
182             final SpannableString spannable = new SpannableString(text.toString());
183             int spanStart = 0;
184             for (int i = 0; i < reasonsSize; i++) {
185                 final int length = reasons.get(i).length();
186                 spannable.setSpan(new BulletSpan(bulletIndent, bulletColor, bulletSize),
187                         spanStart, spanStart + length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
188                 spanStart += length + 1;
189             }
190 
191             reasonTextView.setText(spannable);
192         }
193 
194         ((TextView) content.findViewById(R.id.message)).setText(message);
195 
196         final ArrayList<Drawable> images = details.getImages();
197         final int imagesSize = images.size();
198         if (imagesSize > 0) {
199             content.findViewById(R.id.imageScrollView).setVisibility(View.VISIBLE);
200 
201             final LinearLayout imageList = (LinearLayout) content.findViewById(R.id.imageList);
202 
203             final int width = res.getDimensionPixelSize(R.dimen.incident_image_width);
204             final int height = res.getDimensionPixelSize(R.dimen.incident_image_height);
205 
206             for (int i = 0; i < imagesSize; i++) {
207                 final ImageView imageView = new ImageView(this);
208                 imageView.setImageDrawable(images.get(i));
209                 imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
210 
211                 imageList.addView(imageView, new LinearLayout.LayoutParams(width, height));
212             }
213         }
214 
215         final AlertDialog dialog = new AlertDialog.Builder(this)
216                 .setTitle(R.string.incident_report_dialog_title)
217                 .setPositiveButton(R.string.incident_report_dialog_allow_label, this)
218                 .setNegativeButton(R.string.incident_report_dialog_deny_label, this)
219                 .setOnDismissListener(this)
220                 .setView(content)
221                 .create();
222         if (Settings.canDrawOverlays(this)) {
223             final Window w = dialog.getWindow();
224             if (SdkLevel.isAtLeastT()) {
225                 WindowManager.LayoutParams lpm = new WindowManager.LayoutParams();
226                 lpm.setSystemApplicationOverlay(true);
227                 w.setAttributes(lpm);
228             }
229             w.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
230         }
231         dialog.show();
232     }
233 
234     /**
235      * Activity lifecycle callback.  Now visible.
236      */
237     @Override
onStart()238     protected void onStart() {
239         super.onStart();
240         sCurrentActivity = this;
241         sCurrentUri = getIntent().getData();
242     }
243 
244     /**
245      * Activity lifecycle callback.  Now not visible.
246      */
247     @Override
onStop()248     protected void onStop() {
249         super.onStop();
250         sCurrentActivity = null;
251         sCurrentUri = null;
252     }
253 
254     /**
255      * Dialog canceled.
256      */
257     @Override
onDismiss(DialogInterface dialog)258     public void onDismiss(DialogInterface dialog) {
259         finish();
260     }
261 
262     /**
263      * Explicit button click.
264      */
265     @Override
onClick(DialogInterface dialog, int which)266     public void onClick(DialogInterface dialog, int which) {
267         final IncidentManager incidentManager = getSystemService(IncidentManager.class);
268 
269         switch (which) {
270             case DialogInterface.BUTTON_POSITIVE:
271                 incidentManager.approveReport(getIntent().getData());
272                 PendingList.getInstance().updateState(this, 0);
273                 break;
274             case DialogInterface.BUTTON_NEGATIVE:
275                 incidentManager.denyReport(getIntent().getData());
276                 PendingList.getInstance().updateState(this, 0);
277                 break;
278         }
279         finish();
280     }
281 }
282