1 /* 2 * Copyright (C) 2016 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.Notification; 20 import android.app.NotificationChannel; 21 import android.app.NotificationManager; 22 import android.app.PendingIntent; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.SharedPreferences; 26 import android.os.IncidentManager; 27 import android.util.ArraySet; 28 import android.util.Log; 29 30 import com.android.permissioncontroller.Constants; 31 import com.android.permissioncontroller.R; 32 33 import java.text.Collator; 34 import java.text.SimpleDateFormat; 35 import java.util.ArrayList; 36 import java.util.Date; 37 import java.util.List; 38 import java.util.Set; 39 40 /** 41 * Represents the current list of pending records. 42 */ 43 class PendingList { 44 private static final String TAG = "PermissionController.incident"; 45 46 /** 47 * Flag for {@link #UpdateState} to flag whether this update is coming from the 48 * notification handling. If it is, then no dialogs will be shown. 49 */ 50 static final int FLAG_FROM_NOTIFICATION = 0x1; 51 52 /** 53 * Shared preferences file name. 54 */ 55 private static final String SHARED_PREFS_NAME = 56 "com.android.packageinstaller.incident.PendingList"; 57 58 /** 59 * Key for the list of currently showing notifications. 60 */ 61 private static final String SHARED_PREFS_KEY_NOTIFICATIONS = "notifications"; 62 63 /** 64 * Singleton instance. 65 */ 66 private static final PendingList sInstance = new PendingList(); 67 68 /** 69 * Date format that will sort lexicographical, so we can have our notifications sorted. 70 */ 71 private static final SimpleDateFormat sDateFormatter = 72 new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 73 74 /** 75 * List of currently pending records. 76 */ 77 private static class Rec { 78 /** 79 * Constructor. 80 */ Rec(IncidentManager.PendingReport r, String l)81 Rec(IncidentManager.PendingReport r, String l) { 82 this.report = r; 83 this.label = l; 84 } 85 86 /** 87 * The incident report to show. 88 */ 89 public final IncidentManager.PendingReport report; 90 91 /** 92 * The user-visible name of the entry. 93 */ 94 public final String label; 95 } 96 97 /** 98 * Class to update the state. Holds the Context, and other system services for 99 * the duration of the update. 100 */ 101 private static class Updater { 102 private final Context mContext; 103 private final int mFlags; 104 private final NotificationManager mNm; 105 private final Formatting mFormatting; 106 private Collator mCollator; 107 108 /** 109 * Constructor. 110 */ Updater(Context context, int flags)111 Updater(Context context, int flags) { 112 mContext = context; 113 mFlags = flags; 114 mNm = context.getSystemService(NotificationManager.class); 115 mFormatting = new Formatting(context); 116 mCollator = Collator.getInstance( 117 context.getResources().getConfiguration().getLocales().get(0)); 118 } 119 120 /** 121 * Perform the update. 122 */ updateState()123 void updateState() { 124 final IncidentManager incidentManager = 125 mContext.getSystemService(IncidentManager.class); 126 final List<IncidentManager.PendingReport> reports = incidentManager.getPendingReports(); 127 128 // Load whatever we previously displayed. This may result in some spurious 129 // cancel calls across reboots... but that's not an actual problem. 130 final SharedPreferences prefs = mContext.getSharedPreferences(SHARED_PREFS_NAME, 131 Context.MODE_PRIVATE); 132 final Set<String> prevNotifications = 133 prefs.getStringSet(SHARED_PREFS_KEY_NOTIFICATIONS, null); 134 final ArraySet<String> remainingNotifications = new ArraySet<String>(); 135 if (prevNotifications != null) { 136 for (final String s: prevNotifications) { 137 remainingNotifications.add(s); 138 } 139 } 140 final ArraySet<String> currentNotifications = new ArraySet<String>(); 141 142 // Load everything we will need for display 143 final List<Rec> recs = new ArrayList(); 144 final int recCount = reports.size(); 145 for (int i = 0; i < recCount; i++) { 146 final IncidentManager.PendingReport report = reports.get(i); 147 final String label = mFormatting.getAppLabel(report.getRequestingPackage()); 148 if (label == null) { 149 Log.w(TAG, "Application (or its label) could not be found. Summarily " 150 + " denying report: " + report.getRequestingPackage()); 151 incidentManager.denyReport(report.getUri()); 152 continue; 153 } 154 155 recs.add(new Rec(report, label)); 156 } 157 158 // Sort by timestamp, then by label name (for a stable ordering, with the assumption 159 // that apps only post one at a time). 160 recs.sort((a, b) -> { 161 long val = a.report.getTimestamp() - b.report.getTimestamp(); 162 if (val == 0) { 163 return mCollator.compare(a.label, b.label); 164 } else { 165 return val < 0 ? -1 : 1; 166 } 167 }); 168 169 // Collect what we are going to do. 170 Rec firstDialog = null; 171 final List<Rec> notificationRecs = new ArrayList(); 172 final int notificationCount = recs.size(); 173 for (int i = 0; i < notificationCount; i++) { 174 final Rec rec = recs.get(i); 175 notificationRecs.add(rec); 176 final String uri = rec.report.getUri().toString(); 177 remainingNotifications.remove(uri); 178 currentNotifications.add(uri); 179 if ((rec.report.getFlags() & IncidentManager.FLAG_CONFIRMATION_DIALOG) != 0) { 180 if (firstDialog == null) { 181 firstDialog = rec; 182 } 183 } 184 } 185 186 if (false) { 187 Log.d(TAG, "PermissionController pending list plan ... {"); 188 Log.d(TAG, " showing {"); 189 for (int i = 0; i < notificationRecs.size(); i++) { 190 Log.d(TAG, " [" + i + "] " + notificationRecs.get(i).report.getUri()); 191 } 192 Log.d(TAG, " }"); 193 Log.d(TAG, " canceling {"); 194 for (int i = 0; i < remainingNotifications.size(); i++) { 195 Log.d(TAG, " [" + i + "] " + remainingNotifications.valueAt(i)); 196 } 197 Log.d(TAG, " }"); 198 Log.d(TAG, "}"); 199 } 200 201 // Show the notifications 202 showNotifications(notificationRecs); 203 204 // Cancel any previously remaining notifications 205 final int remainingCount = remainingNotifications.size(); 206 for (int i = 0; i < remainingCount; i++) { 207 mNm.cancel(remainingNotifications.valueAt(i), Constants.INCIDENT_NOTIFICATION_ID); 208 } 209 210 // The dialog 211 if (firstDialog != null) { 212 // Show the new dialog. The FLAG_ACTIVITY_CLEAR_TASK in the intent 213 // will remove any previously showing dialog. We check the static 214 // on ConfirmationActivity so that if the dialog is currently on 215 // top, for the same Uri, then we won't cause jank by re-showing 216 // the same one. 217 if (!firstDialog.report.getUri().equals(ConfirmationActivity.getCurrentUri())) { 218 if ((mFlags & FLAG_FROM_NOTIFICATION) == 0) { 219 mContext.startActivity(newDialogIntent(firstDialog)); 220 } 221 } 222 } else { 223 // Cancel any previously showing one. The activity has the noHistory 224 // flag set in the manifest, so we know that if won't be somewhere in 225 // the background, waiting to come back. 226 ConfirmationActivity.finishCurrent(); 227 } 228 229 // Save this list, so we know what we did for next time. 230 final SharedPreferences.Editor editor = prefs.edit(); 231 editor.putStringSet(SHARED_PREFS_KEY_NOTIFICATIONS, currentNotifications); 232 editor.apply(); 233 } 234 235 /** 236 * Show the list of notifications and cancel any unneeded ones. 237 */ showNotifications(List<Rec> recs)238 private void showNotifications(List<Rec> recs) { 239 createNotificationChannel(); 240 241 final int recCount = recs.size(); 242 for (int i = 0; i < recCount; i++) { 243 final Rec rec = recs.get(i); 244 245 // Intent for the confirmation dialog. 246 final PendingIntent dialog = PendingIntent.getActivity(mContext, 0, 247 newDialogIntent(rec), 0); 248 249 // Intent for the approval and denial. 250 final PendingIntent deny = PendingIntent.getBroadcast(mContext, 0, 251 new Intent(ApprovalReceiver.ACTION_DENY, rec.report.getUri(), 252 mContext, ApprovalReceiver.class), 253 0); 254 255 // Construct the notification 256 final Notification notification = new Notification.Builder(mContext) 257 .setStyle(new Notification.BigTextStyle()) 258 .setContentTitle( 259 mContext.getString(R.string.incident_report_notification_title)) 260 .setContentText( 261 mContext.getString(R.string.incident_report_notification_text, 262 rec.label)) 263 .setSmallIcon(R.drawable.ic_bug_report_black_24dp) 264 .setWhen(rec.report.getTimestamp()) 265 .setGroup(Constants.INCIDENT_NOTIFICATION_GROUP_KEY) 266 .setChannelId(Constants.INCIDENT_NOTIFICATION_CHANNEL_ID) 267 .setSortKey(getSortKey(rec.report.getTimestamp())) 268 .setContentIntent(dialog) 269 .setDeleteIntent(deny) 270 .setColor(mContext.getColor( 271 android.R.color.system_notification_accent_color)) 272 .extend(new Notification.TvExtender()) 273 .build(); 274 275 // Show the notification 276 mNm.notify(rec.report.getUri().toString(), Constants.INCIDENT_NOTIFICATION_ID, 277 notification); 278 } 279 } 280 281 /** 282 * Create the notification channel for {@link #NOTIFICATION_CHANNEL_ID}. 283 */ createNotificationChannel()284 private void createNotificationChannel() { 285 final NotificationChannel channel = new NotificationChannel( 286 Constants.INCIDENT_NOTIFICATION_CHANNEL_ID, 287 mContext.getString(R.string.incident_report_channel_name), 288 NotificationManager.IMPORTANCE_DEFAULT); 289 290 // TODO: Not in SystemApi, so we can't use it. 291 // channel.setBlockableSystem(true); 292 293 mNm.createNotificationChannel(channel); 294 } 295 296 /** 297 * Get the sort key for the order of our notifications. 298 */ getSortKey(long timestamp)299 private String getSortKey(long timestamp) { 300 return sDateFormatter.format(new Date(timestamp)); 301 } 302 303 /** 304 * Create the intent to launch the dialog activity for the Rec. 305 */ newDialogIntent(Rec rec)306 private Intent newDialogIntent(Rec rec) { 307 final Intent result = new Intent(Intent.ACTION_MAIN, rec.report.getUri(), 308 mContext, ConfirmationActivity.class); 309 result.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 310 return result; 311 } 312 } 313 314 /** 315 * Get the singleton instance. Note that there is no Context associated 316 * with this object. The context should be passed in to updateState, and 317 * the assumption is that it could be a background context (i.e. the one for a 318 * BroadcastReceiver), so no direct UI can be done on it as it would be with 319 * an Activity object. 320 */ getInstance()321 public static PendingList getInstance() { 322 return sInstance; 323 } 324 325 /** 326 * Constructor. 327 */ PendingList()328 private PendingList() { 329 } 330 331 /** 332 * Update the notifications and dialog to reflect the current state of affairs. 333 */ updateState(Context context, int flags)334 public void updateState(Context context, int flags) { 335 (new Updater(context, flags)).updateState(); 336 } 337 } 338