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