• 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.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 
NotificationListener()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
onCreate()150     public void onCreate() {
151         super.onCreate();
152         sIsCreated = true;
153     }
154 
155     @Override
onDestroy()156     public void onDestroy() {
157         super.onDestroy();
158         sIsCreated = false;
159     }
160 
getInstanceIfConnected()161     public static @Nullable NotificationListener getInstanceIfConnected() {
162         return sIsConnected ? sNotificationListenerInstance : null;
163     }
164 
setNotificationsChangedListener(NotificationsChangedListener listener)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 
setStatusBarNotificationsChangedListener(StatusBarNotificationsChangedListener listener)179     public static void setStatusBarNotificationsChangedListener
180             (StatusBarNotificationsChangedListener listener) {
181         sStatusBarNotificationsChangedListener = listener;
182     }
183 
removeNotificationsChangedListener()184     public static void removeNotificationsChangedListener() {
185         sNotificationsChangedListener = null;
186     }
187 
removeStatusBarNotificationsChangedListener()188     public static void removeStatusBarNotificationsChangedListener() {
189         sStatusBarNotificationsChangedListener = null;
190     }
191 
192     @Override
onListenerConnected()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 
onNotificationFullRefresh()210     private void onNotificationFullRefresh() {
211         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget();
212     }
213 
214     @Override
onListenerDisconnected()215     public void onListenerDisconnected() {
216         super.onListenerDisconnected();
217         sIsConnected = false;
218         mNotificationBadgingObserver.unregister();
219     }
220 
221     @Override
onNotificationPosted(final StatusBarNotification sbn)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 
NotificationPostedMsg(StatusBarNotification sbn)243         NotificationPostedMsg(StatusBarNotification sbn) {
244             packageUserKey = PackageUserKey.fromNotification(sbn);
245             notificationKey = NotificationKeyData.fromNotification(sbn);
246             shouldBeFilteredOut = shouldBeFilteredOut(sbn);
247         }
248     }
249 
250     @Override
onNotificationRemoved(final StatusBarNotification sbn)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 
cancelNotificationFromLauncher(String key)283     public void cancelNotificationFromLauncher(String key) {
284         mLastKeyDismissedByLauncher = key;
285         cancelNotification(key);
286     }
287 
288     @Override
onNotificationRankingUpdate(RankingMap rankingMap)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 
updateGroupKeyIfNecessary(StatusBarNotification sbn)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. */
getNotificationsForKeys(List<NotificationKeyData> keys)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      */
filterNotifications( StatusBarNotification[] notifications)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 
shouldBeFilteredOut(StatusBarNotification sbn)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 {
onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey, boolean shouldBeFilteredOut)388         void onNotificationPosted(PackageUserKey postedPackageUserKey,
389                 NotificationKeyData notificationKey, boolean shouldBeFilteredOut);
onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey)390         void onNotificationRemoved(PackageUserKey removedPackageUserKey,
391                 NotificationKeyData notificationKey);
onNotificationFullRefresh(List<StatusBarNotification> activeNotifications)392         void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
393     }
394 
395     public interface StatusBarNotificationsChangedListener {
onNotificationPosted(StatusBarNotification sbn)396         void onNotificationPosted(StatusBarNotification sbn);
onNotificationRemoved(StatusBarNotification sbn)397         void onNotificationRemoved(StatusBarNotification sbn);
398     }
399 }
400