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.MODEL_EXECUTOR;
20 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
21 
22 import android.annotation.TargetApi;
23 import android.app.Notification;
24 import android.app.NotificationChannel;
25 import android.os.Build;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.os.Message;
29 import android.service.notification.NotificationListenerService;
30 import android.service.notification.StatusBarNotification;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.util.Pair;
34 
35 import androidx.annotation.Nullable;
36 
37 import com.android.launcher3.util.IntSet;
38 import com.android.launcher3.util.PackageUserKey;
39 import com.android.launcher3.util.SecureSettingsObserver;
40 
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.Collections;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
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 SecureSettingsObserver mNotificationDotsObserver;
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(MODEL_EXECUTOR.getLooper(), 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 dots 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         mNotificationDotsObserver =
198                 newNotificationSettingsObserver(this, this::onNotificationSettingsChanged);
199         mNotificationDotsObserver.register();
200         mNotificationDotsObserver.dispatchOnChange();
201 
202         onNotificationFullRefresh();
203     }
204 
onNotificationSettingsChanged(boolean areNotificationDotsEnabled)205     private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
206         if (!areNotificationDotsEnabled && sIsConnected) {
207             requestUnbind();
208         }
209     }
210 
onNotificationFullRefresh()211     private void onNotificationFullRefresh() {
212         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget();
213     }
214 
215     @Override
onListenerDisconnected()216     public void onListenerDisconnected() {
217         super.onListenerDisconnected();
218         sIsConnected = false;
219         mNotificationDotsObserver.unregister();
220     }
221 
222     @Override
onNotificationPosted(final StatusBarNotification sbn)223     public void onNotificationPosted(final StatusBarNotification sbn) {
224         super.onNotificationPosted(sbn);
225         if (sbn == null) {
226             // There is a bug in platform where we can get a null notification; just ignore it.
227             return;
228         }
229         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn))
230             .sendToTarget();
231         if (sStatusBarNotificationsChangedListener != null) {
232             sStatusBarNotificationsChangedListener.onNotificationPosted(sbn);
233         }
234     }
235 
236     /**
237      * An object containing data to send to MSG_NOTIFICATION_POSTED targets.
238      */
239     private class NotificationPostedMsg {
240         final PackageUserKey packageUserKey;
241         final NotificationKeyData notificationKey;
242         final boolean shouldBeFilteredOut;
243 
NotificationPostedMsg(StatusBarNotification sbn)244         NotificationPostedMsg(StatusBarNotification sbn) {
245             packageUserKey = PackageUserKey.fromNotification(sbn);
246             notificationKey = NotificationKeyData.fromNotification(sbn);
247             shouldBeFilteredOut = shouldBeFilteredOut(sbn);
248         }
249     }
250 
251     @Override
onNotificationRemoved(final StatusBarNotification sbn)252     public void onNotificationRemoved(final StatusBarNotification sbn) {
253         super.onNotificationRemoved(sbn);
254         if (sbn == null) {
255             // There is a bug in platform where we can get a null notification; just ignore it.
256             return;
257         }
258         Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey
259             = new Pair<>(PackageUserKey.fromNotification(sbn),
260             NotificationKeyData.fromNotification(sbn));
261         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
262             .sendToTarget();
263         if (sStatusBarNotificationsChangedListener != null) {
264             sStatusBarNotificationsChangedListener.onNotificationRemoved(sbn);
265         }
266 
267         NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey());
268         String key = sbn.getKey();
269         if (notificationGroup != null) {
270             notificationGroup.removeChildKey(key);
271             if (notificationGroup.isEmpty()) {
272                 if (key.equals(mLastKeyDismissedByLauncher)) {
273                     // Only cancel the group notification if launcher dismissed the last child.
274                     cancelNotification(notificationGroup.getGroupSummaryKey());
275                 }
276                 mNotificationGroupMap.remove(sbn.getGroupKey());
277             }
278         }
279         if (key.equals(mLastKeyDismissedByLauncher)) {
280             mLastKeyDismissedByLauncher = null;
281         }
282     }
283 
cancelNotificationFromLauncher(String key)284     public void cancelNotificationFromLauncher(String key) {
285         mLastKeyDismissedByLauncher = key;
286         cancelNotification(key);
287     }
288 
289     @Override
onNotificationRankingUpdate(RankingMap rankingMap)290     public void onNotificationRankingUpdate(RankingMap rankingMap) {
291         super.onNotificationRankingUpdate(rankingMap);
292         String[] keys = rankingMap.getOrderedKeys();
293         for (StatusBarNotification sbn : getActiveNotifications(keys)) {
294             updateGroupKeyIfNecessary(sbn);
295         }
296     }
297 
updateGroupKeyIfNecessary(StatusBarNotification sbn)298     private void updateGroupKeyIfNecessary(StatusBarNotification sbn) {
299         String childKey = sbn.getKey();
300         String oldGroupKey = mNotificationGroupKeyMap.get(childKey);
301         String newGroupKey = sbn.getGroupKey();
302         if (oldGroupKey == null || !oldGroupKey.equals(newGroupKey)) {
303             // The group key has changed.
304             mNotificationGroupKeyMap.put(childKey, newGroupKey);
305             if (oldGroupKey != null && mNotificationGroupMap.containsKey(oldGroupKey)) {
306                 // Remove the child key from the old group.
307                 NotificationGroup oldGroup = mNotificationGroupMap.get(oldGroupKey);
308                 oldGroup.removeChildKey(childKey);
309                 if (oldGroup.isEmpty()) {
310                     mNotificationGroupMap.remove(oldGroupKey);
311                 }
312             }
313         }
314         if (sbn.isGroup() && newGroupKey != null) {
315             // Maintain group info so we can cancel the summary when the last child is canceled.
316             NotificationGroup notificationGroup = mNotificationGroupMap.get(newGroupKey);
317             if (notificationGroup == null) {
318                 notificationGroup = new NotificationGroup();
319                 mNotificationGroupMap.put(newGroupKey, notificationGroup);
320             }
321             boolean isGroupSummary = (sbn.getNotification().flags
322                     & Notification.FLAG_GROUP_SUMMARY) != 0;
323             if (isGroupSummary) {
324                 notificationGroup.setGroupSummaryKey(childKey);
325             } else {
326                 notificationGroup.addChildKey(childKey);
327             }
328         }
329     }
330 
331     /** This makes a potentially expensive binder call and should be run on a background thread. */
getNotificationsForKeys(List<NotificationKeyData> keys)332     public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) {
333         StatusBarNotification[] notifications = NotificationListener.this
334                 .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys)
335                         .toArray(new String[keys.size()]));
336         return notifications == null
337                 ? Collections.<StatusBarNotification>emptyList() : Arrays.asList(notifications);
338     }
339 
340     /**
341      * Filter out notifications that don't have an intent
342      * or are headers for grouped notifications.
343      *
344      * @see #shouldBeFilteredOut(StatusBarNotification)
345      */
filterNotifications( StatusBarNotification[] notifications)346     private List<StatusBarNotification> filterNotifications(
347             StatusBarNotification[] notifications) {
348         if (notifications == null) return null;
349         IntSet removedNotifications = new IntSet();
350         for (int i = 0; i < notifications.length; i++) {
351             if (shouldBeFilteredOut(notifications[i])) {
352                 removedNotifications.add(i);
353             }
354         }
355         List<StatusBarNotification> filteredNotifications = new ArrayList<>(
356                 notifications.length - removedNotifications.size());
357         for (int i = 0; i < notifications.length; i++) {
358             if (!removedNotifications.contains(i)) {
359                 filteredNotifications.add(notifications[i]);
360             }
361         }
362         return filteredNotifications;
363     }
364 
shouldBeFilteredOut(StatusBarNotification sbn)365     private boolean shouldBeFilteredOut(StatusBarNotification sbn) {
366         Notification notification = sbn.getNotification();
367 
368         updateGroupKeyIfNecessary(sbn);
369 
370         getCurrentRanking().getRanking(sbn.getKey(), mTempRanking);
371         if (!mTempRanking.canShowBadge()) {
372             return true;
373         }
374         if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
375             // Special filtering for the default, legacy "Miscellaneous" channel.
376             if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
377                 return true;
378             }
379         }
380 
381         CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
382         CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
383         boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text);
384         boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
385         return (isGroupHeader || missingTitleAndText);
386     }
387 
388     public interface NotificationsChangedListener {
onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey, boolean shouldBeFilteredOut)389         void onNotificationPosted(PackageUserKey postedPackageUserKey,
390                 NotificationKeyData notificationKey, boolean shouldBeFilteredOut);
onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey)391         void onNotificationRemoved(PackageUserKey removedPackageUserKey,
392                 NotificationKeyData notificationKey);
onNotificationFullRefresh(List<StatusBarNotification> activeNotifications)393         void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
394     }
395 
396     public interface StatusBarNotificationsChangedListener {
onNotificationPosted(StatusBarNotification sbn)397         void onNotificationPosted(StatusBarNotification sbn);
onNotificationRemoved(StatusBarNotification sbn)398         void onNotificationRemoved(StatusBarNotification sbn);
399     }
400 }
401