• 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 static android.view.ViewTreeObserver.InternalInsetsInfo;
19 import static android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
20 import static android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
21 import static android.view.ViewTreeObserver.OnGlobalLayoutListener;
22 
23 import static com.android.car.assist.client.CarAssistUtils.isCarCompatibleMessagingNotification;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorListenerAdapter;
27 import android.animation.AnimatorSet;
28 import android.app.KeyguardManager;
29 import android.app.Notification;
30 import android.app.NotificationChannel;
31 import android.app.NotificationManager;
32 import android.car.drivingstate.CarUxRestrictions;
33 import android.car.drivingstate.CarUxRestrictionsManager;
34 import android.content.Context;
35 import android.service.notification.NotificationListenerService;
36 import android.util.Log;
37 import android.util.Pair;
38 import android.view.LayoutInflater;
39 import android.view.View;
40 import android.view.ViewTreeObserver;
41 
42 import androidx.annotation.VisibleForTesting;
43 
44 import com.android.car.notification.headsup.CarHeadsUpNotificationContainer;
45 import com.android.car.notification.headsup.animationhelper.HeadsUpNotificationAnimationHelper;
46 import com.android.car.notification.template.MessageNotificationViewHolder;
47 
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Objects;
53 
54 /**
55  * Notification Manager for heads-up notifications in car.
56  */
57 public class CarHeadsUpNotificationManager
58         implements CarUxRestrictionsManager.OnUxRestrictionsChangedListener {
59 
60     /**
61      * Callback that will be issued after a Heads up notification state is changed.
62      */
63     public interface OnHeadsUpNotificationStateChange {
64         /**
65          * Will be called if a new notification added/updated changes the heads up state for that
66          * notification.
67          */
onStateChange(AlertEntry alertEntry, boolean isHeadsUp)68         void onStateChange(AlertEntry alertEntry, boolean isHeadsUp);
69     }
70 
71     private static final String TAG = CarHeadsUpNotificationManager.class.getSimpleName();
72 
73     private final Beeper mBeeper;
74     private final Context mContext;
75     private final boolean mEnableNavigationHeadsup;
76     private final long mDuration;
77     private final long mMinDisplayDuration;
78     private HeadsUpNotificationAnimationHelper mAnimationHelper;
79     private final int mNotificationHeadsUpCardMarginTop;
80 
81     private final KeyguardManager mKeyguardManager;
82     private final PreprocessingManager mPreprocessingManager;
83     private final LayoutInflater mInflater;
84     private final CarHeadsUpNotificationContainer mHunContainer;
85 
86     // key for the map is the statusbarnotification key
87     private final Map<String, HeadsUpEntry> mActiveHeadsUpNotifications = new HashMap<>();
88     private final List<OnHeadsUpNotificationStateChange> mNotificationStateChangeListeners =
89             new ArrayList<>();
90     private final Map<HeadsUpEntry,
91             Pair<OnComputeInternalInsetsListener, OnGlobalFocusChangeListener>>
92             mRegisteredViewTreeListeners = new HashMap<>();
93 
94     private boolean mShouldRestrictMessagePreview;
95     private NotificationClickHandlerFactory mClickHandlerFactory;
96     private NotificationDataManager mNotificationDataManager;
97 
98 
CarHeadsUpNotificationManager(Context context, NotificationClickHandlerFactory clickHandlerFactory, NotificationDataManager notificationDataManager, CarHeadsUpNotificationContainer hunContainer)99     public CarHeadsUpNotificationManager(Context context,
100             NotificationClickHandlerFactory clickHandlerFactory,
101             NotificationDataManager notificationDataManager,
102             CarHeadsUpNotificationContainer hunContainer) {
103         mContext = context.getApplicationContext();
104         mEnableNavigationHeadsup =
105                 context.getResources().getBoolean(R.bool.config_showNavigationHeadsup);
106         mClickHandlerFactory = clickHandlerFactory;
107         mNotificationDataManager = notificationDataManager;
108         mBeeper = new Beeper(mContext);
109         mDuration = mContext.getResources().getInteger(R.integer.headsup_notification_duration_ms);
110         mNotificationHeadsUpCardMarginTop = (int) mContext.getResources().getDimension(
111                 R.dimen.headsup_notification_top_margin);
112         mMinDisplayDuration = mContext.getResources().getInteger(
113                 R.integer.heads_up_notification_minimum_time);
114         mAnimationHelper = getAnimationHelper();
115 
116         mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
117         mPreprocessingManager = PreprocessingManager.getInstance(context);
118         mInflater = LayoutInflater.from(mContext);
119         mClickHandlerFactory.registerClickListener(
120                 (launchResult, alertEntry) -> dismissHun(alertEntry));
121         mHunContainer = hunContainer;
122     }
123 
getAnimationHelper()124     private HeadsUpNotificationAnimationHelper getAnimationHelper() {
125         String helperName = mContext.getResources().getString(
126                 R.string.config_headsUpNotificationAnimationHelper);
127         try {
128             Class<?> clazz = Class.forName(helperName);
129             return (HeadsUpNotificationAnimationHelper) clazz.getConstructor().newInstance();
130         } catch (Exception e) {
131             throw new IllegalArgumentException(
132                     String.format("Invalid animation helper: %s", helperName), e);
133         }
134     }
135 
136     /**
137      * Show the notification as a heads-up if it meets the criteria.
138      *
139      * <p>Return's true if the notification will be shown as a heads up, false otherwise.
140      */
maybeShowHeadsUp( AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap, Map<String, AlertEntry> activeNotifications)141     public boolean maybeShowHeadsUp(
142             AlertEntry alertEntry,
143             NotificationListenerService.RankingMap rankingMap,
144             Map<String, AlertEntry> activeNotifications) {
145         if (!shouldShowHeadsUp(alertEntry, rankingMap)) {
146             // check if this is an update to the existing notification and if it should still show
147             // as a heads up or not.
148             HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
149                     alertEntry.getKey());
150             if (currentActiveHeadsUpNotification == null) {
151                 return false;
152             }
153             if (CarNotificationDiff.sameNotificationKey(currentActiveHeadsUpNotification,
154                     alertEntry)
155                     && currentActiveHeadsUpNotification.getHandler().hasMessagesOrCallbacks()) {
156                 dismissHun(alertEntry);
157             }
158             return false;
159         }
160         if (!activeNotifications.containsKey(alertEntry.getKey()) || canUpdate(alertEntry)
161                 || alertAgain(alertEntry.getNotification())) {
162             showHeadsUp(mPreprocessingManager.optimizeForDriving(alertEntry),
163                     rankingMap);
164             return true;
165         }
166         return false;
167     }
168 
169     /**
170      * This method gets called when an app wants to cancel or withdraw its notification.
171      */
maybeRemoveHeadsUp(AlertEntry alertEntry)172     public void maybeRemoveHeadsUp(AlertEntry alertEntry) {
173         HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
174                 alertEntry.getKey());
175         // if the heads up notification is already removed do nothing.
176         if (currentActiveHeadsUpNotification == null) {
177             return;
178         }
179 
180         long totalDisplayDuration =
181                 System.currentTimeMillis() - currentActiveHeadsUpNotification.getPostTime();
182         // ongoing notification that has passed the minimum threshold display time.
183         if (totalDisplayDuration >= mMinDisplayDuration) {
184             removeHun(alertEntry);
185             return;
186         }
187 
188         long earliestRemovalTime = mMinDisplayDuration - totalDisplayDuration;
189 
190         currentActiveHeadsUpNotification.getHandler().postDelayed(() ->
191                 removeHun(alertEntry), earliestRemovalTime);
192     }
193 
194     /**
195      * Registers a new {@link OnHeadsUpNotificationStateChange} to the list of listeners.
196      */
registerHeadsUpNotificationStateChangeListener( OnHeadsUpNotificationStateChange listener)197     public void registerHeadsUpNotificationStateChangeListener(
198             OnHeadsUpNotificationStateChange listener) {
199         if (!mNotificationStateChangeListeners.contains(listener)) {
200             mNotificationStateChangeListeners.add(listener);
201         }
202     }
203 
204     /**
205      * Unregisters a {@link OnHeadsUpNotificationStateChange} from the list of listeners.
206      */
unregisterHeadsUpNotificationStateChangeListener( OnHeadsUpNotificationStateChange listener)207     public void unregisterHeadsUpNotificationStateChangeListener(
208             OnHeadsUpNotificationStateChange listener) {
209         mNotificationStateChangeListeners.remove(listener);
210     }
211 
212     /**
213      * Invokes all OnHeadsUpNotificationStateChange handlers registered in {@link
214      * OnHeadsUpNotificationStateChange}s array.
215      */
handleHeadsUpNotificationStateChanged(AlertEntry alertEntry, boolean isHeadsUp)216     private void handleHeadsUpNotificationStateChanged(AlertEntry alertEntry, boolean isHeadsUp) {
217         mNotificationStateChangeListeners.forEach(
218                 listener -> listener.onStateChange(alertEntry, isHeadsUp));
219     }
220 
221     /**
222      * Returns true if the notification's flag is not set to
223      * {@link Notification#FLAG_ONLY_ALERT_ONCE}
224      */
alertAgain(Notification newNotification)225     private boolean alertAgain(Notification newNotification) {
226         return (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0;
227     }
228 
229     /**
230      * Return true if the currently displaying notification have the same key as the new added
231      * notification. In that case it will be considered as an update to the currently displayed
232      * notification.
233      */
isUpdate(AlertEntry alertEntry)234     private boolean isUpdate(AlertEntry alertEntry) {
235         HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
236                 alertEntry.getKey());
237         if (currentActiveHeadsUpNotification == null) {
238             return false;
239         }
240         return CarNotificationDiff.sameNotificationKey(currentActiveHeadsUpNotification,
241                 alertEntry);
242     }
243 
244     /**
245      * Updates only when the notification is being displayed.
246      */
canUpdate(AlertEntry alertEntry)247     private boolean canUpdate(AlertEntry alertEntry) {
248         HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
249                 alertEntry.getKey());
250         return currentActiveHeadsUpNotification != null && System.currentTimeMillis() -
251                 currentActiveHeadsUpNotification.getPostTime() < mDuration;
252     }
253 
254     /**
255      * Returns the active headsUpEntry or creates a new one while adding it to the list of
256      * mActiveHeadsUpNotifications.
257      */
addNewHeadsUpEntry(AlertEntry alertEntry)258     private HeadsUpEntry addNewHeadsUpEntry(AlertEntry alertEntry) {
259         HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
260                 alertEntry.getKey());
261         if (currentActiveHeadsUpNotification == null) {
262             currentActiveHeadsUpNotification = new HeadsUpEntry(
263                     alertEntry.getStatusBarNotification());
264             handleHeadsUpNotificationStateChanged(alertEntry, /* isHeadsUp= */ true);
265             mActiveHeadsUpNotifications.put(alertEntry.getKey(),
266                     currentActiveHeadsUpNotification);
267             currentActiveHeadsUpNotification.mIsAlertAgain = alertAgain(
268                     alertEntry.getNotification());
269             currentActiveHeadsUpNotification.mIsNewHeadsUp = true;
270             return currentActiveHeadsUpNotification;
271         }
272         currentActiveHeadsUpNotification.mIsNewHeadsUp = false;
273         currentActiveHeadsUpNotification.mIsAlertAgain = alertAgain(
274                 alertEntry.getNotification());
275         if (currentActiveHeadsUpNotification.mIsAlertAgain) {
276             // This is a ongoing notification which needs to be alerted again to the user. This
277             // requires for the post time to be updated.
278             currentActiveHeadsUpNotification.updatePostTime();
279         }
280         return currentActiveHeadsUpNotification;
281     }
282 
283     /**
284      * Controls three major conditions while showing heads up notification.
285      * <p>
286      * <ol>
287      * <li> When a new HUN comes in it will be displayed with animations
288      * <li> If an update to existing HUN comes in which enforces to alert the HUN again to user,
289      * then the post time will be updated to current time. This will only be done if {@link
290      * Notification#FLAG_ONLY_ALERT_ONCE} flag is not set.
291      * <li> If an update to existing HUN comes in which just updates the data and does not want to
292      * alert itself again, then the animations will not be shown and the data will get updated. This
293      * will only be done if {@link Notification#FLAG_ONLY_ALERT_ONCE} flag is not set.
294      * </ol>
295      */
showHeadsUp(AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)296     private void showHeadsUp(AlertEntry alertEntry,
297             NotificationListenerService.RankingMap rankingMap) {
298         // Show animations only when there is no active HUN and notification is new. This check
299         // needs to be done here because after this the new notification will be added to the map
300         // holding ongoing notifications.
301         boolean shouldShowAnimation = !isUpdate(alertEntry);
302         HeadsUpEntry currentNotification = addNewHeadsUpEntry(alertEntry);
303         if (currentNotification.mIsNewHeadsUp) {
304             playSound(alertEntry, rankingMap);
305             setAutoDismissViews(currentNotification, alertEntry);
306         } else if (currentNotification.mIsAlertAgain) {
307             setAutoDismissViews(currentNotification, alertEntry);
308         }
309         CarNotificationTypeItem notificationTypeItem = NotificationUtils.getNotificationViewType(
310                 alertEntry);
311         currentNotification.setClickHandlerFactory(mClickHandlerFactory);
312 
313         if (currentNotification.getNotificationView() == null) {
314             currentNotification.setNotificationView(mInflater.inflate(
315                     notificationTypeItem.getHeadsUpTemplate(),
316                     null));
317             mHunContainer.displayNotification(currentNotification.getNotificationView(),
318                     notificationTypeItem);
319             currentNotification.setViewHolder(
320                     notificationTypeItem.getViewHolder(currentNotification.getNotificationView(),
321                             mClickHandlerFactory));
322         }
323 
324         currentNotification.getViewHolder().setHideDismissButton(!shouldDismissOnSwipe(alertEntry));
325 
326         if (mShouldRestrictMessagePreview && notificationTypeItem.getNotificationType()
327                 == NotificationViewType.MESSAGE) {
328             ((MessageNotificationViewHolder) currentNotification.getViewHolder())
329                     .bindRestricted(alertEntry, /* isInGroup= */ false, /* isHeadsUp= */ true);
330         } else {
331             currentNotification.getViewHolder().bind(alertEntry, /* isInGroup= */false,
332                     /* isHeadsUp= */ true);
333         }
334 
335         resetViewTreeListenersEntry(currentNotification);
336 
337         ViewTreeObserver viewTreeObserver =
338                 currentNotification.getNotificationView().getViewTreeObserver();
339 
340         // measure the size of the card and make that area of the screen touchable
341         OnComputeInternalInsetsListener onComputeInternalInsetsListener =
342                 info -> setInternalInsetsInfo(info, currentNotification,
343                         /* panelExpanded= */ false);
344         viewTreeObserver.addOnComputeInternalInsetsListener(onComputeInternalInsetsListener);
345         // Get the height of the notification view after onLayout() in order to animate the
346         // notification into the screen.
347         viewTreeObserver.addOnGlobalLayoutListener(
348                 new OnGlobalLayoutListener() {
349                     @Override
350                     public void onGlobalLayout() {
351                         View view = currentNotification.getNotificationView();
352                         if (shouldShowAnimation) {
353                             mAnimationHelper.resetHUNPosition(view);
354                             AnimatorSet animatorSet = mAnimationHelper.getAnimateInAnimator(
355                                     mContext, view);
356                             animatorSet.setTarget(view);
357                             animatorSet.start();
358                         }
359                         view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
360                     }
361                 });
362         // Reset the auto dismiss timeout for each rotary event.
363         OnGlobalFocusChangeListener onGlobalFocusChangeListener =
364                 (oldFocus, newFocus) -> setAutoDismissViews(currentNotification, alertEntry);
365         viewTreeObserver.addOnGlobalFocusChangeListener(onGlobalFocusChangeListener);
366 
367         mRegisteredViewTreeListeners.put(currentNotification,
368                 new Pair<>(onComputeInternalInsetsListener, onGlobalFocusChangeListener));
369 
370         if (currentNotification.mIsNewHeadsUp) {
371             // Add swipe gesture
372             View cardView = currentNotification.getNotificationView().findViewById(R.id.card_view);
373             cardView.setOnTouchListener(new HeadsUpNotificationOnTouchListener(cardView,
374                     shouldDismissOnSwipe(alertEntry), () -> resetView(alertEntry)));
375 
376             // Add dismiss button listener
377             View dismissButton = currentNotification.getNotificationView().findViewById(
378                     R.id.dismiss_button);
379             if (dismissButton != null) {
380                 dismissButton.setOnClickListener(v -> dismissHun(alertEntry));
381             }
382         }
383     }
384 
resetViewTreeListenersEntry(HeadsUpEntry headsUpEntry)385     private void resetViewTreeListenersEntry(HeadsUpEntry headsUpEntry) {
386         Pair<OnComputeInternalInsetsListener, OnGlobalFocusChangeListener> listeners =
387                 mRegisteredViewTreeListeners.get(headsUpEntry);
388         if (listeners == null) {
389             return;
390         }
391 
392         ViewTreeObserver observer = headsUpEntry.getNotificationView().getViewTreeObserver();
393         observer.removeOnComputeInternalInsetsListener(listeners.first);
394         observer.removeOnGlobalFocusChangeListener(listeners.second);
395         mRegisteredViewTreeListeners.remove(headsUpEntry);
396     }
397 
setInternalInsetsInfo(InternalInsetsInfo info, HeadsUpEntry currentNotification, boolean panelExpanded)398     protected void setInternalInsetsInfo(InternalInsetsInfo info,
399             HeadsUpEntry currentNotification, boolean panelExpanded) {
400         // If the panel is not on screen don't modify the touch region
401         if (!mHunContainer.isVisible()) return;
402         int[] mTmpTwoArray = new int[2];
403         View cardView = currentNotification.getNotificationView().findViewById(
404                 R.id.card_view);
405 
406         if (cardView == null) return;
407 
408         if (panelExpanded) {
409             info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
410             return;
411         }
412 
413         cardView.getLocationInWindow(mTmpTwoArray);
414         int minX = mTmpTwoArray[0];
415         int maxX = mTmpTwoArray[0] + cardView.getWidth();
416         int height = cardView.getHeight();
417         info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
418         info.touchableRegion.set(minX, mNotificationHeadsUpCardMarginTop, maxX,
419                 height + mNotificationHeadsUpCardMarginTop);
420     }
421 
playSound(AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)422     private void playSound(AlertEntry alertEntry,
423             NotificationListenerService.RankingMap rankingMap) {
424         NotificationListenerService.Ranking ranking = getRanking();
425         if (rankingMap.getRanking(alertEntry.getKey(), ranking)) {
426             NotificationChannel notificationChannel = ranking.getChannel();
427             // If sound is not set on the notification channel and default is not chosen it
428             // can be null.
429             if (notificationChannel.getSound() != null) {
430                 // make the sound
431                 mBeeper.beep(alertEntry.getStatusBarNotification().getPackageName(),
432                         notificationChannel.getSound());
433             }
434         }
435     }
436 
shouldDismissOnSwipe(AlertEntry alertEntry)437     private boolean shouldDismissOnSwipe(AlertEntry alertEntry) {
438         return !(hasFullScreenIntent(alertEntry)
439                 && Objects.equals(alertEntry.getNotification().category, Notification.CATEGORY_CALL)
440                 && alertEntry.getStatusBarNotification().isOngoing());
441     }
442 
443     @VisibleForTesting
getActiveHeadsUpNotifications()444     protected Map<String, HeadsUpEntry> getActiveHeadsUpNotifications() {
445         return mActiveHeadsUpNotifications;
446     }
447 
setAutoDismissViews(HeadsUpEntry currentNotification, AlertEntry alertEntry)448     private void setAutoDismissViews(HeadsUpEntry currentNotification, AlertEntry alertEntry) {
449         // Should not auto dismiss if HUN has a full screen Intent.
450         if (hasFullScreenIntent(alertEntry)) {
451             return;
452         }
453         currentNotification.getHandler().removeCallbacksAndMessages(null);
454         currentNotification.getHandler().postDelayed(() -> dismissHun(alertEntry), mDuration);
455     }
456 
457     /**
458      * Returns true if AlertEntry has a full screen Intent.
459      */
hasFullScreenIntent(AlertEntry alertEntry)460     private boolean hasFullScreenIntent(AlertEntry alertEntry) {
461         return alertEntry.getNotification().fullScreenIntent != null;
462     }
463 
464     /**
465      * Animates the heads up notification out of the screen and reset the views.
466      */
animateOutHun(AlertEntry alertEntry, boolean isRemoved)467     private void animateOutHun(AlertEntry alertEntry, boolean isRemoved) {
468         Log.d(TAG, "clearViews for Heads Up Notification: ");
469         // get the current notification to perform animations and remove it immediately from the
470         // active notification maps and cancel all other call backs if any.
471         HeadsUpEntry currentHeadsUpNotification = mActiveHeadsUpNotifications.get(
472                 alertEntry.getKey());
473         // view can also be removed when swipped away.
474         if (currentHeadsUpNotification == null) {
475             return;
476         }
477         currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null);
478         resetViewTreeListenersEntry(currentHeadsUpNotification);
479         View view = currentHeadsUpNotification.getNotificationView();
480 
481         AnimatorSet animatorSet = mAnimationHelper.getAnimateOutAnimator(mContext, view);
482         animatorSet.setTarget(view);
483         animatorSet.addListener(new AnimatorListenerAdapter() {
484             @Override
485             public void onAnimationEnd(Animator animation) {
486                 mHunContainer.removeNotification(view);
487 
488                 // Remove HUN after the animation ends to prevent accidental touch on the card
489                 // triggering another remove call.
490                 mActiveHeadsUpNotifications.remove(alertEntry.getKey());
491 
492                 // If the HUN was not specifically removed then add it to the panel.
493                 if (!isRemoved) {
494                     handleHeadsUpNotificationStateChanged(alertEntry, /* isHeadsUp= */ false);
495                 }
496             }
497         });
498         animatorSet.start();
499     }
500 
dismissHun(AlertEntry alertEntry)501     private void dismissHun(AlertEntry alertEntry) {
502         animateOutHun(alertEntry, /* isRemoved= */ false);
503     }
504 
removeHun(AlertEntry alertEntry)505     private void removeHun(AlertEntry alertEntry) {
506         animateOutHun(alertEntry, /* isRemoved= */ true);
507     }
508 
509     /**
510      * Removes the view for the active heads up notification and also removes the HUN from the map
511      * of active Notifications.
512      */
resetView(AlertEntry alertEntry)513     private void resetView(AlertEntry alertEntry) {
514         HeadsUpEntry currentHeadsUpNotification = mActiveHeadsUpNotifications.get(
515                 alertEntry.getKey());
516         if (currentHeadsUpNotification == null) return;
517 
518         currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null);
519         mHunContainer.removeNotification(currentHeadsUpNotification.getNotificationView());
520         mActiveHeadsUpNotifications.remove(alertEntry.getKey());
521         handleHeadsUpNotificationStateChanged(alertEntry, /* isHeadsUp= */ false);
522         resetViewTreeListenersEntry(currentHeadsUpNotification);
523     }
524 
525     /**
526      * Helper method that determines whether a notification should show as a heads-up.
527      *
528      * <p> A notification will never be shown as a heads-up if:
529      * <ul>
530      * <li> Keyguard (lock screen) is showing
531      * <li> OEMs configured CATEGORY_NAVIGATION should not be shown
532      * <li> Notification is muted.
533      * </ul>
534      *
535      * <p> A notification will be shown as a heads-up if:
536      * <ul>
537      * <li> Importance >= HIGH
538      * <li> it comes from an app signed with the platform key.
539      * <li> it comes from a privileged system app.
540      * <li> is a car compatible notification.
541      * {@link com.android.car.assist.client.CarAssistUtils#isCarCompatibleMessagingNotification}
542      * <li> Notification category is one of CATEGORY_CALL or CATEGORY_NAVIGATION
543      * </ul>
544      *
545      * <p> Group alert behavior still follows API documentation.
546      *
547      * @return true if a notification should be shown as a heads-up
548      */
shouldShowHeadsUp( AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)549     private boolean shouldShowHeadsUp(
550             AlertEntry alertEntry,
551             NotificationListenerService.RankingMap rankingMap) {
552         if (mKeyguardManager.isKeyguardLocked()) {
553             return false;
554         }
555         Notification notification = alertEntry.getNotification();
556 
557         // Navigation notification configured by OEM
558         if (!mEnableNavigationHeadsup && Notification.CATEGORY_NAVIGATION.equals(
559                 notification.category)) {
560             return false;
561         }
562         // Group alert behavior
563         if (notification.suppressAlertingDueToGrouping()) {
564             return false;
565         }
566         // Messaging notification muted by user.
567         if (mNotificationDataManager.isMessageNotificationMuted(alertEntry)) {
568             return false;
569         }
570 
571         // Do not show if importance < HIGH
572         NotificationListenerService.Ranking ranking = getRanking();
573         if (rankingMap.getRanking(alertEntry.getKey(), ranking)) {
574             if (ranking.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
575                 return false;
576             }
577         }
578 
579         if (NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, alertEntry)) {
580             return true;
581         }
582 
583         // Allow car messaging type.
584         if (isCarCompatibleMessagingNotification(alertEntry.getStatusBarNotification())) {
585             return true;
586         }
587 
588         if (notification.category == null) {
589             Log.d(TAG, "category not set for: "
590                     + alertEntry.getStatusBarNotification().getPackageName());
591         }
592 
593         // Allow for Call, and nav TBT categories.
594         return Notification.CATEGORY_CALL.equals(notification.category)
595                 || Notification.CATEGORY_NAVIGATION.equals(notification.category);
596     }
597 
598     @VisibleForTesting
getRanking()599     protected NotificationListenerService.Ranking getRanking() {
600         return new NotificationListenerService.Ranking();
601     }
602 
603     @Override
onUxRestrictionsChanged(CarUxRestrictions restrictions)604     public void onUxRestrictionsChanged(CarUxRestrictions restrictions) {
605         mShouldRestrictMessagePreview =
606                 (restrictions.getActiveRestrictions()
607                         & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0;
608     }
609 
610     /**
611      * Sets the source of {@link View.OnClickListener}
612      *
613      * @param clickHandlerFactory used to generate onClickListeners
614      */
615     @VisibleForTesting
setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)616     public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) {
617         mClickHandlerFactory = clickHandlerFactory;
618     }
619 }
620