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 android.app.Notification;
20 import android.app.NotificationChannel;
21 import android.os.Handler;
22 import android.os.Looper;
23 import android.os.Message;
24 import android.service.notification.NotificationListenerService;
25 import android.service.notification.StatusBarNotification;
26 import android.support.annotation.Nullable;
27 import android.support.v4.util.Pair;
28 import android.text.TextUtils;
29 
30 import com.android.launcher3.LauncherModel;
31 import com.android.launcher3.config.FeatureFlags;
32 import com.android.launcher3.util.PackageUserKey;
33 
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collections;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Set;
40 
41 /**
42  * A {@link NotificationListenerService} that sends updates to its
43  * {@link NotificationsChangedListener} when notifications are posted or canceled,
44  * as well and when this service first connects. An instance of NotificationListener,
45  * and its methods for getting notifications, can be obtained via {@link #getInstanceIfConnected()}.
46  */
47 public class NotificationListener extends NotificationListenerService {
48 
49     private static final int MSG_NOTIFICATION_POSTED = 1;
50     private static final int MSG_NOTIFICATION_REMOVED = 2;
51     private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
52 
53     private static NotificationListener sNotificationListenerInstance = null;
54     private static NotificationsChangedListener sNotificationsChangedListener;
55     private static boolean sIsConnected;
56 
57     private final Handler mWorkerHandler;
58     private final Handler mUiHandler;
59 
60     private Ranking mTempRanking = new Ranking();
61 
62     private Handler.Callback mWorkerCallback = new Handler.Callback() {
63         @Override
64         public boolean handleMessage(Message message) {
65             switch (message.what) {
66                 case MSG_NOTIFICATION_POSTED:
67                     mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
68                     break;
69                 case MSG_NOTIFICATION_REMOVED:
70                     mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
71                     break;
72                 case MSG_NOTIFICATION_FULL_REFRESH:
73                     final List<StatusBarNotification> activeNotifications = sIsConnected
74                             ? filterNotifications(getActiveNotifications())
75                             : new ArrayList<StatusBarNotification>();
76                     mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
77                     break;
78             }
79             return true;
80         }
81     };
82 
83     private Handler.Callback mUiCallback = new Handler.Callback() {
84         @Override
85         public boolean handleMessage(Message message) {
86             switch (message.what) {
87                 case MSG_NOTIFICATION_POSTED:
88                     if (sNotificationsChangedListener != null) {
89                         NotificationPostedMsg msg = (NotificationPostedMsg) message.obj;
90                         sNotificationsChangedListener.onNotificationPosted(msg.packageUserKey,
91                                 msg.notificationKey, msg.shouldBeFilteredOut);
92                     }
93                     break;
94                 case MSG_NOTIFICATION_REMOVED:
95                     if (sNotificationsChangedListener != null) {
96                         Pair<PackageUserKey, NotificationKeyData> pair
97                                 = (Pair<PackageUserKey, NotificationKeyData>) message.obj;
98                         sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
99                     }
100                     break;
101                 case MSG_NOTIFICATION_FULL_REFRESH:
102                     if (sNotificationsChangedListener != null) {
103                         sNotificationsChangedListener.onNotificationFullRefresh(
104                                 (List<StatusBarNotification>) message.obj);
105                     }
106                     break;
107             }
108             return true;
109         }
110     };
111 
NotificationListener()112     public NotificationListener() {
113         super();
114         mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback);
115         mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
116         sNotificationListenerInstance = this;
117     }
118 
getInstanceIfConnected()119     public static @Nullable NotificationListener getInstanceIfConnected() {
120         return sIsConnected ? sNotificationListenerInstance : null;
121     }
122 
setNotificationsChangedListener(NotificationsChangedListener listener)123     public static void setNotificationsChangedListener(NotificationsChangedListener listener) {
124         if (!FeatureFlags.BADGE_ICONS) {
125             return;
126         }
127         sNotificationsChangedListener = listener;
128 
129         if (sNotificationListenerInstance != null) {
130             sNotificationListenerInstance.onNotificationFullRefresh();
131         }
132     }
133 
removeNotificationsChangedListener()134     public static void removeNotificationsChangedListener() {
135         sNotificationsChangedListener = null;
136     }
137 
138     @Override
onListenerConnected()139     public void onListenerConnected() {
140         super.onListenerConnected();
141         sIsConnected = true;
142         onNotificationFullRefresh();
143     }
144 
onNotificationFullRefresh()145     private void onNotificationFullRefresh() {
146         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget();
147     }
148 
149     @Override
onListenerDisconnected()150     public void onListenerDisconnected() {
151         super.onListenerDisconnected();
152         sIsConnected = false;
153     }
154 
155     @Override
onNotificationPosted(final StatusBarNotification sbn)156     public void onNotificationPosted(final StatusBarNotification sbn) {
157         super.onNotificationPosted(sbn);
158         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn))
159                 .sendToTarget();
160     }
161 
162     /**
163      * An object containing data to send to MSG_NOTIFICATION_POSTED targets.
164      */
165     private class NotificationPostedMsg {
166         PackageUserKey packageUserKey;
167         NotificationKeyData notificationKey;
168         boolean shouldBeFilteredOut;
169 
NotificationPostedMsg(StatusBarNotification sbn)170         NotificationPostedMsg(StatusBarNotification sbn) {
171             packageUserKey = PackageUserKey.fromNotification(sbn);
172             notificationKey = NotificationKeyData.fromNotification(sbn);
173             shouldBeFilteredOut = shouldBeFilteredOut(sbn);
174         }
175     }
176 
177     @Override
onNotificationRemoved(final StatusBarNotification sbn)178     public void onNotificationRemoved(final StatusBarNotification sbn) {
179         super.onNotificationRemoved(sbn);
180         Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey
181                 = new Pair<>(PackageUserKey.fromNotification(sbn),
182                         NotificationKeyData.fromNotification(sbn));
183         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
184                 .sendToTarget();
185     }
186 
187     /** This makes a potentially expensive binder call and should be run on a background thread. */
getNotificationsForKeys(List<NotificationKeyData> keys)188     public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) {
189         StatusBarNotification[] notifications = NotificationListener.this
190                 .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys)
191                         .toArray(new String[keys.size()]));
192         return notifications == null ? Collections.EMPTY_LIST : Arrays.asList(notifications);
193     }
194 
195     /**
196      * Filter out notifications that don't have an intent
197      * or are headers for grouped notifications.
198      *
199      * @see #shouldBeFilteredOut(StatusBarNotification)
200      */
filterNotifications( StatusBarNotification[] notifications)201     private List<StatusBarNotification> filterNotifications(
202             StatusBarNotification[] notifications) {
203         if (notifications == null) return null;
204         Set<Integer> removedNotifications = new HashSet<>();
205         for (int i = 0; i < notifications.length; i++) {
206             if (shouldBeFilteredOut(notifications[i])) {
207                 removedNotifications.add(i);
208             }
209         }
210         List<StatusBarNotification> filteredNotifications = new ArrayList<>(
211                 notifications.length - removedNotifications.size());
212         for (int i = 0; i < notifications.length; i++) {
213             if (!removedNotifications.contains(i)) {
214                 filteredNotifications.add(notifications[i]);
215             }
216         }
217         return filteredNotifications;
218     }
219 
shouldBeFilteredOut(StatusBarNotification sbn)220     private boolean shouldBeFilteredOut(StatusBarNotification sbn) {
221         getCurrentRanking().getRanking(sbn.getKey(), mTempRanking);
222         if (!mTempRanking.canShowBadge()) {
223             return true;
224         }
225         Notification notification = sbn.getNotification();
226         if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
227             // Special filtering for the default, legacy "Miscellaneous" channel.
228             if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
229                 return true;
230             }
231         }
232         boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
233         CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
234         CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
235         boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text);
236         return (isGroupHeader || missingTitleAndText);
237     }
238 
239     public interface NotificationsChangedListener {
onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey, boolean shouldBeFilteredOut)240         void onNotificationPosted(PackageUserKey postedPackageUserKey,
241                 NotificationKeyData notificationKey, boolean shouldBeFilteredOut);
onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey)242         void onNotificationRemoved(PackageUserKey removedPackageUserKey,
243                 NotificationKeyData notificationKey);
onNotificationFullRefresh(List<StatusBarNotification> activeNotifications)244         void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
245     }
246 }
247