1 /* 2 * Copyright (C) 2019 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 package com.android.car.notification; 17 18 import android.app.NotificationManager; 19 import android.os.Build; 20 import android.service.notification.NotificationListenerService; 21 import android.util.Log; 22 23 import androidx.annotation.VisibleForTesting; 24 25 import com.android.car.assist.client.CarAssistUtils; 26 27 import java.util.ArrayList; 28 import java.util.Collections; 29 import java.util.HashMap; 30 import java.util.HashSet; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.Set; 34 import java.util.stream.Collectors; 35 36 /** 37 * Keeps track of the additional state of notifications. This class is not thread safe and should 38 * only be called from the main thread. 39 */ 40 public class NotificationDataManager { 41 /** 42 * Interface for listeners that want to register for receiving updates to the notification 43 * unseen count. 44 */ 45 public interface OnUnseenCountUpdateListener { 46 /** 47 * Called when unseen notification count is changed. 48 */ onUnseenCountUpdate()49 void onUnseenCountUpdate(); 50 } 51 52 private static final boolean DEBUG = Build.IS_DEBUGGABLE; 53 private static final String TAG = "NotificationDataManager"; 54 55 private static NotificationDataManager sInstance; 56 57 /** 58 * Map that contains the key of all message notifications, mapped to whether or not the key's 59 * notification should be muted. 60 * 61 * Muted notifications should show an "Unmute" button on their notification and should not 62 * trigger the HUN when new notifications arrive with the same key. Unmuted should show a "Mute" 63 * button on their notification and should trigger the HUN. Both should update the notification 64 * in the Notification Center. 65 */ 66 private final Map<String, Boolean> mMessageNotificationToMuteStateMap = new HashMap<>(); 67 68 /** 69 * Map that contains the key of all unseen notifications. 70 */ 71 private final Map<String, Boolean> mUnseenNotificationMap = new HashMap<>(); 72 73 /** 74 * List of notifications that are visible to the user. 75 */ 76 private final Set<AlertEntry> mVisibleNotifications = new HashSet<>(); 77 78 private OnUnseenCountUpdateListener mOnUnseenCountUpdateListener; 79 80 /** 81 * @return the {@link NotificationDataManager} singleton 82 */ getInstance()83 public static NotificationDataManager getInstance() { 84 if (sInstance == null) { 85 sInstance = new NotificationDataManager(); 86 } 87 return sInstance; 88 } 89 90 @VisibleForTesting refreshInstance()91 static void refreshInstance() { 92 sInstance = null; 93 } 94 NotificationDataManager()95 private NotificationDataManager() { 96 clearAll(); 97 } 98 99 /** 100 * Sets listener for unseen notification count change event. 101 * 102 * @param listener UnseenCountUpdateListener 103 */ setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener)104 public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) { 105 mOnUnseenCountUpdateListener = listener; 106 } 107 addNewMessageNotification(AlertEntry alertEntry)108 void addNewMessageNotification(AlertEntry alertEntry) { 109 if (CarAssistUtils.isCarCompatibleMessagingNotification( 110 alertEntry.getStatusBarNotification())) { 111 mMessageNotificationToMuteStateMap 112 .putIfAbsent(alertEntry.getKey(), /* muteState= */ false); 113 114 if (mUnseenNotificationMap.containsKey(alertEntry.getKey())) { 115 mUnseenNotificationMap.put(alertEntry.getKey(), true); 116 mVisibleNotifications.add(alertEntry); 117 118 if (mOnUnseenCountUpdateListener != null) { 119 if (DEBUG) { 120 Log.d(TAG, "Unseen notification map: " + mUnseenNotificationMap); 121 } 122 mOnUnseenCountUpdateListener.onUnseenCountUpdate(); 123 } 124 } 125 } 126 } 127 untrackUnseenNotification(AlertEntry alertEntry)128 void untrackUnseenNotification(AlertEntry alertEntry) { 129 if (mUnseenNotificationMap.containsKey(alertEntry.getKey())) { 130 mUnseenNotificationMap.remove(alertEntry.getKey()); 131 if (mOnUnseenCountUpdateListener != null) { 132 if (DEBUG) { 133 Log.d(TAG, "UnseenNotificationMap: " + mUnseenNotificationMap); 134 } 135 mOnUnseenCountUpdateListener.onUnseenCountUpdate(); 136 } 137 } 138 } 139 updateUnseenNotificationGroups(List<NotificationGroup> notificationGroups)140 void updateUnseenNotificationGroups(List<NotificationGroup> notificationGroups) { 141 List<AlertEntry> alertEntries = new ArrayList<>(); 142 143 notificationGroups.forEach(group -> { 144 if (group.getGroupSummaryNotification() != null) { 145 alertEntries.add(group.getGroupSummaryNotification()); 146 } 147 alertEntries.addAll(group.getChildNotifications()); 148 }); 149 150 updateUnseenAlertEntries(alertEntries); 151 } 152 updateUnseenAlertEntries(List<AlertEntry> alertEntries)153 void updateUnseenAlertEntries(List<AlertEntry> alertEntries) { 154 Set<String> currentNotificationKeys = new HashSet<>(); 155 156 Collections.addAll(currentNotificationKeys, 157 mUnseenNotificationMap.keySet().toArray(new String[0])); 158 159 for (AlertEntry alertEntry : alertEntries) { 160 // add new notifications 161 mUnseenNotificationMap.putIfAbsent(alertEntry.getKey(), true); 162 163 // sbn exists in both sets. 164 currentNotificationKeys.remove(alertEntry.getKey()); 165 } 166 167 // These keys were removed from notificationGroups. Remove from mUnseenNotificationMap. 168 for (String notificationKey : currentNotificationKeys) { 169 mUnseenNotificationMap.remove(notificationKey); 170 } 171 172 if (mOnUnseenCountUpdateListener != null) { 173 if (DEBUG) { 174 Log.d(TAG, "UnseenNotificationMap: " + mUnseenNotificationMap); 175 } 176 mOnUnseenCountUpdateListener.onUnseenCountUpdate(); 177 } 178 } 179 isNotificationSeen(AlertEntry alertEntry)180 boolean isNotificationSeen(AlertEntry alertEntry) { 181 return !mUnseenNotificationMap.getOrDefault(alertEntry.getKey(), false); 182 } 183 184 /** 185 * Returns the mute state of the notification, or false if notification does not have a mute 186 * state. Only message notifications can be muted. 187 **/ isMessageNotificationMuted(AlertEntry alertEntry)188 public boolean isMessageNotificationMuted(AlertEntry alertEntry) { 189 if (!mMessageNotificationToMuteStateMap.containsKey(alertEntry.getKey())) { 190 addNewMessageNotification(alertEntry); 191 } 192 return mMessageNotificationToMuteStateMap.getOrDefault(alertEntry.getKey(), false); 193 } 194 195 /** 196 * If {@param sbn} is a messaging notification, this function will toggle its mute state. This 197 * state determines whether or not a HUN will be shown on future updates to the notification. 198 * It also determines the title of the notification's "Mute" button. 199 **/ toggleMute(AlertEntry alertEntry)200 public void toggleMute(AlertEntry alertEntry) { 201 if (CarAssistUtils.isCarCompatibleMessagingNotification( 202 alertEntry.getStatusBarNotification())) { 203 String sbnKey = alertEntry.getKey(); 204 Boolean currentMute = mMessageNotificationToMuteStateMap.get(sbnKey); 205 if (currentMute != null) { 206 mMessageNotificationToMuteStateMap.put(sbnKey, !currentMute); 207 } else { 208 Log.e(TAG, "Msg notification was not initially added to the mute state map: " 209 + alertEntry.getKey()); 210 } 211 } 212 } 213 214 /** 215 * Clear unseen and mute notification state information. 216 */ clearAll()217 public void clearAll() { 218 mMessageNotificationToMuteStateMap.clear(); 219 mUnseenNotificationMap.clear(); 220 mVisibleNotifications.clear(); 221 222 if (mOnUnseenCountUpdateListener != null) { 223 if (DEBUG) { 224 Log.d(TAG, "Unseen notifications cleared"); 225 } 226 mOnUnseenCountUpdateListener.onUnseenCountUpdate(); 227 } 228 } 229 setNotificationsAsSeen(List<AlertEntry> alertEntries)230 void setNotificationsAsSeen(List<AlertEntry> alertEntries) { 231 mVisibleNotifications.clear(); 232 for (AlertEntry alertEntry : alertEntries) { 233 if (mUnseenNotificationMap.containsKey(alertEntry.getKey())) { 234 mUnseenNotificationMap.put(alertEntry.getKey(), false); 235 mVisibleNotifications.add(alertEntry); 236 } 237 } 238 if (mOnUnseenCountUpdateListener != null) { 239 if (DEBUG) { 240 Log.d(TAG, "Unseen notification map: " + mUnseenNotificationMap); 241 } 242 mOnUnseenCountUpdateListener.onUnseenCountUpdate(); 243 } 244 } 245 246 /** 247 * Returns unseen notification count for higher than low importance notifications. 248 */ getNonLowImportanceUnseenNotificationCount( NotificationListenerService.RankingMap rankingMap)249 public int getNonLowImportanceUnseenNotificationCount( 250 NotificationListenerService.RankingMap rankingMap) { 251 final int[] unseenCount = {0}; 252 mUnseenNotificationMap.forEach((key, val) -> { 253 if (val) { 254 NotificationListenerService.Ranking ranking = 255 new NotificationListenerService.Ranking(); 256 rankingMap.getRanking(key, ranking); 257 if (ranking.getImportance() > NotificationManager.IMPORTANCE_LOW) { 258 unseenCount[0]++; 259 } 260 } 261 }); 262 if (DEBUG) { 263 Log.d(TAG, "Unseen notification map: " + mUnseenNotificationMap); 264 } 265 return unseenCount[0]; 266 } 267 268 /** 269 * Returns a collection containing all notifications the user should be seeing right now. 270 */ getVisibleNotifications()271 public List<AlertEntry> getVisibleNotifications() { 272 return mVisibleNotifications.stream().collect(Collectors.toList()); 273 } 274 275 /** 276 * Returns seen notifications. 277 */ getSeenNotifications()278 public String[] getSeenNotifications() { 279 return mUnseenNotificationMap.entrySet() 280 .stream() 281 // Seen notifications have value set to false 282 .filter(map -> !map.getValue()) 283 .map(map -> map.getKey()) 284 .toArray(String[]::new); 285 } 286 } 287