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