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