Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2017 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.launcher3.notification;
     18 
     19 import static com.android.launcher3.SettingsActivity.NOTIFICATION_BADGING;
     20 
     21 import android.annotation.TargetApi;
     22 import android.app.Notification;
     23 import android.app.NotificationChannel;
     24 import android.os.Build;
     25 import android.os.Handler;
     26 import android.os.Looper;
     27 import android.os.Message;
     28 import android.service.notification.NotificationListenerService;
     29 import android.service.notification.StatusBarNotification;
     30 import android.support.annotation.Nullable;
     31 import android.text.TextUtils;
     32 import android.util.ArraySet;
     33 import android.util.Log;
     34 import android.util.Pair;
     35 
     36 import com.android.launcher3.LauncherModel;
     37 import com.android.launcher3.util.PackageUserKey;
     38 import com.android.launcher3.util.SettingsObserver;
     39 
     40 import java.util.ArrayList;
     41 import java.util.Arrays;
     42 import java.util.Collections;
     43 import java.util.HashMap;
     44 import java.util.List;
     45 import java.util.Map;
     46 import java.util.Set;
     47 
     48 /**
     49  * A {@link NotificationListenerService} that sends updates to its
     50  * {@link NotificationsChangedListener} when notifications are posted or canceled,
     51  * as well and when this service first connects. An instance of NotificationListener,
     52  * and its methods for getting notifications, can be obtained via {@link #getInstanceIfConnected()}.
     53  */
     54 @TargetApi(Build.VERSION_CODES.O)
     55 public class NotificationListener extends NotificationListenerService {
     56 
     57     public static final String TAG = "NotificationListener";
     58 
     59     private static final int MSG_NOTIFICATION_POSTED = 1;
     60     private static final int MSG_NOTIFICATION_REMOVED = 2;
     61     private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
     62 
     63     private static NotificationListener sNotificationListenerInstance = null;
     64     private static NotificationsChangedListener sNotificationsChangedListener;
     65     private static StatusBarNotificationsChangedListener sStatusBarNotificationsChangedListener;
     66     private static boolean sIsConnected;
     67     private static boolean sIsCreated;
     68 
     69     private final Handler mWorkerHandler;
     70     private final Handler mUiHandler;
     71     private final Ranking mTempRanking = new Ranking();
     72     /** Maps groupKey's to the corresponding group of notifications. */
     73     private final Map<String, NotificationGroup> mNotificationGroupMap = new HashMap<>();
     74     /** Maps keys to their corresponding current group key */
     75     private final Map<String, String> mNotificationGroupKeyMap = new HashMap<>();
     76 
     77     /** The last notification key that was dismissed from launcher UI */
     78     private String mLastKeyDismissedByLauncher;
     79 
     80     private SettingsObserver mNotificationBadgingObserver;
     81 
     82     private final Handler.Callback mWorkerCallback = new Handler.Callback() {
     83         @Override
     84         public boolean handleMessage(Message message) {
     85             switch (message.what) {
     86                 case MSG_NOTIFICATION_POSTED:
     87                     mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
     88                     break;
     89                 case MSG_NOTIFICATION_REMOVED:
     90                     mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
     91                     break;
     92                 case MSG_NOTIFICATION_FULL_REFRESH:
     93                     List<StatusBarNotification> activeNotifications;
     94                     if (sIsConnected) {
     95                         try {
     96                             activeNotifications = filterNotifications(getActiveNotifications());
     97                         } catch (SecurityException ex) {
     98                             Log.e(TAG, "SecurityException: failed to fetch notifications");
     99                             activeNotifications = new ArrayList<StatusBarNotification>();
    100 
    101                         }
    102                     } else {
    103                         activeNotifications = new ArrayList<StatusBarNotification>();
    104                     }
    105 
    106                     mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
    107                     break;
    108             }
    109             return true;
    110         }
    111     };
    112 
    113     private final Handler.Callback mUiCallback = new Handler.Callback() {
    114         @Override
    115         public boolean handleMessage(Message message) {
    116             switch (message.what) {
    117                 case MSG_NOTIFICATION_POSTED:
    118                     if (sNotificationsChangedListener != null) {
    119                         NotificationPostedMsg msg = (NotificationPostedMsg) message.obj;
    120                         sNotificationsChangedListener.onNotificationPosted(msg.packageUserKey,
    121                                 msg.notificationKey, msg.shouldBeFilteredOut);
    122                     }
    123                     break;
    124                 case MSG_NOTIFICATION_REMOVED:
    125                     if (sNotificationsChangedListener != null) {
    126                         Pair<PackageUserKey, NotificationKeyData> pair
    127                                 = (Pair<PackageUserKey, NotificationKeyData>) message.obj;
    128                         sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
    129                     }
    130                     break;
    131                 case MSG_NOTIFICATION_FULL_REFRESH:
    132                     if (sNotificationsChangedListener != null) {
    133                         sNotificationsChangedListener.onNotificationFullRefresh(
    134                                 (List<StatusBarNotification>) message.obj);
    135                     }
    136                     break;
    137             }
    138             return true;
    139         }
    140     };
    141 
    142     public NotificationListener() {
    143         super();
    144         mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback);
    145         mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
    146         sNotificationListenerInstance = this;
    147     }
    148 
    149     @Override
    150     public void onCreate() {
    151         super.onCreate();
    152         sIsCreated = true;
    153     }
    154 
    155     @Override
    156     public void onDestroy() {
    157         super.onDestroy();
    158         sIsCreated = false;
    159     }
    160 
    161     public static @Nullable NotificationListener getInstanceIfConnected() {
    162         return sIsConnected ? sNotificationListenerInstance : null;
    163     }
    164 
    165     public static void setNotificationsChangedListener(NotificationsChangedListener listener) {
    166         sNotificationsChangedListener = listener;
    167 
    168         NotificationListener notificationListener = getInstanceIfConnected();
    169         if (notificationListener != null) {
    170             notificationListener.onNotificationFullRefresh();
    171         } else if (!sIsCreated && sNotificationsChangedListener != null) {
    172             // User turned off badging globally, so we unbound this service;
    173             // tell the listener that there are no notifications to remove dots.
    174             sNotificationsChangedListener.onNotificationFullRefresh(
    175                     Collections.<StatusBarNotification>emptyList());
    176         }
    177     }
    178 
    179     public static void setStatusBarNotificationsChangedListener
    180             (StatusBarNotificationsChangedListener listener) {
    181         sStatusBarNotificationsChangedListener = listener;
    182     }
    183 
    184     public static void removeNotificationsChangedListener() {
    185         sNotificationsChangedListener = null;
    186     }
    187 
    188     public static void removeStatusBarNotificationsChangedListener() {
    189         sStatusBarNotificationsChangedListener = null;
    190     }
    191 
    192     @Override
    193     public void onListenerConnected() {
    194         super.onListenerConnected();
    195         sIsConnected = true;
    196 
    197         mNotificationBadgingObserver = new SettingsObserver.Secure(getContentResolver()) {
    198             @Override
    199             public void onSettingChanged(boolean isNotificationBadgingEnabled) {
    200                 if (!isNotificationBadgingEnabled) {
    201                     requestUnbind();
    202                 }
    203             }
    204         };
    205         mNotificationBadgingObserver.register(NOTIFICATION_BADGING);
    206 
    207         onNotificationFullRefresh();
    208     }
    209 
    210     private void onNotificationFullRefresh() {
    211         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget();
    212     }
    213 
    214     @Override
    215     public void onListenerDisconnected() {
    216         super.onListenerDisconnected();
    217         sIsConnected = false;
    218         mNotificationBadgingObserver.unregister();
    219     }
    220 
    221     @Override
    222     public void onNotificationPosted(final StatusBarNotification sbn) {
    223         super.onNotificationPosted(sbn);
    224         if (sbn == null) {
    225             // There is a bug in platform where we can get a null notification; just ignore it.
    226             return;
    227         }
    228         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn))
    229             .sendToTarget();
    230         if (sStatusBarNotificationsChangedListener != null) {
    231             sStatusBarNotificationsChangedListener.onNotificationPosted(sbn);
    232         }
    233     }
    234 
    235     /**
    236      * An object containing data to send to MSG_NOTIFICATION_POSTED targets.
    237      */
    238     private class NotificationPostedMsg {
    239         final PackageUserKey packageUserKey;
    240         final NotificationKeyData notificationKey;
    241         final boolean shouldBeFilteredOut;
    242 
    243         NotificationPostedMsg(StatusBarNotification sbn) {
    244             packageUserKey = PackageUserKey.fromNotification(sbn);
    245             notificationKey = NotificationKeyData.fromNotification(sbn);
    246             shouldBeFilteredOut = shouldBeFilteredOut(sbn);
    247         }
    248     }
    249 
    250     @Override
    251     public void onNotificationRemoved(final StatusBarNotification sbn) {
    252         super.onNotificationRemoved(sbn);
    253         if (sbn == null) {
    254             // There is a bug in platform where we can get a null notification; just ignore it.
    255             return;
    256         }
    257         Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey
    258             = new Pair<>(PackageUserKey.fromNotification(sbn),
    259             NotificationKeyData.fromNotification(sbn));
    260         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
    261             .sendToTarget();
    262         if (sStatusBarNotificationsChangedListener != null) {
    263             sStatusBarNotificationsChangedListener.onNotificationRemoved(sbn);
    264         }
    265 
    266         NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey());
    267         String key = sbn.getKey();
    268         if (notificationGroup != null) {
    269             notificationGroup.removeChildKey(key);
    270             if (notificationGroup.isEmpty()) {
    271                 if (key.equals(mLastKeyDismissedByLauncher)) {
    272                     // Only cancel the group notification if launcher dismissed the last child.
    273                     cancelNotification(notificationGroup.getGroupSummaryKey());
    274                 }
    275                 mNotificationGroupMap.remove(sbn.getGroupKey());
    276             }
    277         }
    278         if (key.equals(mLastKeyDismissedByLauncher)) {
    279             mLastKeyDismissedByLauncher = null;
    280         }
    281     }
    282 
    283     public void cancelNotificationFromLauncher(String key) {
    284         mLastKeyDismissedByLauncher = key;
    285         cancelNotification(key);
    286     }
    287 
    288     @Override
    289     public void onNotificationRankingUpdate(RankingMap rankingMap) {
    290         super.onNotificationRankingUpdate(rankingMap);
    291         String[] keys = rankingMap.getOrderedKeys();
    292         for (StatusBarNotification sbn : getActiveNotifications(keys)) {
    293             updateGroupKeyIfNecessary(sbn);
    294         }
    295     }
    296 
    297     private void updateGroupKeyIfNecessary(StatusBarNotification sbn) {
    298         String childKey = sbn.getKey();
    299         String oldGroupKey = mNotificationGroupKeyMap.get(childKey);
    300         String newGroupKey = sbn.getGroupKey();
    301         if (oldGroupKey == null || !oldGroupKey.equals(newGroupKey)) {
    302             // The group key has changed.
    303             mNotificationGroupKeyMap.put(childKey, newGroupKey);
    304             if (oldGroupKey != null && mNotificationGroupMap.containsKey(oldGroupKey)) {
    305                 // Remove the child key from the old group.
    306                 NotificationGroup oldGroup = mNotificationGroupMap.get(oldGroupKey);
    307                 oldGroup.removeChildKey(childKey);
    308                 if (oldGroup.isEmpty()) {
    309                     mNotificationGroupMap.remove(oldGroupKey);
    310                 }
    311             }
    312         }
    313         if (sbn.isGroup() && newGroupKey != null) {
    314             // Maintain group info so we can cancel the summary when the last child is canceled.
    315             NotificationGroup notificationGroup = mNotificationGroupMap.get(newGroupKey);
    316             if (notificationGroup == null) {
    317                 notificationGroup = new NotificationGroup();
    318                 mNotificationGroupMap.put(newGroupKey, notificationGroup);
    319             }
    320             boolean isGroupSummary = (sbn.getNotification().flags
    321                     & Notification.FLAG_GROUP_SUMMARY) != 0;
    322             if (isGroupSummary) {
    323                 notificationGroup.setGroupSummaryKey(childKey);
    324             } else {
    325                 notificationGroup.addChildKey(childKey);
    326             }
    327         }
    328     }
    329 
    330     /** This makes a potentially expensive binder call and should be run on a background thread. */
    331     public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) {
    332         StatusBarNotification[] notifications = NotificationListener.this
    333                 .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys)
    334                         .toArray(new String[keys.size()]));
    335         return notifications == null
    336                 ? Collections.<StatusBarNotification>emptyList() : Arrays.asList(notifications);
    337     }
    338 
    339     /**
    340      * Filter out notifications that don't have an intent
    341      * or are headers for grouped notifications.
    342      *
    343      * @see #shouldBeFilteredOut(StatusBarNotification)
    344      */
    345     private List<StatusBarNotification> filterNotifications(
    346             StatusBarNotification[] notifications) {
    347         if (notifications == null) return null;
    348         Set<Integer> removedNotifications = new ArraySet<>();
    349         for (int i = 0; i < notifications.length; i++) {
    350             if (shouldBeFilteredOut(notifications[i])) {
    351                 removedNotifications.add(i);
    352             }
    353         }
    354         List<StatusBarNotification> filteredNotifications = new ArrayList<>(
    355                 notifications.length - removedNotifications.size());
    356         for (int i = 0; i < notifications.length; i++) {
    357             if (!removedNotifications.contains(i)) {
    358                 filteredNotifications.add(notifications[i]);
    359             }
    360         }
    361         return filteredNotifications;
    362     }
    363 
    364     private boolean shouldBeFilteredOut(StatusBarNotification sbn) {
    365         Notification notification = sbn.getNotification();
    366 
    367         updateGroupKeyIfNecessary(sbn);
    368 
    369         getCurrentRanking().getRanking(sbn.getKey(), mTempRanking);
    370         if (!mTempRanking.canShowBadge()) {
    371             return true;
    372         }
    373         if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
    374             // Special filtering for the default, legacy "Miscellaneous" channel.
    375             if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
    376                 return true;
    377             }
    378         }
    379 
    380         CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
    381         CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
    382         boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text);
    383         boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
    384         return (isGroupHeader || missingTitleAndText);
    385     }
    386 
    387     public interface NotificationsChangedListener {
    388         void onNotificationPosted(PackageUserKey postedPackageUserKey,
    389                 NotificationKeyData notificationKey, boolean shouldBeFilteredOut);
    390         void onNotificationRemoved(PackageUserKey removedPackageUserKey,
    391                 NotificationKeyData notificationKey);
    392         void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
    393     }
    394 
    395     public interface StatusBarNotificationsChangedListener {
    396         void onNotificationPosted(StatusBarNotification sbn);
    397         void onNotificationRemoved(StatusBarNotification sbn);
    398     }
    399 }
    400