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