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