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 package com.android.server.notification;
17 
18 import android.annotation.NonNull;
19 import android.app.AlarmManager;
20 import android.app.PendingIntent;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.net.Uri;
26 import android.os.Binder;
27 import android.os.UserHandle;
28 import android.service.notification.StatusBarNotification;
29 import android.util.ArrayMap;
30 import android.util.IntArray;
31 import android.util.Log;
32 import android.util.Slog;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.internal.logging.MetricsLogger;
36 import com.android.internal.logging.nano.MetricsProto;
37 import com.android.modules.utils.TypedXmlPullParser;
38 import com.android.modules.utils.TypedXmlSerializer;
39 import com.android.server.pm.PackageManagerService;
40 
41 import org.xmlpull.v1.XmlPullParser;
42 import org.xmlpull.v1.XmlPullParserException;
43 
44 import java.io.IOException;
45 import java.io.PrintWriter;
46 import java.util.ArrayList;
47 import java.util.Collection;
48 import java.util.Date;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Objects;
52 import java.util.Set;
53 
54 /**
55  * NotificationManagerService helper for handling snoozed notifications.
56  */
57 public final class SnoozeHelper {
58     public static final int XML_SNOOZED_NOTIFICATION_VERSION = 1;
59 
60     static final int CONCURRENT_SNOOZE_LIMIT = 500;
61 
62     // A safe size for strings to be put in persistent storage, to avoid breaking the XML write.
63     static final int MAX_STRING_LENGTH = 1000;
64 
65     protected static final String XML_TAG_NAME = "snoozed-notifications";
66 
67     private static final String XML_SNOOZED_NOTIFICATION = "notification";
68     private static final String XML_SNOOZED_NOTIFICATION_CONTEXT = "context";
69     private static final String XML_SNOOZED_NOTIFICATION_KEY = "key";
70     //the time the snoozed notification should be reposted
71     private static final String XML_SNOOZED_NOTIFICATION_TIME = "time";
72     private static final String XML_SNOOZED_NOTIFICATION_CONTEXT_ID = "id";
73     private static final String XML_SNOOZED_NOTIFICATION_VERSION_LABEL = "version";
74 
75 
76     private static final String TAG = "SnoozeHelper";
77     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
78     private static final String INDENT = "    ";
79 
80     private static final String REPOST_ACTION = SnoozeHelper.class.getSimpleName() + ".EVALUATE";
81     private static final int REQUEST_CODE_REPOST = 1;
82     private static final String REPOST_SCHEME = "repost";
83     static final String EXTRA_KEY = "key";
84     private static final String EXTRA_USER_ID = "userId";
85 
86     private final Context mContext;
87     private AlarmManager mAm;
88     private final ManagedServices.UserProfiles mUserProfiles;
89 
90     // notification key : record.
91     private ArrayMap<String, NotificationRecord> mSnoozedNotifications = new ArrayMap<>();
92     // notification key : time-milliseconds .
93     // This member stores persisted snoozed notification trigger times. it persists through reboots
94     // It should have the notifications that haven't expired or re-posted yet
95     private final ArrayMap<String, Long> mPersistedSnoozedNotifications = new ArrayMap<>();
96     // notification key : creation ID.
97     // This member stores persisted snoozed notification trigger context for the assistant
98     // it persists through reboots.
99     // It should have the notifications that haven't expired or re-posted yet
100     private final ArrayMap<String, String>
101             mPersistedSnoozedNotificationsWithContext = new ArrayMap<>();
102 
103     private Callback mCallback;
104 
105     private final Object mLock = new Object();
106 
SnoozeHelper(Context context, Callback callback, ManagedServices.UserProfiles userProfiles)107     public SnoozeHelper(Context context, Callback callback,
108             ManagedServices.UserProfiles userProfiles) {
109         mContext = context;
110         IntentFilter filter = new IntentFilter(REPOST_ACTION);
111         filter.addDataScheme(REPOST_SCHEME);
112         mContext.registerReceiver(mBroadcastReceiver, filter,
113                 Context.RECEIVER_EXPORTED_UNAUDITED);
114         mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
115         mCallback = callback;
116         mUserProfiles = userProfiles;
117     }
118 
canSnooze(int numberToSnooze)119     protected boolean canSnooze(int numberToSnooze) {
120         synchronized (mLock) {
121             if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT
122                     || (mPersistedSnoozedNotifications.size()
123                     + mPersistedSnoozedNotificationsWithContext.size() + numberToSnooze)
124                     > CONCURRENT_SNOOZE_LIMIT) {
125                 return false;
126             }
127         }
128         return true;
129     }
130 
131     @NonNull
getSnoozeTimeForUnpostedNotification(int userId, String pkg, String key)132     protected Long getSnoozeTimeForUnpostedNotification(int userId, String pkg, String key) {
133         Long time = null;
134         synchronized (mLock) {
135             time = mPersistedSnoozedNotifications.get(getTrimmedString(key));
136         }
137         if (time == null) {
138             time = 0L;
139         }
140         return time;
141     }
142 
getSnoozeContextForUnpostedNotification(int userId, String pkg, String key)143     protected String getSnoozeContextForUnpostedNotification(int userId, String pkg, String key) {
144         synchronized (mLock) {
145             return mPersistedSnoozedNotificationsWithContext.get(getTrimmedString(key));
146         }
147     }
148 
isSnoozed(int userId, String pkg, String key)149     protected boolean isSnoozed(int userId, String pkg, String key) {
150         synchronized (mLock) {
151             return mSnoozedNotifications.containsKey(key);
152         }
153     }
154 
getSnoozed(int userId, String pkg)155     protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) {
156         synchronized (mLock) {
157             ArrayList snoozed = new ArrayList();
158             for (NotificationRecord r : mSnoozedNotifications.values()) {
159                 if (r.getUserId() == userId && r.getSbn().getPackageName().equals(pkg)) {
160                     snoozed.add(r);
161                 }
162             }
163             return snoozed;
164         }
165     }
166 
167     @NonNull
getNotifications(String pkg, String groupKey, Integer userId)168     ArrayList<NotificationRecord> getNotifications(String pkg,
169             String groupKey, Integer userId) {
170         ArrayList<NotificationRecord> records =  new ArrayList<>();
171         synchronized (mLock) {
172             for (int i = 0; i < mSnoozedNotifications.size(); i++) {
173                 NotificationRecord r = mSnoozedNotifications.valueAt(i);
174                 if (r.getSbn().getPackageName().equals(pkg) && r.getUserId() == userId
175                         && Objects.equals(r.getSbn().getGroup(), groupKey)) {
176                     records.add(r);
177                 }
178             }
179         }
180         return records;
181     }
182 
getNotification(String key)183     protected NotificationRecord getNotification(String key) {
184         synchronized (mLock) {
185             return mSnoozedNotifications.get(key);
186         }
187     }
188 
getSnoozed()189     protected @NonNull List<NotificationRecord> getSnoozed() {
190         synchronized (mLock) {
191             // caller filters records based on the current user profiles and listener access,
192             // so just return everything
193             List<NotificationRecord> snoozed = new ArrayList<>();
194             snoozed.addAll(mSnoozedNotifications.values());
195             return snoozed;
196         }
197     }
198 
199     /**
200      * Snoozes a notification and schedules an alarm to repost at that time.
201      */
snooze(NotificationRecord record, long duration)202     protected void snooze(NotificationRecord record, long duration) {
203         String key = record.getKey();
204 
205         snooze(record);
206         scheduleRepost(key, duration);
207         Long activateAt = System.currentTimeMillis() + duration;
208         synchronized (mLock) {
209             mPersistedSnoozedNotifications.put(getTrimmedString(key), activateAt);
210         }
211     }
212 
213     /**
214      * Records a snoozed notification.
215      */
snooze(NotificationRecord record, String contextId)216     protected void snooze(NotificationRecord record, String contextId) {
217         if (contextId != null) {
218             synchronized (mLock) {
219                 mPersistedSnoozedNotificationsWithContext.put(
220                         getTrimmedString(record.getKey()),
221                         getTrimmedString(contextId)
222                 );
223             }
224         }
225         snooze(record);
226     }
227 
snooze(NotificationRecord record)228     private void snooze(NotificationRecord record) {
229         if (DEBUG) {
230             Slog.d(TAG, "Snoozing " + record.getKey());
231         }
232         synchronized (mLock) {
233             mSnoozedNotifications.put(record.getKey(), record);
234         }
235     }
236 
getTrimmedString(String key)237     private String getTrimmedString(String key) {
238         if (key != null && key.length() > MAX_STRING_LENGTH) {
239             return key.substring(0, MAX_STRING_LENGTH);
240         }
241         return key;
242     }
243 
cancel(int userId, String pkg, String tag, int id)244     protected boolean cancel(int userId, String pkg, String tag, int id) {
245         synchronized (mLock) {
246             final Set<Map.Entry<String, NotificationRecord>> records =
247                     mSnoozedNotifications.entrySet();
248             for (Map.Entry<String, NotificationRecord> record : records) {
249                 final StatusBarNotification sbn = record.getValue().getSbn();
250                 if (sbn.getPackageName().equals(pkg) && sbn.getUserId() == userId
251                         && Objects.equals(sbn.getTag(), tag) && sbn.getId() == id) {
252                     record.getValue().isCanceled = true;
253                     return true;
254                 }
255             }
256         }
257         return false;
258     }
259 
cancel(int userId, boolean includeCurrentProfiles)260     protected void cancel(int userId, boolean includeCurrentProfiles) {
261         synchronized (mLock) {
262             if (mSnoozedNotifications.size() == 0) {
263                 return;
264             }
265             IntArray userIds = new IntArray();
266             userIds.add(userId);
267             if (includeCurrentProfiles) {
268                 userIds = mUserProfiles.getCurrentProfileIds();
269             }
270             for (NotificationRecord r : mSnoozedNotifications.values()) {
271                 if (userIds.binarySearch(r.getUserId()) >= 0) {
272                     r.isCanceled = true;
273                 }
274             }
275         }
276     }
277 
cancel(int userId, String pkg)278     protected boolean cancel(int userId, String pkg) {
279         synchronized (mLock) {
280             int n = mSnoozedNotifications.size();
281             for (int i = 0; i < n; i++) {
282                 final NotificationRecord r = mSnoozedNotifications.valueAt(i);
283                 if (r.getSbn().getPackageName().equals(pkg) && r.getUserId() == userId) {
284                     r.isCanceled = true;
285                 }
286             }
287             return true;
288         }
289     }
290 
291     /**
292      * Updates the notification record so the most up to date information is shown on re-post.
293      */
update(int userId, NotificationRecord record)294     protected void update(int userId, NotificationRecord record) {
295         synchronized (mLock) {
296             if (mSnoozedNotifications.containsKey(record.getKey())) {
297                 mSnoozedNotifications.put(record.getKey(), record);
298             }
299         }
300     }
301 
302     /**
303      * Unsnooze & repost all snoozed notifications for userId and its profiles
304      */
repostAll(IntArray userIds)305     protected void repostAll(IntArray userIds) {
306         synchronized (mLock) {
307             List<NotificationRecord> snoozedNotifications = getSnoozed();
308             for (NotificationRecord r : snoozedNotifications) {
309                 if (userIds.binarySearch(r.getUserId()) >= 0) {
310                     repost(r.getKey(), r.getUserId(), false);
311                 }
312             }
313         }
314     }
315 
repost(String key, boolean muteOnReturn)316     protected void repost(String key, boolean muteOnReturn) {
317         synchronized (mLock) {
318             final NotificationRecord r = mSnoozedNotifications.get(key);
319             if (r != null) {
320                 repost(key, r.getUserId(), muteOnReturn);
321             }
322         }
323     }
324 
repost(String key, int userId, boolean muteOnReturn)325     protected void repost(String key, int userId, boolean muteOnReturn) {
326         final String trimmedKey = getTrimmedString(key);
327 
328         NotificationRecord record;
329         synchronized (mLock) {
330             mPersistedSnoozedNotifications.remove(trimmedKey);
331             mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
332             record = mSnoozedNotifications.remove(key);
333         }
334 
335         if (record != null && !record.isCanceled) {
336             final PendingIntent pi = createPendingIntent(record.getKey());
337             mAm.cancel(pi);
338             MetricsLogger.action(record.getLogMaker()
339                     .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
340                     .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
341             mCallback.repost(record.getUserId(), record, muteOnReturn);
342         }
343     }
344 
repostGroupSummary(String pkg, int userId, String groupKey)345     protected void repostGroupSummary(String pkg, int userId, String groupKey) {
346         synchronized (mLock) {
347             String groupSummaryKey = null;
348             int n = mSnoozedNotifications.size();
349             for (int i = 0; i < n; i++) {
350                 final NotificationRecord potentialGroupSummary = mSnoozedNotifications.valueAt(i);
351                 if (potentialGroupSummary.getSbn().getPackageName().equals(pkg)
352                         && potentialGroupSummary.getUserId() == userId
353                         && potentialGroupSummary.getSbn().isGroup()
354                         && potentialGroupSummary.getNotification().isGroupSummary()
355                         && groupKey.equals(potentialGroupSummary.getGroupKey())) {
356                     groupSummaryKey = potentialGroupSummary.getKey();
357                     break;
358                 }
359             }
360 
361             if (groupSummaryKey != null) {
362                 NotificationRecord record = mSnoozedNotifications.remove(groupSummaryKey);
363                 String trimmedKey = getTrimmedString(groupSummaryKey);
364                 mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
365                 mPersistedSnoozedNotifications.remove(trimmedKey);
366 
367                 if (record != null && !record.isCanceled) {
368                     Runnable runnable = () -> {
369                         MetricsLogger.action(record.getLogMaker()
370                                 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
371                                 .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
372                         mCallback.repost(record.getUserId(), record, false);
373                     };
374                     runnable.run();
375                 }
376             }
377         }
378     }
379 
clearData(int userId, String pkg)380     protected void clearData(int userId, String pkg) {
381         synchronized (mLock) {
382             int n = mSnoozedNotifications.size();
383             for (int i = n - 1; i >= 0; i--) {
384                 final NotificationRecord record = mSnoozedNotifications.valueAt(i);
385                 if (record.getUserId() == userId && record.getSbn().getPackageName().equals(pkg)) {
386                     mSnoozedNotifications.removeAt(i);
387                     String trimmedKey = getTrimmedString(record.getKey());
388                     mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
389                     mPersistedSnoozedNotifications.remove(trimmedKey);
390                     Runnable runnable = () -> {
391                         final PendingIntent pi = createPendingIntent(record.getKey());
392                         mAm.cancel(pi);
393                         MetricsLogger.action(record.getLogMaker()
394                                 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
395                                 .setType(MetricsProto.MetricsEvent.TYPE_DISMISS));
396                     };
397                     runnable.run();
398                 }
399             }
400         }
401     }
402 
clearData(int userId)403     protected void clearData(int userId) {
404         synchronized (mLock) {
405             int n = mSnoozedNotifications.size();
406             for (int i = n - 1; i >= 0; i--) {
407                 final NotificationRecord record = mSnoozedNotifications.valueAt(i);
408                 if (record.getUserId() == userId) {
409                     mSnoozedNotifications.removeAt(i);
410                     String trimmedKey = getTrimmedString(record.getKey());
411                     mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
412                     mPersistedSnoozedNotifications.remove(trimmedKey);
413 
414                     Runnable runnable = () -> {
415                         final PendingIntent pi = createPendingIntent(record.getKey());
416                         mAm.cancel(pi);
417                         MetricsLogger.action(record.getLogMaker()
418                                 .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
419                                 .setType(MetricsProto.MetricsEvent.TYPE_DISMISS));
420                     };
421                     runnable.run();
422                 }
423             }
424         }
425     }
426 
createPendingIntent(String key)427     private PendingIntent createPendingIntent(String key) {
428         return PendingIntent.getBroadcast(mContext,
429                 REQUEST_CODE_REPOST,
430                 new Intent(REPOST_ACTION)
431                         .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME)
432                         .setData(new Uri.Builder().scheme(REPOST_SCHEME).appendPath(key).build())
433                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
434                         .putExtra(EXTRA_KEY, key),
435                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
436     }
437 
scheduleRepostsForPersistedNotifications(long currentTime)438     public void scheduleRepostsForPersistedNotifications(long currentTime) {
439         synchronized (mLock) {
440             for (int i = 0; i < mPersistedSnoozedNotifications.size(); i++) {
441                 String key = mPersistedSnoozedNotifications.keyAt(i);
442                 Long time = mPersistedSnoozedNotifications.valueAt(i);
443                 if (time != null && time > currentTime) {
444                     scheduleRepostAtTime(key, time);
445                 }
446             }
447         }
448     }
449 
scheduleRepost(String key, long duration)450     private void scheduleRepost(String key, long duration) {
451         scheduleRepostAtTime(key, System.currentTimeMillis() + duration);
452     }
453 
scheduleRepostAtTime(String key, long time)454     private void scheduleRepostAtTime(String key, long time) {
455         Runnable runnable = () -> {
456             final long identity = Binder.clearCallingIdentity();
457             try {
458                 final PendingIntent pi = createPendingIntent(key);
459                 mAm.cancel(pi);
460                 if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time));
461                 mAm.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi);
462             } finally {
463                 Binder.restoreCallingIdentity(identity);
464             }
465         };
466         runnable.run();
467     }
468 
dump(PrintWriter pw, NotificationManagerService.DumpFilter filter)469     public void dump(PrintWriter pw, NotificationManagerService.DumpFilter filter) {
470         synchronized (mLock) {
471             pw.println("\n  Snoozed notifications:");
472             for (String key : mSnoozedNotifications.keySet()) {
473                 pw.print(INDENT);
474                 pw.println("key: " + key);
475             }
476             pw.println("\n Pending snoozed notifications");
477             for (String key : mPersistedSnoozedNotifications.keySet()) {
478                 pw.print(INDENT);
479                 pw.println("key: " + key + " until: " + mPersistedSnoozedNotifications.get(key));
480             }
481         }
482     }
483 
writeXml(TypedXmlSerializer out)484     protected void writeXml(TypedXmlSerializer out) throws IOException {
485         synchronized (mLock) {
486             final long currentTime = System.currentTimeMillis();
487             out.startTag(null, XML_TAG_NAME);
488             writeXml(out, mPersistedSnoozedNotifications, XML_SNOOZED_NOTIFICATION,
489                     value -> {
490                         if (value < currentTime) {
491                             return;
492                         }
493                         out.attributeLong(null, XML_SNOOZED_NOTIFICATION_TIME,
494                                 value);
495                     });
496             writeXml(out, mPersistedSnoozedNotificationsWithContext,
497                     XML_SNOOZED_NOTIFICATION_CONTEXT,
498                     value -> {
499                         out.attribute(null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID,
500                                 value);
501                     });
502             out.endTag(null, XML_TAG_NAME);
503         }
504     }
505 
506     private interface Inserter<T> {
insert(T t)507         void insert(T t) throws IOException;
508     }
509 
writeXml(TypedXmlSerializer out, ArrayMap<String, T> targets, String tag, Inserter<T> attributeInserter)510     private <T> void writeXml(TypedXmlSerializer out, ArrayMap<String, T> targets, String tag,
511             Inserter<T> attributeInserter) throws IOException {
512         for (int j = 0; j < targets.size(); j++) {
513             String key = targets.keyAt(j);
514             // T is a String (snoozed until context) or Long (snoozed until time)
515             T value = targets.valueAt(j);
516 
517             out.startTag(null, tag);
518 
519             attributeInserter.insert(value);
520 
521             out.attributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL,
522                     XML_SNOOZED_NOTIFICATION_VERSION);
523             out.attribute(null, XML_SNOOZED_NOTIFICATION_KEY, key);
524 
525             out.endTag(null, tag);
526         }
527     }
528 
readXml(TypedXmlPullParser parser, long currentTime)529     protected void readXml(TypedXmlPullParser parser, long currentTime)
530             throws XmlPullParserException, IOException {
531         int type;
532         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
533             String tag = parser.getName();
534             if (type == XmlPullParser.END_TAG
535                     && XML_TAG_NAME.equals(tag)) {
536                 break;
537             }
538             if (type == XmlPullParser.START_TAG
539                     && (XML_SNOOZED_NOTIFICATION.equals(tag)
540                         || tag.equals(XML_SNOOZED_NOTIFICATION_CONTEXT))
541                     && parser.getAttributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL, -1)
542                         == XML_SNOOZED_NOTIFICATION_VERSION) {
543                 try {
544                     final String key = parser.getAttributeValue(null, XML_SNOOZED_NOTIFICATION_KEY);
545                     if (tag.equals(XML_SNOOZED_NOTIFICATION)) {
546                         final Long time = parser.getAttributeLong(
547                                 null, XML_SNOOZED_NOTIFICATION_TIME, 0);
548                         if (time > currentTime) { //only read new stuff
549                             synchronized (mLock) {
550                                 mPersistedSnoozedNotifications.put(key, time);
551                             }
552                         }
553                     }
554                     if (tag.equals(XML_SNOOZED_NOTIFICATION_CONTEXT)) {
555                         final String creationId = parser.getAttributeValue(
556                                 null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID);
557                         synchronized (mLock) {
558                             mPersistedSnoozedNotificationsWithContext.put(key, creationId);
559                         }
560                     }
561                 } catch (Exception e) {
562                     Slog.e(TAG,  "Exception in reading snooze data from policy xml", e);
563                 }
564             }
565         }
566     }
567 
568     @VisibleForTesting
setAlarmManager(AlarmManager am)569     void setAlarmManager(AlarmManager am) {
570         mAm = am;
571     }
572 
573     protected interface Callback {
repost(int userId, NotificationRecord r, boolean muteOnReturn)574         void repost(int userId, NotificationRecord r, boolean muteOnReturn);
575     }
576 
577     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
578         @Override
579         public void onReceive(Context context, Intent intent) {
580             if (DEBUG) {
581                 Slog.d(TAG, "Reposting notification");
582             }
583             if (REPOST_ACTION.equals(intent.getAction())) {
584                 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID,
585                         UserHandle.USER_SYSTEM), false);
586             }
587         }
588     };
589 }
590