1 /*
2  * Copyright (C) 2018 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.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.os.Binder;
23 import android.os.Build;
24 import android.os.Handler;
25 import android.os.IBinder;
26 import android.os.Message;
27 import android.os.RemoteException;
28 import android.os.UserHandle;
29 import android.service.notification.NotificationListenerService;
30 import android.service.notification.StatusBarNotification;
31 import android.util.Log;
32 
33 import androidx.annotation.Nullable;
34 import androidx.annotation.VisibleForTesting;
35 
36 import com.android.car.notification.headsup.CarHeadsUpNotificationAppContainer;
37 
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.concurrent.ConcurrentHashMap;
41 import java.util.concurrent.ConcurrentMap;
42 import java.util.stream.Collectors;
43 import java.util.stream.Stream;
44 
45 /**
46  * NotificationListenerService that fetches all notifications from system.
47  */
48 public class CarNotificationListener extends NotificationListenerService implements
49         CarHeadsUpNotificationManager.OnHeadsUpNotificationStateChange {
50     private static final String TAG = "CarNotificationListener";
51     private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
52     static final String ACTION_LOCAL_BINDING = "local_binding";
53     static final int NOTIFY_NOTIFICATION_POSTED = 1;
54     static final int NOTIFY_NOTIFICATION_REMOVED = 2;
55     static final int NOTIFY_RANKING_UPDATED = 3;
56     /** Temporary {@link Ranking} object that serves as a reused value holder */
57     final private Ranking mTemporaryRanking = new Ranking();
58 
59     private Handler mHandler;
60     private RankingMap mRankingMap;
61     private CarHeadsUpNotificationManager mHeadsUpManager;
62     private NotificationDataManager mNotificationDataManager;
63     private boolean mIsNotificationPanelVisible;
64     private boolean mIsListenerConnected;
65 
66     /**
67      * Map that contains all the active notifications that are not currently HUN. These
68      * notifications may or may not be visible to the user if they get filtered out. The only time
69      * these will be removed from the map is when the {@llink NotificationListenerService} calls the
70      * onNotificationRemoved method. New notifications will be added to this map if the notification
71      * is posted as a non-HUN or when a HUN's state is changed to non-HUN.
72      */
73     private ConcurrentMap<String, AlertEntry> mActiveNotifications = new ConcurrentHashMap<>();
74 
75     /**
76      * Call this if to register this service as a system service and connect to HUN. This is useful
77      * if the notification service is being used as a lib instead of a standalone app. The
78      * standalone app version has a manifest entry that will have the same effect.
79      *
80      * @param context Context required for registering the service.
81      * @param carUxRestrictionManagerWrapper will have the heads up manager registered with it.
82      * @param carHeadsUpNotificationManager HUN controller.
83      */
registerAsSystemService(Context context, CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper, CarHeadsUpNotificationManager carHeadsUpNotificationManager)84     public void registerAsSystemService(Context context,
85             CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper,
86             CarHeadsUpNotificationManager carHeadsUpNotificationManager) {
87         try {
88             mNotificationDataManager = NotificationDataManager.getInstance();
89             registerAsSystemService(context,
90                     new ComponentName(context.getPackageName(), getClass().getCanonicalName()),
91                     NotificationUtils.getCurrentUser(context));
92             mHeadsUpManager = carHeadsUpNotificationManager;
93             mHeadsUpManager.registerHeadsUpNotificationStateChangeListener(this);
94             carUxRestrictionManagerWrapper.setCarHeadsUpNotificationManager(
95                     carHeadsUpNotificationManager);
96         } catch (RemoteException e) {
97             Log.e(TAG, "Unable to register notification listener", e);
98         }
99     }
100 
101     @VisibleForTesting
setNotificationDataManager(NotificationDataManager notificationDataManager)102     void setNotificationDataManager(NotificationDataManager notificationDataManager) {
103         mNotificationDataManager = notificationDataManager;
104     }
105 
106     @Override
onCreate()107     public void onCreate() {
108         super.onCreate();
109         mNotificationDataManager = NotificationDataManager.getInstance();
110         NotificationApplication app = (NotificationApplication) getApplication();
111 
112         mHeadsUpManager = new CarHeadsUpNotificationManager(/* context= */ this,
113                 app.getClickHandlerFactory(), new CarHeadsUpNotificationAppContainer(this));
114         mHeadsUpManager.registerHeadsUpNotificationStateChangeListener(this);
115         app.getCarUxRestrictionWrapper().setCarHeadsUpNotificationManager(mHeadsUpManager);
116     }
117 
118     @Override
onDestroy()119     public void onDestroy() {
120         super.onDestroy();
121         mHeadsUpManager.unregisterListeners();
122     }
123 
124     @Override
onBind(Intent intent)125     public IBinder onBind(Intent intent) {
126         return ACTION_LOCAL_BINDING.equals(intent.getAction())
127                 ? new LocalBinder() : super.onBind(intent);
128     }
129 
130     @Override
onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)131     public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
132         if (sbn == null) {
133             Log.e(TAG, "onNotificationPosted: StatusBarNotification is null");
134             return;
135         }
136 
137         if (DEBUG) {
138             Log.d(TAG, "onNotificationPosted: " + sbn);
139             Log.d(TAG, "Is incoming notification a group summary?: "
140                     + sbn.getNotification().isGroupSummary());
141         }
142         if (!isNotificationForCurrentUser(sbn)) {
143             if (DEBUG) {
144                 Log.d(TAG, "Notification is not for current user: " + sbn);
145                 Log.d(TAG, "Notification user: " + sbn.getUser().getIdentifier());
146                 Log.d(TAG, "Current user: " + NotificationUtils.getCurrentUser(getContext()));
147             }
148             return;
149         }
150         AlertEntry alertEntry = new AlertEntry(sbn);
151         onNotificationRankingUpdate(rankingMap);
152         notifyNotificationPosted(alertEntry);
153     }
154 
155     @Override
onNotificationRemoved(StatusBarNotification sbn)156     public void onNotificationRemoved(StatusBarNotification sbn) {
157         if (sbn == null) {
158             Log.e(TAG, "onNotificationRemoved: StatusBarNotification is null");
159             return;
160         }
161 
162         if (DEBUG) {
163             Log.d(TAG, "onNotificationRemoved: " + sbn);
164         }
165 
166         if (!isNotificationForCurrentUser(sbn)) {
167             if (DEBUG) {
168                 Log.d(TAG, "Notification is not for current user: " + sbn);
169                 Log.d(TAG, "Notification user: " + sbn.getUser().getIdentifier());
170                 Log.d(TAG, "Current user: " + NotificationUtils.getCurrentUser(getContext()));
171             }
172             return;
173         }
174 
175         AlertEntry alertEntry = mActiveNotifications.get(sbn.getKey());
176 
177         if (alertEntry != null) {
178             mActiveNotifications.remove(alertEntry.getKey());
179         } else {
180             // HUN notifications are not tracked in mActiveNotifications but still need to be
181             // removed
182             alertEntry = new AlertEntry(sbn);
183         }
184 
185         removeNotification(alertEntry);
186     }
187 
188     @Override
onNotificationRankingUpdate(RankingMap rankingMap)189     public void onNotificationRankingUpdate(RankingMap rankingMap) {
190         mRankingMap = rankingMap;
191         boolean overrideGroupKeyUpdated = false;
192         for (AlertEntry alertEntry : mActiveNotifications.values()) {
193             if (updateOverrideGroupKey(alertEntry)) {
194                 overrideGroupKeyUpdated = true;
195             }
196         }
197         if (overrideGroupKeyUpdated) {
198             sendNotificationEventToHandler(/* alertEntry= */ null, NOTIFY_RANKING_UPDATED);
199         }
200     }
201 
updateOverrideGroupKey(AlertEntry alertEntry)202     private boolean updateOverrideGroupKey(AlertEntry alertEntry) {
203         if (!mRankingMap.getRanking(alertEntry.getKey(), mTemporaryRanking)) {
204             if (DEBUG) {
205                 Log.d(TAG, "OverrideGroupKey not applied: " + alertEntry);
206             }
207             return false;
208         }
209 
210         String oldOverrideGroupKey =
211                 alertEntry.getStatusBarNotification().getOverrideGroupKey();
212         String newOverrideGroupKey = getOverrideGroupKey(alertEntry.getKey());
213         if (Objects.equals(oldOverrideGroupKey, newOverrideGroupKey)) {
214             return false;
215         }
216         alertEntry.getStatusBarNotification().setOverrideGroupKey(newOverrideGroupKey);
217         return true;
218     }
219 
220     /**
221      * Get the override group key of a {@link AlertEntry} given its key.
222      */
223     @Nullable
getOverrideGroupKey(String key)224     private String getOverrideGroupKey(String key) {
225         if (mRankingMap != null) {
226             mRankingMap.getRanking(key, mTemporaryRanking);
227             return mTemporaryRanking.getOverrideGroupKey();
228         }
229         return null;
230     }
231 
232     /**
233      * Get all active notifications that are not heads-up notifications.
234      *
235      * @return a map of all active notifications with key being the notification key.
236      */
getNotifications()237     Map<String, AlertEntry> getNotifications() {
238         return mActiveNotifications.entrySet().stream()
239                 .filter(x -> (isNotificationForCurrentUser(
240                         x.getValue().getStatusBarNotification())))
241                 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
242     }
243 
244     @Override
getCurrentRanking()245     public RankingMap getCurrentRanking() {
246         return mRankingMap;
247     }
248 
249     @Override
onListenerConnected()250     public void onListenerConnected() {
251         mActiveNotifications = Stream.of(getActiveNotifications()).collect(
252                 Collectors.toConcurrentMap(StatusBarNotification::getKey, AlertEntry::new));
253         mRankingMap = super.getCurrentRanking();
254         mIsListenerConnected = true;
255     }
256 
257     @Override
onListenerDisconnected()258     public void onListenerDisconnected() {
259         mIsListenerConnected = false;
260     }
261 
setHandler(Handler handler)262     public void setHandler(Handler handler) {
263         mHandler = handler;
264     }
265 
266     /**
267      * Clears all local cached variables.
268      * Note: This is a blocking call so should not execute any long-running or time-consuming tasks
269      * like storing cache.
270      */
clearCache()271     public void clearCache() {
272         mHeadsUpManager.clearCache();
273         mNotificationDataManager.clearAll();
274         mActiveNotifications.clear();
275     }
276 
277     /**
278      * Called when Notification Panel's visibility changes.
279      */
onVisibilityChanged(boolean isVisible)280     public void onVisibilityChanged(boolean isVisible) {
281         mIsNotificationPanelVisible = isVisible;
282         if (mIsNotificationPanelVisible) {
283             mHeadsUpManager.releaseQueue();
284         }
285     }
286 
287 
notifyNotificationPosted(AlertEntry alertEntry)288     private void notifyNotificationPosted(AlertEntry alertEntry) {
289         if (isNotificationHigherThanLowImportance(alertEntry)) {
290             mNotificationDataManager.addNewMessageNotification(alertEntry);
291         } else {
292             mNotificationDataManager.untrackUnseenNotification(alertEntry);
293         }
294 
295         boolean isShowingHeadsUp = false;
296         if (!mIsNotificationPanelVisible
297                 || !CarHeadsUpNotificationManager.isHeadsUpDismissible(alertEntry)) {
298             isShowingHeadsUp = mHeadsUpManager.maybeShowHeadsUp(alertEntry, getCurrentRanking(),
299                     mActiveNotifications);
300         }
301         if (DEBUG) {
302             Log.d(TAG, "Is " + alertEntry + " shown as HUN?: " + isShowingHeadsUp);
303         }
304         if (!isShowingHeadsUp) {
305             updateOverrideGroupKey(alertEntry);
306             postNewNotification(alertEntry);
307         }
308     }
309 
isNotificationForCurrentUser(StatusBarNotification sbn)310     private boolean isNotificationForCurrentUser(StatusBarNotification sbn) {
311         // Notifications should only be shown for the current user and the the notifications from
312         // the system when CarNotification is running as SystemUI component.
313         return (sbn.getUser().getIdentifier() == NotificationUtils.getCurrentUser(getContext())
314                 || sbn.getUser().getIdentifier() == UserHandle.USER_ALL);
315     }
316 
317     @Override
onStateChange(AlertEntry alertEntry, CarHeadsUpNotificationManager.HeadsUpState headsUpState)318     public void onStateChange(AlertEntry alertEntry,
319             CarHeadsUpNotificationManager.HeadsUpState headsUpState) {
320         if (headsUpState == CarHeadsUpNotificationManager.HeadsUpState.DISMISSED
321                 || headsUpState == CarHeadsUpNotificationManager.HeadsUpState.REMOVED_FROM_QUEUE) {
322             updateOverrideGroupKey(alertEntry);
323             postNewNotification(alertEntry);
324         }
325     }
326 
327     class LocalBinder extends Binder {
getService()328         public CarNotificationListener getService() {
329             return CarNotificationListener.this;
330         }
331     }
332 
postNewNotification(AlertEntry alertEntry)333     private void postNewNotification(AlertEntry alertEntry) {
334         mActiveNotifications.put(alertEntry.getKey(), alertEntry);
335         sendNotificationEventToHandler(alertEntry, NOTIFY_NOTIFICATION_POSTED);
336     }
337 
removeNotification(AlertEntry alertEntry)338     private void removeNotification(AlertEntry alertEntry) {
339         mHeadsUpManager.maybeRemoveHeadsUp(alertEntry);
340         sendNotificationEventToHandler(alertEntry, NOTIFY_NOTIFICATION_REMOVED);
341     }
342 
sendNotificationEventToHandler(AlertEntry alertEntry, int eventType)343     private void sendNotificationEventToHandler(AlertEntry alertEntry, int eventType) {
344         if (mHandler == null) {
345             return;
346         }
347         Message msg = Message.obtain(mHandler);
348         msg.what = eventType;
349         msg.obj = alertEntry;
350         mHandler.sendMessage(msg);
351     }
352 
isNotificationHigherThanLowImportance(AlertEntry alertEntry)353     private boolean isNotificationHigherThanLowImportance(AlertEntry alertEntry) {
354         Ranking ranking = new NotificationListenerService.Ranking();
355         mRankingMap.getRanking(alertEntry.getKey(), ranking);
356         return ranking.getImportance() > NotificationManager.IMPORTANCE_LOW;
357     }
358 
359     @VisibleForTesting
getIsListenerConnected()360     boolean getIsListenerConnected() {
361         return mIsListenerConnected;
362     }
363 }
364