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