• 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.ActivityManager;
20 import android.app.NotificationManager;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.os.Binder;
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 com.android.car.notification.headsup.CarHeadsUpNotificationAppContainer;
35 
36 import java.util.HashMap;
37 import java.util.Map;
38 import java.util.Objects;
39 import java.util.stream.Collectors;
40 import java.util.stream.Stream;
41 
42 /**
43  * NotificationListenerService that fetches all notifications from system.
44  */
45 public class CarNotificationListener extends NotificationListenerService implements
46         CarHeadsUpNotificationManager.OnHeadsUpNotificationStateChange {
47     private static final String TAG = "CarNotificationListener";
48     static final String ACTION_LOCAL_BINDING = "local_binding";
49     static final int NOTIFY_NOTIFICATION_POSTED = 1;
50     static final int NOTIFY_NOTIFICATION_REMOVED = 2;
51     /** Temporary {@link Ranking} object that serves as a reused value holder */
52     final private Ranking mTemporaryRanking = new Ranking();
53 
54     private Handler mHandler;
55     private RankingMap mRankingMap;
56     private CarHeadsUpNotificationManager mHeadsUpManager;
57     private NotificationDataManager mNotificationDataManager;
58 
59     /**
60      * Map that contains all the active notifications that are not currently HUN. These
61      * notifications may or may not be visible to the user if they get filtered out. The only time
62      * these will be removed from the map is when the {@llink NotificationListenerService} calls the
63      * onNotificationRemoved method. New notifications will be added to this map if the notification
64      * is posted as a non-HUN or when a HUN's state is changed to non-HUN.
65      */
66     private Map<String, AlertEntry> mActiveNotifications = new HashMap<>();
67 
68     /**
69      * Call this if to register this service as a system service and connect to HUN. This is useful
70      * if the notification service is being used as a lib instead of a standalone app. The
71      * standalone app version has a manifest entry that will have the same effect.
72      *
73      * @param context Context required for registering the service.
74      * @param carUxRestrictionManagerWrapper will have the heads up manager registered with it.
75      * @param carHeadsUpNotificationManager HUN controller.
76      * @param notificationDataManager used for keeping track of additional notification states.
77      */
registerAsSystemService(Context context, CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper, CarHeadsUpNotificationManager carHeadsUpNotificationManager, NotificationDataManager notificationDataManager)78     public void registerAsSystemService(Context context,
79             CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper,
80             CarHeadsUpNotificationManager carHeadsUpNotificationManager,
81             NotificationDataManager notificationDataManager) {
82         try {
83             mNotificationDataManager = notificationDataManager;
84             registerAsSystemService(context,
85                     new ComponentName(context.getPackageName(), getClass().getCanonicalName()),
86                     ActivityManager.getCurrentUser());
87             mHeadsUpManager = carHeadsUpNotificationManager;
88             mHeadsUpManager.registerHeadsUpNotificationStateChangeListener(this);
89             carUxRestrictionManagerWrapper.setCarHeadsUpNotificationManager(
90                     carHeadsUpNotificationManager);
91         } catch (RemoteException e) {
92             Log.e(TAG, "Unable to register notification listener", e);
93         }
94     }
95 
96     @Override
onCreate()97     public void onCreate() {
98         super.onCreate();
99         mNotificationDataManager = new NotificationDataManager();
100         NotificationApplication app = (NotificationApplication) getApplication();
101 
102         app.getClickHandlerFactory().setNotificationDataManager(mNotificationDataManager);
103         mHeadsUpManager = new CarHeadsUpNotificationManager(/* context= */ this,
104                 app.getClickHandlerFactory(), mNotificationDataManager, new CarHeadsUpNotificationAppContainer(this));
105         mHeadsUpManager.registerHeadsUpNotificationStateChangeListener(this);
106         app.getCarUxRestrictionWrapper().setCarHeadsUpNotificationManager(mHeadsUpManager);
107     }
108 
109     @Override
onBind(Intent intent)110     public IBinder onBind(Intent intent) {
111         return ACTION_LOCAL_BINDING.equals(intent.getAction())
112                 ? new LocalBinder() : super.onBind(intent);
113     }
114 
115     @Override
onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)116     public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
117         if (sbn == null) {
118             Log.e(TAG, "onNotificationPosted: StatusBarNotification is null");
119             return;
120         }
121 
122         Log.d(TAG, "onNotificationPosted: " + sbn);
123         if (!isNotificationForCurrentUser(sbn)) {
124             return;
125         }
126         AlertEntry alertEntry = new AlertEntry(sbn);
127         onNotificationRankingUpdate(rankingMap);
128         notifyNotificationPosted(alertEntry);
129     }
130 
131     @Override
onNotificationRemoved(StatusBarNotification sbn)132     public void onNotificationRemoved(StatusBarNotification sbn) {
133         if (sbn == null) {
134             Log.e(TAG, "onNotificationRemoved: StatusBarNotification is null");
135             return;
136         }
137 
138         Log.d(TAG, "onNotificationRemoved: " + sbn);
139         AlertEntry alertEntry = mActiveNotifications.get(sbn.getKey());
140 
141         if (alertEntry != null) {
142             mActiveNotifications.remove(alertEntry.getKey());
143         } else {
144             // HUN notifications are not tracked in mActiveNotifications but still need to be
145             // removed
146             alertEntry = new AlertEntry(sbn);
147         }
148 
149         removeNotification(alertEntry);
150     }
151 
152     @Override
onNotificationRankingUpdate(RankingMap rankingMap)153     public void onNotificationRankingUpdate(RankingMap rankingMap) {
154         mRankingMap = rankingMap;
155         for (AlertEntry alertEntry : mActiveNotifications.values()) {
156             if (!mRankingMap.getRanking(alertEntry.getKey(), mTemporaryRanking)) {
157                 continue;
158             }
159             String oldOverrideGroupKey =
160                     alertEntry.getStatusBarNotification().getOverrideGroupKey();
161             String newOverrideGroupKey = getOverrideGroupKey(alertEntry.getKey());
162             if (!Objects.equals(oldOverrideGroupKey, newOverrideGroupKey)) {
163                 alertEntry.getStatusBarNotification().setOverrideGroupKey(newOverrideGroupKey);
164             }
165         }
166     }
167 
168     /**
169      * Get the override group key of a {@link AlertEntry} given its key.
170      */
171     @Nullable
getOverrideGroupKey(String key)172     private String getOverrideGroupKey(String key) {
173         if (mRankingMap != null) {
174             mRankingMap.getRanking(key, mTemporaryRanking);
175             return mTemporaryRanking.getOverrideGroupKey();
176         }
177         return null;
178     }
179 
180     /**
181      * Get all active notifications that are not heads-up notifications.
182      *
183      * @return a map of all active notifications with key being the notification key.
184      */
getNotifications()185     Map<String, AlertEntry> getNotifications() {
186         return mActiveNotifications.entrySet().stream()
187                 .filter(x -> (isNotificationForCurrentUser(
188                         x.getValue().getStatusBarNotification())))
189                 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
190     }
191 
192     @Override
getCurrentRanking()193     public RankingMap getCurrentRanking() {
194         return mRankingMap;
195     }
196 
197     @Override
onListenerConnected()198     public void onListenerConnected() {
199         mActiveNotifications = Stream.of(getActiveNotifications()).collect(
200                 Collectors.toMap(StatusBarNotification::getKey, sbn -> new AlertEntry(sbn)));
201         mRankingMap = super.getCurrentRanking();
202     }
203 
204     @Override
onListenerDisconnected()205     public void onListenerDisconnected() {
206     }
207 
setHandler(Handler handler)208     public void setHandler(Handler handler) {
209         mHandler = handler;
210     }
211 
notifyNotificationPosted(AlertEntry alertEntry)212     private void notifyNotificationPosted(AlertEntry alertEntry) {
213         if (shouldTrackUnseen(alertEntry)) {
214             mNotificationDataManager.addNewMessageNotification(alertEntry);
215         } else {
216             mNotificationDataManager.untrackUnseenNotification(alertEntry);
217         }
218 
219         boolean isShowingHeadsUp = mHeadsUpManager.maybeShowHeadsUp(alertEntry, getCurrentRanking(),
220                 mActiveNotifications);
221 
222         if (!isShowingHeadsUp) {
223             postNewNotification(alertEntry);
224         }
225     }
226 
isNotificationForCurrentUser(StatusBarNotification sbn)227     private boolean isNotificationForCurrentUser(StatusBarNotification sbn) {
228         // Notifications should only be shown for the current user and the the notifications from
229         // the system when CarNotification is running as SystemUI component.
230         return (sbn.getUser().getIdentifier() == ActivityManager.getCurrentUser()
231                 || sbn.getUser().getIdentifier() == UserHandle.USER_ALL);
232     }
233 
234 
235     @Override
onStateChange(AlertEntry alertEntry, boolean isHeadsUp)236     public void onStateChange(AlertEntry alertEntry, boolean isHeadsUp) {
237         // No more a HUN
238         if (!isHeadsUp) {
239             postNewNotification(alertEntry);
240         }
241     }
242 
243     class LocalBinder extends Binder {
getService()244         public CarNotificationListener getService() {
245             return CarNotificationListener.this;
246         }
247     }
248 
postNewNotification(AlertEntry alertEntry)249     private void postNewNotification(AlertEntry alertEntry) {
250         mActiveNotifications.put(alertEntry.getKey(), alertEntry);
251         sendNotificationEventToHandler(alertEntry, NOTIFY_NOTIFICATION_POSTED);
252     }
253 
removeNotification(AlertEntry alertEntry)254     private void removeNotification(AlertEntry alertEntry) {
255         mHeadsUpManager.maybeRemoveHeadsUp(alertEntry);
256         sendNotificationEventToHandler(alertEntry, NOTIFY_NOTIFICATION_REMOVED);
257     }
258 
sendNotificationEventToHandler(AlertEntry alertEntry, int eventType)259     private void sendNotificationEventToHandler(AlertEntry alertEntry, int eventType) {
260         if (mHandler == null) {
261             return;
262         }
263         Message msg = Message.obtain(mHandler);
264         msg.what = eventType;
265         msg.obj = alertEntry;
266         mHandler.sendMessage(msg);
267     }
268 
269     // Don't show unseen markers for <= LOW importance notifications to be consistent
270     // with how these notifications are handled on phones
shouldTrackUnseen(AlertEntry alertEntry)271     boolean shouldTrackUnseen(AlertEntry alertEntry) {
272         Ranking ranking = new NotificationListenerService.Ranking();
273         mRankingMap.getRanking(alertEntry.getKey(), ranking);
274         return ranking.getImportance() > NotificationManager.IMPORTANCE_LOW;
275     }
276 }
277