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