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