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         Log.d(TAG, "onNotificationPosted: " + sbn);
118         if (!isNotificationForCurrentUser(sbn)) {
119             return;
120         }
121         AlertEntry alertEntry = new AlertEntry(sbn);
122         onNotificationRankingUpdate(rankingMap);
123         notifyNotificationPosted(alertEntry);
124     }
125 
126     @Override
onNotificationRemoved(StatusBarNotification sbn)127     public void onNotificationRemoved(StatusBarNotification sbn) {
128         Log.d(TAG, "onNotificationRemoved: " + sbn);
129 
130         AlertEntry alertEntry = mActiveNotifications.get(sbn.getKey());
131 
132         if (alertEntry != null) {
133             mActiveNotifications.remove(alertEntry.getKey());
134         } else {
135             // HUN notifications are not tracked in mActiveNotifications but still need to be
136             // removed
137             alertEntry = new AlertEntry(sbn);
138         }
139 
140         removeNotification(alertEntry);
141     }
142 
143     @Override
onNotificationRankingUpdate(RankingMap rankingMap)144     public void onNotificationRankingUpdate(RankingMap rankingMap) {
145         mRankingMap = rankingMap;
146         for (AlertEntry alertEntry : mActiveNotifications.values()) {
147             if (!mRankingMap.getRanking(alertEntry.getKey(), mTemporaryRanking)) {
148                 continue;
149             }
150             String oldOverrideGroupKey =
151                     alertEntry.getStatusBarNotification().getOverrideGroupKey();
152             String newOverrideGroupKey = getOverrideGroupKey(alertEntry.getKey());
153             if (!Objects.equals(oldOverrideGroupKey, newOverrideGroupKey)) {
154                 alertEntry.getStatusBarNotification().setOverrideGroupKey(newOverrideGroupKey);
155             }
156         }
157     }
158 
159     /**
160      * Get the override group key of a {@link AlertEntry} given its key.
161      */
162     @Nullable
getOverrideGroupKey(String key)163     private String getOverrideGroupKey(String key) {
164         if (mRankingMap != null) {
165             mRankingMap.getRanking(key, mTemporaryRanking);
166             return mTemporaryRanking.getOverrideGroupKey();
167         }
168         return null;
169     }
170 
171     /**
172      * Get all active notifications that are not heads-up notifications.
173      *
174      * @return a map of all active notifications with key being the notification key.
175      */
getNotifications()176     Map<String, AlertEntry> getNotifications() {
177         return mActiveNotifications.entrySet().stream()
178                 .filter(x -> (isNotificationForCurrentUser(
179                         x.getValue().getStatusBarNotification())))
180                 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
181     }
182 
183     @Override
getCurrentRanking()184     public RankingMap getCurrentRanking() {
185         return mRankingMap;
186     }
187 
188     @Override
onListenerConnected()189     public void onListenerConnected() {
190         mActiveNotifications = Stream.of(getActiveNotifications()).collect(
191                 Collectors.toMap(StatusBarNotification::getKey, sbn -> new AlertEntry(sbn)));
192         mRankingMap = super.getCurrentRanking();
193     }
194 
195     @Override
onListenerDisconnected()196     public void onListenerDisconnected() {
197     }
198 
setHandler(Handler handler)199     public void setHandler(Handler handler) {
200         mHandler = handler;
201     }
202 
notifyNotificationPosted(AlertEntry alertEntry)203     private void notifyNotificationPosted(AlertEntry alertEntry) {
204         if (shouldTrackUnseen(alertEntry)) {
205             mNotificationDataManager.addNewMessageNotification(alertEntry);
206         } else {
207             mNotificationDataManager.untrackUnseenNotification(alertEntry);
208         }
209 
210         boolean isShowingHeadsUp = mHeadsUpManager.maybeShowHeadsUp(alertEntry, getCurrentRanking(),
211                 mActiveNotifications);
212 
213         if (!isShowingHeadsUp) {
214             postNewNotification(alertEntry);
215         }
216     }
217 
isNotificationForCurrentUser(StatusBarNotification sbn)218     private boolean isNotificationForCurrentUser(StatusBarNotification sbn) {
219         // Notifications should only be shown for the current user and the the notifications from
220         // the system when CarNotification is running as SystemUI component.
221         return (sbn.getUser().getIdentifier() == ActivityManager.getCurrentUser()
222                 || sbn.getUser().getIdentifier() == UserHandle.USER_ALL);
223     }
224 
225 
226     @Override
onStateChange(AlertEntry alertEntry, boolean isHeadsUp)227     public void onStateChange(AlertEntry alertEntry, boolean isHeadsUp) {
228         // No more a HUN
229         if (!isHeadsUp) {
230             postNewNotification(alertEntry);
231         }
232     }
233 
234     class LocalBinder extends Binder {
getService()235         public CarNotificationListener getService() {
236             return CarNotificationListener.this;
237         }
238     }
239 
postNewNotification(AlertEntry alertEntry)240     private void postNewNotification(AlertEntry alertEntry) {
241         mActiveNotifications.put(alertEntry.getKey(), alertEntry);
242         sendNotificationEventToHandler(alertEntry, NOTIFY_NOTIFICATION_POSTED);
243     }
244 
removeNotification(AlertEntry alertEntry)245     private void removeNotification(AlertEntry alertEntry) {
246         mHeadsUpManager.maybeRemoveHeadsUp(alertEntry);
247         sendNotificationEventToHandler(alertEntry, NOTIFY_NOTIFICATION_REMOVED);
248     }
249 
sendNotificationEventToHandler(AlertEntry alertEntry, int eventType)250     private void sendNotificationEventToHandler(AlertEntry alertEntry, int eventType) {
251         if (mHandler == null) {
252             return;
253         }
254         Message msg = Message.obtain(mHandler);
255         msg.what = eventType;
256         msg.obj = alertEntry;
257         mHandler.sendMessage(msg);
258     }
259 
260     // Don't show unseen markers for <= LOW importance notifications to be consistent
261     // with how these notifications are handled on phones
shouldTrackUnseen(AlertEntry alertEntry)262     boolean shouldTrackUnseen(AlertEntry alertEntry) {
263         Ranking ranking = new NotificationListenerService.Ranking();
264         mRankingMap.getRanking(alertEntry.getKey(), ranking);
265         return ranking.getImportance() > NotificationManager.IMPORTANCE_LOW;
266     }
267 }
268