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