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         Log.d(TAG, "onNotificationPosted: " + sbn);
130         if (DEBUG) {
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         Log.d(TAG, "onNotificationRemoved: " + sbn);
155         AlertEntry alertEntry = mActiveNotifications.get(sbn.getKey());
156 
157         if (alertEntry != null) {
158             mActiveNotifications.remove(alertEntry.getKey());
159         } else {
160             // HUN notifications are not tracked in mActiveNotifications but still need to be
161             // removed
162             alertEntry = new AlertEntry(sbn);
163         }
164 
165         removeNotification(alertEntry);
166     }
167 
168     @Override
onNotificationRankingUpdate(RankingMap rankingMap)169     public void onNotificationRankingUpdate(RankingMap rankingMap) {
170         mRankingMap = rankingMap;
171         boolean overrideGroupKeyUpdated = false;
172         for (AlertEntry alertEntry : mActiveNotifications.values()) {
173             if (updateOverrideGroupKey(alertEntry)) {
174                 overrideGroupKeyUpdated = true;
175             }
176         }
177         if (overrideGroupKeyUpdated) {
178             sendNotificationEventToHandler(/* alertEntry= */ null, NOTIFY_RANKING_UPDATED);
179         }
180     }
181 
updateOverrideGroupKey(AlertEntry alertEntry)182     private boolean updateOverrideGroupKey(AlertEntry alertEntry) {
183         if (!mRankingMap.getRanking(alertEntry.getKey(), mTemporaryRanking)) {
184             if (DEBUG) {
185                 Log.d(TAG, "OverrideGroupKey not applied: " + alertEntry);
186             }
187             return false;
188         }
189 
190         String oldOverrideGroupKey =
191                 alertEntry.getStatusBarNotification().getOverrideGroupKey();
192         String newOverrideGroupKey = getOverrideGroupKey(alertEntry.getKey());
193         if (Objects.equals(oldOverrideGroupKey, newOverrideGroupKey)) {
194             return false;
195         }
196         alertEntry.getStatusBarNotification().setOverrideGroupKey(newOverrideGroupKey);
197         return true;
198     }
199 
200     /**
201      * Get the override group key of a {@link AlertEntry} given its key.
202      */
203     @Nullable
getOverrideGroupKey(String key)204     private String getOverrideGroupKey(String key) {
205         if (mRankingMap != null) {
206             mRankingMap.getRanking(key, mTemporaryRanking);
207             return mTemporaryRanking.getOverrideGroupKey();
208         }
209         return null;
210     }
211 
212     /**
213      * Get all active notifications that are not heads-up notifications.
214      *
215      * @return a map of all active notifications with key being the notification key.
216      */
getNotifications()217     Map<String, AlertEntry> getNotifications() {
218         return mActiveNotifications.entrySet().stream()
219                 .filter(x -> (isNotificationForCurrentUser(
220                         x.getValue().getStatusBarNotification())))
221                 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
222     }
223 
224     @Override
getCurrentRanking()225     public RankingMap getCurrentRanking() {
226         return mRankingMap;
227     }
228 
229     @Override
onListenerConnected()230     public void onListenerConnected() {
231         mActiveNotifications = Stream.of(getActiveNotifications()).collect(
232                 Collectors.toMap(StatusBarNotification::getKey, sbn -> new AlertEntry(sbn)));
233         mRankingMap = super.getCurrentRanking();
234     }
235 
236     @Override
onListenerDisconnected()237     public void onListenerDisconnected() {
238     }
239 
setHandler(Handler handler)240     public void setHandler(Handler handler) {
241         mHandler = handler;
242     }
243 
notifyNotificationPosted(AlertEntry alertEntry)244     private void notifyNotificationPosted(AlertEntry alertEntry) {
245         if (isNotificationHigherThanLowImportance(alertEntry)) {
246             mNotificationDataManager.addNewMessageNotification(alertEntry);
247         } else {
248             mNotificationDataManager.untrackUnseenNotification(alertEntry);
249         }
250 
251         boolean isShowingHeadsUp = mHeadsUpManager.maybeShowHeadsUp(alertEntry, getCurrentRanking(),
252                 mActiveNotifications);
253         if (DEBUG) {
254             Log.d(TAG, "Is " + alertEntry + " shown as HUN?: " + isShowingHeadsUp);
255         }
256         if (!isShowingHeadsUp) {
257             updateOverrideGroupKey(alertEntry);
258             postNewNotification(alertEntry);
259         }
260     }
261 
isNotificationForCurrentUser(StatusBarNotification sbn)262     private boolean isNotificationForCurrentUser(StatusBarNotification sbn) {
263         // Notifications should only be shown for the current user and the the notifications from
264         // the system when CarNotification is running as SystemUI component.
265         return (sbn.getUser().getIdentifier() == ActivityManager.getCurrentUser()
266                 || sbn.getUser().getIdentifier() == UserHandle.USER_ALL);
267     }
268 
269 
270     @Override
onStateChange(AlertEntry alertEntry, boolean isHeadsUp)271     public void onStateChange(AlertEntry alertEntry, boolean isHeadsUp) {
272         // No more a HUN
273         if (!isHeadsUp) {
274             updateOverrideGroupKey(alertEntry);
275             postNewNotification(alertEntry);
276         }
277     }
278 
279     class LocalBinder extends Binder {
getService()280         public CarNotificationListener getService() {
281             return CarNotificationListener.this;
282         }
283     }
284 
postNewNotification(AlertEntry alertEntry)285     private void postNewNotification(AlertEntry alertEntry) {
286         mActiveNotifications.put(alertEntry.getKey(), alertEntry);
287         sendNotificationEventToHandler(alertEntry, NOTIFY_NOTIFICATION_POSTED);
288     }
289 
removeNotification(AlertEntry alertEntry)290     private void removeNotification(AlertEntry alertEntry) {
291         mHeadsUpManager.maybeRemoveHeadsUp(alertEntry);
292         sendNotificationEventToHandler(alertEntry, NOTIFY_NOTIFICATION_REMOVED);
293     }
294 
sendNotificationEventToHandler(AlertEntry alertEntry, int eventType)295     private void sendNotificationEventToHandler(AlertEntry alertEntry, int eventType) {
296         if (mHandler == null) {
297             return;
298         }
299         Message msg = Message.obtain(mHandler);
300         msg.what = eventType;
301         msg.obj = alertEntry;
302         mHandler.sendMessage(msg);
303     }
304 
isNotificationHigherThanLowImportance(AlertEntry alertEntry)305     private boolean isNotificationHigherThanLowImportance(AlertEntry alertEntry) {
306         Ranking ranking = new NotificationListenerService.Ranking();
307         mRankingMap.getRanking(alertEntry.getKey(), ranking);
308         return ranking.getImportance() > NotificationManager.IMPORTANCE_LOW;
309     }
310 }
311