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 android.app.Notification;
     20 import android.app.NotificationChannel;
     21 import android.os.Handler;
     22 import android.os.Looper;
     23 import android.os.Message;
     24 import android.service.notification.NotificationListenerService;
     25 import android.service.notification.StatusBarNotification;
     26 import android.support.annotation.Nullable;
     27 import android.support.v4.util.Pair;
     28 import android.text.TextUtils;
     29 
     30 import com.android.launcher3.LauncherModel;
     31 import com.android.launcher3.config.FeatureFlags;
     32 import com.android.launcher3.util.PackageUserKey;
     33 
     34 import java.util.ArrayList;
     35 import java.util.Arrays;
     36 import java.util.Collections;
     37 import java.util.HashSet;
     38 import java.util.List;
     39 import java.util.Set;
     40 
     41 /**
     42  * A {@link NotificationListenerService} that sends updates to its
     43  * {@link NotificationsChangedListener} when notifications are posted or canceled,
     44  * as well and when this service first connects. An instance of NotificationListener,
     45  * and its methods for getting notifications, can be obtained via {@link #getInstanceIfConnected()}.
     46  */
     47 public class NotificationListener extends NotificationListenerService {
     48 
     49     private static final int MSG_NOTIFICATION_POSTED = 1;
     50     private static final int MSG_NOTIFICATION_REMOVED = 2;
     51     private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
     52 
     53     private static NotificationListener sNotificationListenerInstance = null;
     54     private static NotificationsChangedListener sNotificationsChangedListener;
     55     private static boolean sIsConnected;
     56 
     57     private final Handler mWorkerHandler;
     58     private final Handler mUiHandler;
     59 
     60     private Ranking mTempRanking = new Ranking();
     61 
     62     private Handler.Callback mWorkerCallback = new Handler.Callback() {
     63         @Override
     64         public boolean handleMessage(Message message) {
     65             switch (message.what) {
     66                 case MSG_NOTIFICATION_POSTED:
     67                     mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
     68                     break;
     69                 case MSG_NOTIFICATION_REMOVED:
     70                     mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
     71                     break;
     72                 case MSG_NOTIFICATION_FULL_REFRESH:
     73                     final List<StatusBarNotification> activeNotifications = sIsConnected
     74                             ? filterNotifications(getActiveNotifications())
     75                             : new ArrayList<StatusBarNotification>();
     76                     mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
     77                     break;
     78             }
     79             return true;
     80         }
     81     };
     82 
     83     private Handler.Callback mUiCallback = new Handler.Callback() {
     84         @Override
     85         public boolean handleMessage(Message message) {
     86             switch (message.what) {
     87                 case MSG_NOTIFICATION_POSTED:
     88                     if (sNotificationsChangedListener != null) {
     89                         NotificationPostedMsg msg = (NotificationPostedMsg) message.obj;
     90                         sNotificationsChangedListener.onNotificationPosted(msg.packageUserKey,
     91                                 msg.notificationKey, msg.shouldBeFilteredOut);
     92                     }
     93                     break;
     94                 case MSG_NOTIFICATION_REMOVED:
     95                     if (sNotificationsChangedListener != null) {
     96                         Pair<PackageUserKey, NotificationKeyData> pair
     97                                 = (Pair<PackageUserKey, NotificationKeyData>) message.obj;
     98                         sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
     99                     }
    100                     break;
    101                 case MSG_NOTIFICATION_FULL_REFRESH:
    102                     if (sNotificationsChangedListener != null) {
    103                         sNotificationsChangedListener.onNotificationFullRefresh(
    104                                 (List<StatusBarNotification>) message.obj);
    105                     }
    106                     break;
    107             }
    108             return true;
    109         }
    110     };
    111 
    112     public NotificationListener() {
    113         super();
    114         mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback);
    115         mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
    116         sNotificationListenerInstance = this;
    117     }
    118 
    119     public static @Nullable NotificationListener getInstanceIfConnected() {
    120         return sIsConnected ? sNotificationListenerInstance : null;
    121     }
    122 
    123     public static void setNotificationsChangedListener(NotificationsChangedListener listener) {
    124         if (!FeatureFlags.BADGE_ICONS) {
    125             return;
    126         }
    127         sNotificationsChangedListener = listener;
    128 
    129         if (sNotificationListenerInstance != null) {
    130             sNotificationListenerInstance.onNotificationFullRefresh();
    131         }
    132     }
    133 
    134     public static void removeNotificationsChangedListener() {
    135         sNotificationsChangedListener = null;
    136     }
    137 
    138     @Override
    139     public void onListenerConnected() {
    140         super.onListenerConnected();
    141         sIsConnected = true;
    142         onNotificationFullRefresh();
    143     }
    144 
    145     private void onNotificationFullRefresh() {
    146         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget();
    147     }
    148 
    149     @Override
    150     public void onListenerDisconnected() {
    151         super.onListenerDisconnected();
    152         sIsConnected = false;
    153     }
    154 
    155     @Override
    156     public void onNotificationPosted(final StatusBarNotification sbn) {
    157         super.onNotificationPosted(sbn);
    158         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn))
    159                 .sendToTarget();
    160     }
    161 
    162     /**
    163      * An object containing data to send to MSG_NOTIFICATION_POSTED targets.
    164      */
    165     private class NotificationPostedMsg {
    166         PackageUserKey packageUserKey;
    167         NotificationKeyData notificationKey;
    168         boolean shouldBeFilteredOut;
    169 
    170         NotificationPostedMsg(StatusBarNotification sbn) {
    171             packageUserKey = PackageUserKey.fromNotification(sbn);
    172             notificationKey = NotificationKeyData.fromNotification(sbn);
    173             shouldBeFilteredOut = shouldBeFilteredOut(sbn);
    174         }
    175     }
    176 
    177     @Override
    178     public void onNotificationRemoved(final StatusBarNotification sbn) {
    179         super.onNotificationRemoved(sbn);
    180         Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey
    181                 = new Pair<>(PackageUserKey.fromNotification(sbn),
    182                         NotificationKeyData.fromNotification(sbn));
    183         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
    184                 .sendToTarget();
    185     }
    186 
    187     /** This makes a potentially expensive binder call and should be run on a background thread. */
    188     public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) {
    189         StatusBarNotification[] notifications = NotificationListener.this
    190                 .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys)
    191                         .toArray(new String[keys.size()]));
    192         return notifications == null ? Collections.EMPTY_LIST : Arrays.asList(notifications);
    193     }
    194 
    195     /**
    196      * Filter out notifications that don't have an intent
    197      * or are headers for grouped notifications.
    198      *
    199      * @see #shouldBeFilteredOut(StatusBarNotification)
    200      */
    201     private List<StatusBarNotification> filterNotifications(
    202             StatusBarNotification[] notifications) {
    203         if (notifications == null) return null;
    204         Set<Integer> removedNotifications = new HashSet<>();
    205         for (int i = 0; i < notifications.length; i++) {
    206             if (shouldBeFilteredOut(notifications[i])) {
    207                 removedNotifications.add(i);
    208             }
    209         }
    210         List<StatusBarNotification> filteredNotifications = new ArrayList<>(
    211                 notifications.length - removedNotifications.size());
    212         for (int i = 0; i < notifications.length; i++) {
    213             if (!removedNotifications.contains(i)) {
    214                 filteredNotifications.add(notifications[i]);
    215             }
    216         }
    217         return filteredNotifications;
    218     }
    219 
    220     private boolean shouldBeFilteredOut(StatusBarNotification sbn) {
    221         getCurrentRanking().getRanking(sbn.getKey(), mTempRanking);
    222         if (!mTempRanking.canShowBadge()) {
    223             return true;
    224         }
    225         Notification notification = sbn.getNotification();
    226         if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
    227             // Special filtering for the default, legacy "Miscellaneous" channel.
    228             if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
    229                 return true;
    230             }
    231         }
    232         boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
    233         CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
    234         CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
    235         boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text);
    236         return (isGroupHeader || missingTitleAndText);
    237     }
    238 
    239     public interface NotificationsChangedListener {
    240         void onNotificationPosted(PackageUserKey postedPackageUserKey,
    241                 NotificationKeyData notificationKey, boolean shouldBeFilteredOut);
    242         void onNotificationRemoved(PackageUserKey removedPackageUserKey,
    243                 NotificationKeyData notificationKey);
    244         void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
    245     }
    246 }
    247