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.AnyThread; 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 import androidx.annotation.WorkerThread; 41 42 import com.android.launcher3.util.PackageUserKey; 43 import com.android.launcher3.util.SettingsCache; 44 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.Collections; 48 import java.util.HashMap; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.stream.Collectors; 52 53 /** 54 * A {@link NotificationListenerService} that sends updates to its 55 * {@link NotificationsChangedListener} when notifications are posted or canceled, 56 * as well and when this service first connects. An instance of NotificationListener, 57 * and its methods for getting notifications, can be obtained via {@link #getInstanceIfConnected()}. 58 */ 59 @TargetApi(Build.VERSION_CODES.O) 60 public class NotificationListener extends NotificationListenerService { 61 62 public static final String TAG = "NotificationListener"; 63 64 private static final int MSG_NOTIFICATION_POSTED = 1; 65 private static final int MSG_NOTIFICATION_REMOVED = 2; 66 private static final int MSG_NOTIFICATION_FULL_REFRESH = 3; 67 private static final int MSG_CANCEL_NOTIFICATION = 4; 68 private static final int MSG_RANKING_UPDATE = 5; 69 70 private static NotificationListener sNotificationListenerInstance = null; 71 private static final ArraySet<NotificationsChangedListener> sNotificationsChangedListeners = 72 new ArraySet<>(); 73 private static boolean sIsConnected; 74 75 private final Handler mWorkerHandler; 76 private final Handler mUiHandler; 77 private final Ranking mTempRanking = new Ranking(); 78 79 /** Maps groupKey's to the corresponding group of notifications. */ 80 private final Map<String, NotificationGroup> mNotificationGroupMap = new HashMap<>(); 81 /** Maps keys to their corresponding current group key */ 82 private final Map<String, String> mNotificationGroupKeyMap = new HashMap<>(); 83 84 /** The last notification key that was dismissed from launcher UI */ 85 private String mLastKeyDismissedByLauncher; 86 87 private SettingsCache mSettingsCache; 88 private SettingsCache.OnChangeListener mNotificationSettingsChangedListener; 89 NotificationListener()90 public NotificationListener() { 91 mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage); 92 mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage); 93 sNotificationListenerInstance = this; 94 } 95 getInstanceIfConnected()96 public static @Nullable NotificationListener getInstanceIfConnected() { 97 return sIsConnected ? sNotificationListenerInstance : null; 98 } 99 addNotificationsChangedListener(NotificationsChangedListener listener)100 public static void addNotificationsChangedListener(NotificationsChangedListener listener) { 101 if (listener == null) { 102 return; 103 } 104 sNotificationsChangedListeners.add(listener); 105 106 NotificationListener notificationListener = getInstanceIfConnected(); 107 if (notificationListener != null) { 108 notificationListener.onNotificationFullRefresh(); 109 } else { 110 // User turned off dots globally, so we unbound this service; 111 // tell the listener that there are no notifications to remove dots. 112 MODEL_EXECUTOR.submit(() -> MAIN_EXECUTOR.submit(() -> 113 listener.onNotificationFullRefresh(Collections.emptyList()))); 114 } 115 } 116 removeNotificationsChangedListener(NotificationsChangedListener listener)117 public static void removeNotificationsChangedListener(NotificationsChangedListener listener) { 118 if (listener != null) { 119 sNotificationsChangedListeners.remove(listener); 120 } 121 } 122 handleWorkerMessage(Message message)123 private boolean handleWorkerMessage(Message message) { 124 switch (message.what) { 125 case MSG_NOTIFICATION_POSTED: { 126 StatusBarNotification sbn = (StatusBarNotification) message.obj; 127 mUiHandler.obtainMessage(notificationIsValidForUI(sbn) 128 ? MSG_NOTIFICATION_POSTED : MSG_NOTIFICATION_REMOVED, 129 toKeyPair(sbn)).sendToTarget(); 130 return true; 131 } 132 case MSG_NOTIFICATION_REMOVED: { 133 StatusBarNotification sbn = (StatusBarNotification) message.obj; 134 mUiHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, 135 toKeyPair(sbn)).sendToTarget(); 136 137 NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey()); 138 String key = sbn.getKey(); 139 if (notificationGroup != null) { 140 notificationGroup.removeChildKey(key); 141 if (notificationGroup.isEmpty()) { 142 if (key.equals(mLastKeyDismissedByLauncher)) { 143 // Only cancel the group notification if launcher dismissed the 144 // last child. 145 cancelNotification(notificationGroup.getGroupSummaryKey()); 146 } 147 mNotificationGroupMap.remove(sbn.getGroupKey()); 148 } 149 } 150 if (key.equals(mLastKeyDismissedByLauncher)) { 151 mLastKeyDismissedByLauncher = null; 152 } 153 return true; 154 } 155 case MSG_NOTIFICATION_FULL_REFRESH: 156 List<StatusBarNotification> activeNotifications = null; 157 if (sIsConnected) { 158 activeNotifications = Arrays.stream(getActiveNotificationsSafely(null)) 159 .filter(this::notificationIsValidForUI) 160 .collect(Collectors.toList()); 161 } else { 162 activeNotifications = new ArrayList<>(); 163 } 164 165 mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget(); 166 return true; 167 case MSG_CANCEL_NOTIFICATION: { 168 mLastKeyDismissedByLauncher = (String) message.obj; 169 cancelNotification(mLastKeyDismissedByLauncher); 170 return true; 171 } 172 case MSG_RANKING_UPDATE: { 173 String[] keys = ((RankingMap) message.obj).getOrderedKeys(); 174 for (StatusBarNotification sbn : getActiveNotificationsSafely(keys)) { 175 updateGroupKeyIfNecessary(sbn); 176 } 177 return true; 178 } 179 } 180 return false; 181 } 182 handleUiMessage(Message message)183 private boolean handleUiMessage(Message message) { 184 switch (message.what) { 185 case MSG_NOTIFICATION_POSTED: 186 if (sNotificationsChangedListeners.size() > 0) { 187 Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj; 188 for (NotificationsChangedListener listener : sNotificationsChangedListeners) { 189 listener.onNotificationPosted(msg.first, msg.second); 190 } 191 } 192 break; 193 case MSG_NOTIFICATION_REMOVED: 194 if (sNotificationsChangedListeners.size() > 0) { 195 Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj; 196 for (NotificationsChangedListener listener : sNotificationsChangedListeners) { 197 listener.onNotificationRemoved(msg.first, msg.second); 198 } 199 } 200 break; 201 case MSG_NOTIFICATION_FULL_REFRESH: 202 if (sNotificationsChangedListeners.size() > 0) { 203 for (NotificationsChangedListener listener : sNotificationsChangedListeners) { 204 listener.onNotificationFullRefresh( 205 (List<StatusBarNotification>) message.obj); 206 } 207 } 208 break; 209 } 210 return true; 211 } 212 getActiveNotificationsSafely(@ullable String[] keys)213 private @NonNull StatusBarNotification[] getActiveNotificationsSafely(@Nullable String[] keys) { 214 StatusBarNotification[] result = null; 215 try { 216 result = getActiveNotifications(keys); 217 } catch (SecurityException e) { 218 Log.e(TAG, "SecurityException: failed to fetch notifications"); 219 } 220 return result == null ? new StatusBarNotification[0] : result; 221 } 222 223 @Override onListenerConnected()224 public void onListenerConnected() { 225 super.onListenerConnected(); 226 sIsConnected = true; 227 228 // Register an observer to rebind the notification listener when dots are re-enabled. 229 mSettingsCache = SettingsCache.INSTANCE.get(this); 230 mNotificationSettingsChangedListener = this::onNotificationSettingsChanged; 231 mSettingsCache.register(NOTIFICATION_BADGING_URI, 232 mNotificationSettingsChangedListener); 233 onNotificationSettingsChanged(mSettingsCache.getValue(NOTIFICATION_BADGING_URI)); 234 235 onNotificationFullRefresh(); 236 } 237 onNotificationSettingsChanged(boolean areNotificationDotsEnabled)238 private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) { 239 if (!areNotificationDotsEnabled && sIsConnected) { 240 requestUnbind(); 241 } 242 } 243 onNotificationFullRefresh()244 private void onNotificationFullRefresh() { 245 mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget(); 246 } 247 248 @Override onListenerDisconnected()249 public void onListenerDisconnected() { 250 super.onListenerDisconnected(); 251 sIsConnected = false; 252 mSettingsCache.unregister(NOTIFICATION_BADGING_URI, mNotificationSettingsChangedListener); 253 onNotificationFullRefresh(); 254 } 255 256 @Override onNotificationPosted(final StatusBarNotification sbn)257 public void onNotificationPosted(final StatusBarNotification sbn) { 258 if (sbn != null) { 259 mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, sbn).sendToTarget(); 260 } 261 } 262 263 @Override onNotificationRemoved(final StatusBarNotification sbn)264 public void onNotificationRemoved(final StatusBarNotification sbn) { 265 if (sbn != null) { 266 mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, sbn).sendToTarget(); 267 } 268 } 269 270 @Override onNotificationRankingUpdate(RankingMap rankingMap)271 public void onNotificationRankingUpdate(RankingMap rankingMap) { 272 mWorkerHandler.obtainMessage(MSG_RANKING_UPDATE, rankingMap).sendToTarget(); 273 } 274 275 /** 276 * Cancels a notification 277 */ 278 @AnyThread cancelNotificationFromLauncher(String key)279 public void cancelNotificationFromLauncher(String key) { 280 mWorkerHandler.obtainMessage(MSG_CANCEL_NOTIFICATION, key).sendToTarget(); 281 } 282 283 @WorkerThread updateGroupKeyIfNecessary(StatusBarNotification sbn)284 private void updateGroupKeyIfNecessary(StatusBarNotification sbn) { 285 String childKey = sbn.getKey(); 286 String oldGroupKey = mNotificationGroupKeyMap.get(childKey); 287 String newGroupKey = sbn.getGroupKey(); 288 if (oldGroupKey == null || !oldGroupKey.equals(newGroupKey)) { 289 // The group key has changed. 290 mNotificationGroupKeyMap.put(childKey, newGroupKey); 291 if (oldGroupKey != null && mNotificationGroupMap.containsKey(oldGroupKey)) { 292 // Remove the child key from the old group. 293 NotificationGroup oldGroup = mNotificationGroupMap.get(oldGroupKey); 294 oldGroup.removeChildKey(childKey); 295 if (oldGroup.isEmpty()) { 296 mNotificationGroupMap.remove(oldGroupKey); 297 } 298 } 299 } 300 if (sbn.isGroup() && newGroupKey != null) { 301 // Maintain group info so we can cancel the summary when the last child is canceled. 302 NotificationGroup notificationGroup = mNotificationGroupMap.get(newGroupKey); 303 if (notificationGroup == null) { 304 notificationGroup = new NotificationGroup(); 305 mNotificationGroupMap.put(newGroupKey, notificationGroup); 306 } 307 boolean isGroupSummary = (sbn.getNotification().flags 308 & Notification.FLAG_GROUP_SUMMARY) != 0; 309 if (isGroupSummary) { 310 notificationGroup.setGroupSummaryKey(childKey); 311 } else { 312 notificationGroup.addChildKey(childKey); 313 } 314 } 315 } 316 317 /** 318 * This makes a potentially expensive binder call and should be run on a background thread. 319 */ 320 @WorkerThread getNotificationsForKeys(List<NotificationKeyData> keys)321 public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) { 322 return Arrays.asList(getActiveNotificationsSafely( 323 keys.stream().map(n -> n.notificationKey).toArray(String[]::new))); 324 } 325 326 /** 327 * Returns true for notifications that have an intent and are not headers for grouped 328 * notifications and should be shown in the notification popup. 329 */ 330 @WorkerThread notificationIsValidForUI(StatusBarNotification sbn)331 private boolean notificationIsValidForUI(StatusBarNotification sbn) { 332 Notification notification = sbn.getNotification(); 333 updateGroupKeyIfNecessary(sbn); 334 335 getCurrentRanking().getRanking(sbn.getKey(), mTempRanking); 336 if (!mTempRanking.canShowBadge()) { 337 return false; 338 } 339 if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) { 340 // Special filtering for the default, legacy "Miscellaneous" channel. 341 if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) { 342 return false; 343 } 344 } 345 346 CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE); 347 CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT); 348 boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text); 349 boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0; 350 return !isGroupHeader && !missingTitleAndText; 351 } 352 toKeyPair(StatusBarNotification sbn)353 private static Pair<PackageUserKey, NotificationKeyData> toKeyPair(StatusBarNotification sbn) { 354 return Pair.create(PackageUserKey.fromNotification(sbn), 355 NotificationKeyData.fromNotification(sbn)); 356 } 357 358 public interface NotificationsChangedListener { onNotificationPosted(PackageUserKey postedPackageUserKey, NotificationKeyData notificationKey)359 void onNotificationPosted(PackageUserKey postedPackageUserKey, 360 NotificationKeyData notificationKey); onNotificationRemoved(PackageUserKey removedPackageUserKey, NotificationKeyData notificationKey)361 void onNotificationRemoved(PackageUserKey removedPackageUserKey, 362 NotificationKeyData notificationKey); onNotificationFullRefresh(List<StatusBarNotification> activeNotifications)363 void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications); 364 } 365 } 366