• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                 notifyUnseenCountUpdateListeners();
119             }
120         }
121     }
122 
untrackUnseenNotification(AlertEntry alertEntry)123     void untrackUnseenNotification(AlertEntry alertEntry) {
124         if (mUnseenNotificationMap.containsKey(alertEntry.getKey())) {
125             mUnseenNotificationMap.remove(alertEntry.getKey());
126             notifyUnseenCountUpdateListeners();
127         }
128     }
129 
updateUnseenNotificationGroups(List<NotificationGroup> notificationGroups)130     void updateUnseenNotificationGroups(List<NotificationGroup> notificationGroups) {
131         List<AlertEntry> alertEntries = new ArrayList<>();
132 
133         notificationGroups.forEach(group -> {
134             if (group.getGroupSummaryNotification() != null) {
135                 alertEntries.add(group.getGroupSummaryNotification());
136             }
137             alertEntries.addAll(group.getChildNotifications());
138         });
139 
140         updateUnseenAlertEntries(alertEntries);
141     }
142 
updateUnseenAlertEntries(List<AlertEntry> alertEntries)143     void updateUnseenAlertEntries(List<AlertEntry> alertEntries) {
144         Set<String> currentNotificationKeys = new HashSet<>();
145 
146         Collections.addAll(currentNotificationKeys,
147                 mUnseenNotificationMap.keySet().toArray(new String[0]));
148 
149         for (AlertEntry alertEntry : alertEntries) {
150             // add new notifications
151             mUnseenNotificationMap.putIfAbsent(alertEntry.getKey(), true);
152 
153             // sbn exists in both sets.
154             currentNotificationKeys.remove(alertEntry.getKey());
155         }
156 
157         // These keys were removed from notificationGroups. Remove from mUnseenNotificationMap.
158         for (String notificationKey : currentNotificationKeys) {
159             mUnseenNotificationMap.remove(notificationKey);
160         }
161 
162         notifyUnseenCountUpdateListeners();
163     }
164 
isNotificationSeen(AlertEntry alertEntry)165     boolean isNotificationSeen(AlertEntry alertEntry) {
166         return !mUnseenNotificationMap.getOrDefault(alertEntry.getKey(), false);
167     }
168 
169     /**
170      * Returns the mute state of the notification, or false if notification does not have a mute
171      * state. Only message notifications can be muted.
172      **/
isMessageNotificationMuted(AlertEntry alertEntry)173     public boolean isMessageNotificationMuted(AlertEntry alertEntry) {
174         if (!mMessageNotificationToMuteStateMap.containsKey(alertEntry.getKey())) {
175             addNewMessageNotification(alertEntry);
176         }
177         return mMessageNotificationToMuteStateMap.getOrDefault(alertEntry.getKey(), false);
178     }
179 
180     /**
181      * If {@param sbn} is a messaging notification, this function will toggle its mute state. This
182      * state determines whether or not a HUN will be shown on future updates to the notification.
183      * It also determines the title of the notification's "Mute" button.
184      **/
toggleMute(AlertEntry alertEntry)185     public void toggleMute(AlertEntry alertEntry) {
186         if (CarAssistUtils.isCarCompatibleMessagingNotification(
187                 alertEntry.getStatusBarNotification())) {
188             String sbnKey = alertEntry.getKey();
189             Boolean currentMute = mMessageNotificationToMuteStateMap.get(sbnKey);
190             if (currentMute != null) {
191                 mMessageNotificationToMuteStateMap.put(sbnKey, !currentMute);
192             } else {
193                 Log.e(TAG, "Msg notification was not initially added to the mute state map: "
194                         + alertEntry.getKey());
195             }
196         }
197     }
198 
199     /**
200      * Clear unseen and mute notification state information.
201      */
clearAll()202     public void clearAll() {
203         mMessageNotificationToMuteStateMap.clear();
204         mUnseenNotificationMap.clear();
205         mVisibleNotifications.clear();
206 
207         notifyUnseenCountUpdateListeners();
208     }
209 
210     /**
211      * Uses the {@code alertEntries} to reset the visible notifications and marks them as seen.
212      *
213      * @param alertEntries List of {@link AlertEntry} that are currently visible to be marked seen.
214      */
setVisibleNotificationsAsSeen(List<AlertEntry> alertEntries)215     void setVisibleNotificationsAsSeen(List<AlertEntry> alertEntries) {
216         mVisibleNotifications.clear();
217         for (AlertEntry alertEntry : alertEntries) {
218             if (mUnseenNotificationMap.containsKey(alertEntry.getKey())) {
219                 mUnseenNotificationMap.put(alertEntry.getKey(), false);
220                 mVisibleNotifications.add(alertEntry);
221             }
222         }
223         notifyUnseenCountUpdateListeners();
224     }
225 
226     /**
227      * @param alertEntry {@link AlertEntry} to be marked seen and notify listeners.
228      */
setNotificationAsSeen(AlertEntry alertEntry)229     void setNotificationAsSeen(AlertEntry alertEntry) {
230         mUnseenNotificationMap.put(alertEntry.getKey(), false);
231         notifyUnseenCountUpdateListeners();
232     }
233 
234     /**
235      * Returns unseen notification count for higher than low importance notifications.
236      */
getNonLowImportanceUnseenNotificationCount( NotificationListenerService.RankingMap rankingMap)237     public int getNonLowImportanceUnseenNotificationCount(
238             NotificationListenerService.RankingMap rankingMap) {
239         final int[] unseenCount = {0};
240         mUnseenNotificationMap.forEach((key, val) -> {
241             if (val) {
242                 NotificationListenerService.Ranking ranking =
243                         new NotificationListenerService.Ranking();
244                 rankingMap.getRanking(key, ranking);
245                 if (ranking.getImportance() > NotificationManager.IMPORTANCE_LOW) {
246                     unseenCount[0]++;
247                 }
248             }
249         });
250         if (DEBUG) {
251             Log.d(TAG, "Unseen notification map: " + mUnseenNotificationMap);
252         }
253         return unseenCount[0];
254     }
255 
256     /**
257      * Returns a collection containing all notifications the user should be seeing right now.
258      */
getVisibleNotifications()259     public List<AlertEntry> getVisibleNotifications() {
260         return mVisibleNotifications.stream().collect(Collectors.toList());
261     }
262 
263     /**
264      * Returns seen notifications.
265      */
getSeenNotifications()266     public String[] getSeenNotifications() {
267         return mUnseenNotificationMap.entrySet()
268                 .stream()
269                 // Seen notifications have value set to false
270                 .filter(map -> !map.getValue())
271                 .map(map -> map.getKey())
272                 .toArray(String[]::new);
273     }
274 
notifyUnseenCountUpdateListeners()275     private void notifyUnseenCountUpdateListeners() {
276         if (mOnUnseenCountUpdateListener == null) {
277             return;
278         }
279         if (DEBUG) {
280             Log.d(TAG, "Unseen notifications cleared");
281         }
282         mOnUnseenCountUpdateListener.onUnseenCountUpdate();
283     }
284 }
285