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