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