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