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