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