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