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