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.SecureSettingsObserver.newNotificationSettingsObserver; 20 21 import android.annotation.TargetApi; 22 import android.app.Notification; 23 import android.app.NotificationChannel; 24 import android.os.Build; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.os.Message; 28 import android.service.notification.NotificationListenerService; 29 import android.service.notification.StatusBarNotification; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.util.Pair; 33 34 import com.android.launcher3.LauncherModel; 35 import com.android.launcher3.util.IntSet; 36 import com.android.launcher3.util.PackageUserKey; 37 import com.android.launcher3.util.SecureSettingsObserver; 38 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Collections; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 46 import androidx.annotation.Nullable; 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(LauncherModel.getWorkerLooper(), 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