Home | History | Annotate | Download | only in notification
      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 com.android.internal.annotations.VisibleForTesting;
     19 import com.android.internal.logging.MetricsLogger;
     20 import com.android.internal.logging.nano.MetricsProto;
     21 
     22 import org.xmlpull.v1.XmlPullParser;
     23 import org.xmlpull.v1.XmlPullParserException;
     24 import org.xmlpull.v1.XmlSerializer;
     25 
     26 import android.annotation.NonNull;
     27 import android.app.AlarmManager;
     28 import android.app.Notification;
     29 import android.app.PendingIntent;
     30 import android.content.BroadcastReceiver;
     31 import android.content.Context;
     32 import android.content.Intent;
     33 import android.content.IntentFilter;
     34 import android.net.Uri;
     35 import android.os.Binder;
     36 import android.os.SystemClock;
     37 import android.os.UserHandle;
     38 import android.service.notification.StatusBarNotification;
     39 import android.util.ArrayMap;
     40 import android.util.Log;
     41 import android.util.Slog;
     42 
     43 import java.io.IOException;
     44 import java.io.PrintWriter;
     45 import java.util.ArrayList;
     46 import java.util.Collection;
     47 import java.util.Collections;
     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 class SnoozeHelper {
     58     private static final String TAG = "SnoozeHelper";
     59     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     60     private static final String INDENT = "    ";
     61 
     62     private static final String REPOST_ACTION = SnoozeHelper.class.getSimpleName() + ".EVALUATE";
     63     private static final int REQUEST_CODE_REPOST = 1;
     64     private static final String REPOST_SCHEME = "repost";
     65     private static final String EXTRA_KEY = "key";
     66     private static final String EXTRA_USER_ID = "userId";
     67 
     68     private final Context mContext;
     69     private AlarmManager mAm;
     70     private final ManagedServices.UserProfiles mUserProfiles;
     71 
     72     // User id : package name : notification key : record.
     73     private ArrayMap<Integer, ArrayMap<String, ArrayMap<String, NotificationRecord>>>
     74             mSnoozedNotifications = new ArrayMap<>();
     75     // notification key : package.
     76     private ArrayMap<String, String> mPackages = new ArrayMap<>();
     77     // key : userId
     78     private ArrayMap<String, Integer> mUsers = new ArrayMap<>();
     79     private Callback mCallback;
     80 
     81     public SnoozeHelper(Context context, Callback callback,
     82             ManagedServices.UserProfiles userProfiles) {
     83         mContext = context;
     84         IntentFilter filter = new IntentFilter(REPOST_ACTION);
     85         filter.addDataScheme(REPOST_SCHEME);
     86         mContext.registerReceiver(mBroadcastReceiver, filter);
     87         mAm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
     88         mCallback = callback;
     89         mUserProfiles = userProfiles;
     90     }
     91 
     92     protected boolean isSnoozed(int userId, String pkg, String key) {
     93         return mSnoozedNotifications.containsKey(userId)
     94                 && mSnoozedNotifications.get(userId).containsKey(pkg)
     95                 && mSnoozedNotifications.get(userId).get(pkg).containsKey(key);
     96     }
     97 
     98     protected Collection<NotificationRecord> getSnoozed(int userId, String pkg) {
     99         if (mSnoozedNotifications.containsKey(userId)
    100                 && mSnoozedNotifications.get(userId).containsKey(pkg)) {
    101             return mSnoozedNotifications.get(userId).get(pkg).values();
    102         }
    103         return Collections.EMPTY_LIST;
    104     }
    105 
    106     protected @NonNull List<NotificationRecord> getSnoozed() {
    107         List<NotificationRecord> snoozedForUser = new ArrayList<>();
    108         int[] userIds = mUserProfiles.getCurrentProfileIds();
    109         if (userIds != null) {
    110             final int N = userIds.length;
    111             for (int i = 0; i < N; i++) {
    112                 final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
    113                         mSnoozedNotifications.get(userIds[i]);
    114                 if (snoozedPkgs != null) {
    115                     final int M = snoozedPkgs.size();
    116                     for (int j = 0; j < M; j++) {
    117                         final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
    118                         if (records != null) {
    119                             snoozedForUser.addAll(records.values());
    120                         }
    121                     }
    122                 }
    123             }
    124         }
    125         return snoozedForUser;
    126     }
    127 
    128     /**
    129      * Snoozes a notification and schedules an alarm to repost at that time.
    130      */
    131     protected void snooze(NotificationRecord record, long duration) {
    132         snooze(record);
    133         scheduleRepost(record.sbn.getPackageName(), record.getKey(), record.getUserId(), duration);
    134     }
    135 
    136     /**
    137      * Records a snoozed notification.
    138      */
    139     protected void snooze(NotificationRecord record) {
    140         int userId = record.getUser().getIdentifier();
    141         if (DEBUG) {
    142             Slog.d(TAG, "Snoozing " + record.getKey());
    143         }
    144         ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
    145                 mSnoozedNotifications.get(userId);
    146         if (records == null) {
    147             records = new ArrayMap<>();
    148         }
    149         ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
    150         if (pkgRecords == null) {
    151             pkgRecords = new ArrayMap<>();
    152         }
    153         pkgRecords.put(record.getKey(), record);
    154         records.put(record.sbn.getPackageName(), pkgRecords);
    155         mSnoozedNotifications.put(userId, records);
    156         mPackages.put(record.getKey(), record.sbn.getPackageName());
    157         mUsers.put(record.getKey(), userId);
    158     }
    159 
    160     protected boolean cancel(int userId, String pkg, String tag, int id) {
    161         if (mSnoozedNotifications.containsKey(userId)) {
    162             ArrayMap<String, NotificationRecord> recordsForPkg =
    163                     mSnoozedNotifications.get(userId).get(pkg);
    164             if (recordsForPkg != null) {
    165                 final Set<Map.Entry<String, NotificationRecord>> records = recordsForPkg.entrySet();
    166                 String key = null;
    167                 for (Map.Entry<String, NotificationRecord> record : records) {
    168                     final StatusBarNotification sbn = record.getValue().sbn;
    169                     if (Objects.equals(sbn.getTag(), tag) && sbn.getId() == id) {
    170                         record.getValue().isCanceled = true;
    171                         return true;
    172                     }
    173                 }
    174             }
    175         }
    176         return false;
    177     }
    178 
    179     protected boolean cancel(int userId, boolean includeCurrentProfiles) {
    180         int[] userIds = {userId};
    181         if (includeCurrentProfiles) {
    182             userIds = mUserProfiles.getCurrentProfileIds();
    183         }
    184         final int N = userIds.length;
    185         for (int i = 0; i < N; i++) {
    186             final ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
    187                     mSnoozedNotifications.get(userIds[i]);
    188             if (snoozedPkgs != null) {
    189                 final int M = snoozedPkgs.size();
    190                 for (int j = 0; j < M; j++) {
    191                     final ArrayMap<String, NotificationRecord> records = snoozedPkgs.valueAt(j);
    192                     if (records != null) {
    193                         int P = records.size();
    194                         for (int k = 0; k < P; k++) {
    195                             records.valueAt(k).isCanceled = true;
    196                         }
    197                     }
    198                 }
    199                 return true;
    200             }
    201         }
    202         return false;
    203     }
    204 
    205     protected boolean cancel(int userId, String pkg) {
    206         if (mSnoozedNotifications.containsKey(userId)) {
    207             if (mSnoozedNotifications.get(userId).containsKey(pkg)) {
    208                 ArrayMap<String, NotificationRecord> records =
    209                         mSnoozedNotifications.get(userId).get(pkg);
    210                 int N = records.size();
    211                 for (int i = 0; i < N; i++) {
    212                     records.valueAt(i).isCanceled = true;
    213                 }
    214                 return true;
    215             }
    216         }
    217         return false;
    218     }
    219 
    220     /**
    221      * Updates the notification record so the most up to date information is shown on re-post.
    222      */
    223     protected void update(int userId, NotificationRecord record) {
    224         ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
    225                 mSnoozedNotifications.get(userId);
    226         if (records == null) {
    227             return;
    228         }
    229         ArrayMap<String, NotificationRecord> pkgRecords = records.get(record.sbn.getPackageName());
    230         if (pkgRecords == null) {
    231             return;
    232         }
    233         NotificationRecord existing = pkgRecords.get(record.getKey());
    234         if (existing != null && existing.isCanceled) {
    235             return;
    236         }
    237         pkgRecords.put(record.getKey(), record);
    238     }
    239 
    240     protected void repost(String key) {
    241         Integer userId = mUsers.get(key);
    242         if (userId != null) {
    243             repost(key, userId);
    244         }
    245     }
    246 
    247     protected void repost(String key, int userId) {
    248         final String pkg = mPackages.remove(key);
    249         ArrayMap<String, ArrayMap<String, NotificationRecord>> records =
    250                 mSnoozedNotifications.get(userId);
    251         if (records == null) {
    252             return;
    253         }
    254         ArrayMap<String, NotificationRecord> pkgRecords = records.get(pkg);
    255         if (pkgRecords == null) {
    256             return;
    257         }
    258         final NotificationRecord record = pkgRecords.remove(key);
    259         mPackages.remove(key);
    260         mUsers.remove(key);
    261 
    262         if (record != null && !record.isCanceled) {
    263             MetricsLogger.action(record.getLogMaker()
    264                     .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
    265                     .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
    266             mCallback.repost(userId, record);
    267         }
    268     }
    269 
    270     protected void repostGroupSummary(String pkg, int userId, String groupKey) {
    271         if (mSnoozedNotifications.containsKey(userId)) {
    272             ArrayMap<String, ArrayMap<String, NotificationRecord>> keysByPackage
    273                     = mSnoozedNotifications.get(userId);
    274 
    275             if (keysByPackage != null && keysByPackage.containsKey(pkg)) {
    276                 ArrayMap<String, NotificationRecord> recordsByKey = keysByPackage.get(pkg);
    277 
    278                 if (recordsByKey != null) {
    279                     String groupSummaryKey = null;
    280                     int N = recordsByKey.size();
    281                     for (int i = 0; i < N; i++) {
    282                         final NotificationRecord potentialGroupSummary = recordsByKey.valueAt(i);
    283                         if (potentialGroupSummary.sbn.isGroup()
    284                                 && potentialGroupSummary.getNotification().isGroupSummary()
    285                                 && groupKey.equals(potentialGroupSummary.getGroupKey())) {
    286                             groupSummaryKey = potentialGroupSummary.getKey();
    287                             break;
    288                         }
    289                     }
    290 
    291                     if (groupSummaryKey != null) {
    292                         NotificationRecord record = recordsByKey.remove(groupSummaryKey);
    293                         mPackages.remove(groupSummaryKey);
    294                         mUsers.remove(groupSummaryKey);
    295 
    296                         if (record != null && !record.isCanceled) {
    297                             MetricsLogger.action(record.getLogMaker()
    298                                     .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED)
    299                                     .setType(MetricsProto.MetricsEvent.TYPE_OPEN));
    300                             mCallback.repost(userId, record);
    301                         }
    302                     }
    303                 }
    304             }
    305         }
    306     }
    307 
    308     private PendingIntent createPendingIntent(String pkg, String key, int userId) {
    309         return PendingIntent.getBroadcast(mContext,
    310                 REQUEST_CODE_REPOST,
    311                 new Intent(REPOST_ACTION)
    312                         .setData(new Uri.Builder().scheme(REPOST_SCHEME).appendPath(key).build())
    313                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
    314                         .putExtra(EXTRA_KEY, key)
    315                         .putExtra(EXTRA_USER_ID, userId),
    316                 PendingIntent.FLAG_UPDATE_CURRENT);
    317     }
    318 
    319     private void scheduleRepost(String pkg, String key, int userId, long duration) {
    320         long identity = Binder.clearCallingIdentity();
    321         try {
    322             final PendingIntent pi = createPendingIntent(pkg, key, userId);
    323             mAm.cancel(pi);
    324             long time = SystemClock.elapsedRealtime() + duration;
    325             if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + new Date(time));
    326             mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pi);
    327         } finally {
    328             Binder.restoreCallingIdentity(identity);
    329         }
    330     }
    331 
    332     public void dump(PrintWriter pw, NotificationManagerService.DumpFilter filter) {
    333         pw.println("\n  Snoozed notifications:");
    334         for (int userId : mSnoozedNotifications.keySet()) {
    335             pw.print(INDENT);
    336             pw.println("user: " + userId);
    337             ArrayMap<String, ArrayMap<String, NotificationRecord>> snoozedPkgs =
    338                     mSnoozedNotifications.get(userId);
    339             for (String pkg : snoozedPkgs.keySet()) {
    340                 pw.print(INDENT);
    341                 pw.print(INDENT);
    342                 pw.println("package: " + pkg);
    343                 Set<String> snoozedKeys = snoozedPkgs.get(pkg).keySet();
    344                 for (String key : snoozedKeys) {
    345                     pw.print(INDENT);
    346                     pw.print(INDENT);
    347                     pw.print(INDENT);
    348                     pw.println(key);
    349                 }
    350             }
    351         }
    352     }
    353 
    354     protected void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
    355 
    356     }
    357 
    358     public void readXml(XmlPullParser parser, boolean forRestore)
    359             throws XmlPullParserException, IOException {
    360 
    361     }
    362 
    363     @VisibleForTesting
    364     void setAlarmManager(AlarmManager am) {
    365         mAm = am;
    366     }
    367 
    368     protected interface Callback {
    369         void repost(int userId, NotificationRecord r);
    370     }
    371 
    372     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
    373         @Override
    374         public void onReceive(Context context, Intent intent) {
    375             if (DEBUG) {
    376                 Slog.d(TAG, "Reposting notification");
    377             }
    378             if (REPOST_ACTION.equals(intent.getAction())) {
    379                 repost(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_USER_ID,
    380                         UserHandle.USER_SYSTEM));
    381             }
    382         }
    383     };
    384 }
    385