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