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.SettingsActivity.NOTIFICATION_BADGING; 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.support.annotation.Nullable; 31 import android.text.TextUtils; 32 import android.util.ArraySet; 33 import android.util.Log; 34 import android.util.Pair; 35 36 import com.android.launcher3.LauncherModel; 37 import com.android.launcher3.util.PackageUserKey; 38 import com.android.launcher3.util.SettingsObserver; 39 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.Collections; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Set; 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 SettingsObserver mNotificationBadgingObserver; 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 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 150 public void onCreate() { 151 super.onCreate(); 152 sIsCreated = true; 153 } 154 155 @Override 156 public void onDestroy() { 157 super.onDestroy(); 158 sIsCreated = false; 159 } 160 161 public static @Nullable NotificationListener getInstanceIfConnected() { 162 return sIsConnected ? sNotificationListenerInstance : null; 163 } 164 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 badging 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 179 public static void setStatusBarNotificationsChangedListener 180 (StatusBarNotificationsChangedListener listener) { 181 sStatusBarNotificationsChangedListener = listener; 182 } 183 184 public static void removeNotificationsChangedListener() { 185 sNotificationsChangedListener = null; 186 } 187 188 public static void removeStatusBarNotificationsChangedListener() { 189 sStatusBarNotificationsChangedListener = null; 190 } 191 192 @Override 193 public void onListenerConnected() { 194 super.onListenerConnected(); 195 sIsConnected = true; 196 197 mNotificationBadgingObserver = new SettingsObserver.Secure(getContentResolver()) { 198 @Override 199 public void onSettingChanged(boolean isNotificationBadgingEnabled) { 200 if (!isNotificationBadgingEnabled) { 201 requestUnbind(); 202 } 203 } 204 }; 205 mNotificationBadgingObserver.register(NOTIFICATION_BADGING); 206 207 onNotificationFullRefresh(); 208 } 209 210 private void onNotificationFullRefresh() { 211 mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget(); 212 } 213 214 @Override 215 public void onListenerDisconnected() { 216 super.onListenerDisconnected(); 217 sIsConnected = false; 218 mNotificationBadgingObserver.unregister(); 219 } 220 221 @Override 222 public void onNotificationPosted(final StatusBarNotification sbn) { 223 super.onNotificationPosted(sbn); 224 if (sbn == null) { 225 // There is a bug in platform where we can get a null notification; just ignore it. 226 return; 227 } 228 mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn)) 229 .sendToTarget(); 230 if (sStatusBarNotificationsChangedListener != null) { 231 sStatusBarNotificationsChangedListener.onNotificationPosted(sbn); 232 } 233 } 234 235 /** 236 * An object containing data to send to MSG_NOTIFICATION_POSTED targets. 237 */ 238 private class NotificationPostedMsg { 239 final PackageUserKey packageUserKey; 240 final NotificationKeyData notificationKey; 241 final boolean shouldBeFilteredOut; 242 243 NotificationPostedMsg(StatusBarNotification sbn) { 244 packageUserKey = PackageUserKey.fromNotification(sbn); 245 notificationKey = NotificationKeyData.fromNotification(sbn); 246 shouldBeFilteredOut = shouldBeFilteredOut(sbn); 247 } 248 } 249 250 @Override 251 public void onNotificationRemoved(final StatusBarNotification sbn) { 252 super.onNotificationRemoved(sbn); 253 if (sbn == null) { 254 // There is a bug in platform where we can get a null notification; just ignore it. 255 return; 256 } 257 Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey 258 = new Pair<>(PackageUserKey.fromNotification(sbn), 259 NotificationKeyData.fromNotification(sbn)); 260 mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey) 261 .sendToTarget(); 262 if (sStatusBarNotificationsChangedListener != null) { 263 sStatusBarNotificationsChangedListener.onNotificationRemoved(sbn); 264 } 265 266 NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey()); 267 String key = sbn.getKey(); 268 if (notificationGroup != null) { 269 notificationGroup.removeChildKey(key); 270 if (notificationGroup.isEmpty()) { 271 if (key.equals(mLastKeyDismissedByLauncher)) { 272 // Only cancel the group notification if launcher dismissed the last child. 273 cancelNotification(notificationGroup.getGroupSummaryKey()); 274 } 275 mNotificationGroupMap.remove(sbn.getGroupKey()); 276 } 277 } 278 if (key.equals(mLastKeyDismissedByLauncher)) { 279 mLastKeyDismissedByLauncher = null; 280 } 281 } 282 283 public void cancelNotificationFromLauncher(String key) { 284 mLastKeyDismissedByLauncher = key; 285 cancelNotification(key); 286 } 287 288 @Override 289 public void onNotificationRankingUpdate(RankingMap rankingMap) { 290 super.onNotificationRankingUpdate(rankingMap); 291 String[] keys = rankingMap.getOrderedKeys(); 292 for (StatusBarNotification sbn : getActiveNotifications(keys)) { 293 updateGroupKeyIfNecessary(sbn); 294 } 295 } 296 297 private void updateGroupKeyIfNecessary(StatusBarNotification sbn) { 298 String childKey = sbn.getKey(); 299 String oldGroupKey = mNotificationGroupKeyMap.get(childKey); 300 String newGroupKey = sbn.getGroupKey(); 301 if (oldGroupKey == null || !oldGroupKey.equals(newGroupKey)) { 302 // The group key has changed. 303 mNotificationGroupKeyMap.put(childKey, newGroupKey); 304 if (oldGroupKey != null && mNotificationGroupMap.containsKey(oldGroupKey)) { 305 // Remove the child key from the old group. 306 NotificationGroup oldGroup = mNotificationGroupMap.get(oldGroupKey); 307 oldGroup.removeChildKey(childKey); 308 if (oldGroup.isEmpty()) { 309 mNotificationGroupMap.remove(oldGroupKey); 310 } 311 } 312 } 313 if (sbn.isGroup() && newGroupKey != null) { 314 // Maintain group info so we can cancel the summary when the last child is canceled. 315 NotificationGroup notificationGroup = mNotificationGroupMap.get(newGroupKey); 316 if (notificationGroup == null) { 317 notificationGroup = new NotificationGroup(); 318 mNotificationGroupMap.put(newGroupKey, notificationGroup); 319 } 320 boolean isGroupSummary = (sbn.getNotification().flags 321 & Notification.FLAG_GROUP_SUMMARY) != 0; 322 if (isGroupSummary) { 323 notificationGroup.setGroupSummaryKey(childKey); 324 } else { 325 notificationGroup.addChildKey(childKey); 326 } 327 } 328 } 329 330 /** This makes a potentially expensive binder call and should be run on a background thread. */ 331 public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) { 332 StatusBarNotification[] notifications = NotificationListener.this 333 .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys) 334 .toArray(new String[keys.size()])); 335 return notifications == null 336 ? Collections.<StatusBarNotification>emptyList() : Arrays.asList(notifications); 337 } 338 339 /** 340 * Filter out notifications that don't have an intent 341 * or are headers for grouped notifications. 342 * 343 * @see #shouldBeFilteredOut(StatusBarNotification) 344 */ 345 private List<StatusBarNotification> filterNotifications( 346 StatusBarNotification[] notifications) { 347 if (notifications == null) return null; 348 Set<Integer> removedNotifications = new ArraySet<>(); 349 for (int i = 0; i < notifications.length; i++) { 350 if (shouldBeFilteredOut(notifications[i])) { 351 removedNotifications.add(i); 352 } 353 } 354 List<StatusBarNotification> filteredNotifications = new ArrayList<>( 355 notifications.length - removedNotifications.size()); 356 for (int i = 0; i < notifications.length; i++) { 357 if (!removedNotifications.contains(i)) { 358 filteredNotifications.add(notifications[i]); 359 } 360 } 361 return filteredNotifications; 362 } 363 364 private boolean shouldBeFilteredOut(StatusBarNotification sbn) { 365 Notification notification = sbn.getNotification(); 366 367 updateGroupKeyIfNecessary(sbn); 368 369 getCurrentRanking().getRanking(sbn.getKey(), mTempRanking); 370 if (!mTempRanking.canShowBadge()) { 371 return true; 372 } 373 if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) { 374 // Special filtering for the default, legacy "Miscellaneous" channel. 375 if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) { 376 return true; 377 } 378 } 379 380 CharSequence title = notification.extras.getCharSequence(Notification.EXTRA_TITLE); 381 CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT); 382 boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text); 383 boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0; 384 return (isGroupHeader || missingTitleAndText); 385 } 386 387 public interface NotificationsChangedListener { 388 void onNotificationPosted(PackageUserKey postedPackageUserKey, 389 NotificationKeyData notificationKey, boolean shouldBeFilteredOut); 390 void onNotificationRemoved(PackageUserKey removedPackageUserKey, 391 NotificationKeyData notificationKey); 392 void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications); 393 } 394 395 public interface StatusBarNotificationsChangedListener { 396 void onNotificationPosted(StatusBarNotification sbn); 397 void onNotificationRemoved(StatusBarNotification sbn); 398 } 399 } 400