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