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