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.ActivityTaskManager;
29 import android.app.KeyguardManager;
30 import android.app.Notification;
31 import android.app.NotificationChannel;
32 import android.app.NotificationManager;
33 import android.car.drivingstate.CarUxRestrictions;
34 import android.car.drivingstate.CarUxRestrictionsManager;
35 import android.content.Context;
36 import android.os.Build;
37 import android.service.notification.NotificationListenerService;
38 import android.util.Log;
39 import android.util.Pair;
40 import android.view.LayoutInflater;
41 import android.view.View;
42 import android.view.ViewTreeObserver;
43 
44 import androidx.annotation.NonNull;
45 import androidx.annotation.Nullable;
46 import androidx.annotation.UiThread;
47 import androidx.annotation.VisibleForTesting;
48 
49 import com.android.car.notification.headsup.CarHeadsUpNotificationContainer;
50 import com.android.car.notification.headsup.animationhelper.HeadsUpNotificationAnimationHelper;
51 import com.android.car.notification.template.MessageNotificationViewHolder;
52 
53 import java.time.Clock;
54 import java.util.ArrayList;
55 import java.util.HashMap;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Objects;
59 import java.util.Set;
60 import java.util.concurrent.ConcurrentHashMap;
61 import java.util.concurrent.ScheduledThreadPoolExecutor;
62 
63 /**
64  * Notification Manager for heads-up notifications in car.
65  */
66 public class CarHeadsUpNotificationManager
67         implements CarUxRestrictionsManager.OnUxRestrictionsChangedListener {
68 
69     /**
70      * Callback that will be issued after a Heads up notification state is changed.
71      */
72     public interface OnHeadsUpNotificationStateChange {
73         /**
74          * Will be called if a new notification added/updated changes the heads up state for that
75          * notification.
76          */
onStateChange(AlertEntry alertEntry, HeadsUpState headsUpState)77         void onStateChange(AlertEntry alertEntry, HeadsUpState headsUpState);
78     }
79 
80     /**
81      * Captures HUN State with following values:
82      * <ul>
83      * {@link HeadsUpState.SHOWN}: shown on screen
84      * {@link HeadsUpState.DISMISSED}: HUN dismissed by user/timeout/system and no longer displayed
85      * {@link HeadsUpState.REMOVED_FROM_QUEUE}: removed from {@link CarHeadsUpNotificationQueue}
86      * without displaying
87      * {@link HeadsUpState.REMOVED_BY_SENDER}: HUN dismissed because it was removed by the sender
88      * app
89      * <ul/>
90      */
91     public enum HeadsUpState {
92         SHOWN,
93         DISMISSED,
94         REMOVED_FROM_QUEUE,
95         REMOVED_BY_SENDER
96     }
97 
98     private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
99     private static final String TAG = CarHeadsUpNotificationManager.class.getSimpleName();
100 
101     private final Beeper mBeeper;
102     private final Context mContext;
103     private final boolean mEnableNavigationHeadsup;
104     private final long mDuration;
105     private final long mMinDisplayDuration;
106     private HeadsUpNotificationAnimationHelper mAnimationHelper;
107     private final int mNotificationHeadsUpCardMarginTop;
108     private final boolean mIsSuppressAndThrottleHeadsUp;
109 
110     private final KeyguardManager mKeyguardManager;
111     private final PreprocessingManager mPreprocessingManager;
112     private final LayoutInflater mInflater;
113     @VisibleForTesting
114     final CarHeadsUpNotificationContainer mHunContainer;
115     private final CarHeadsUpNotificationQueue.CarHeadsUpNotificationQueueCallback
116             mCarHeadsUpNotificationQueueCallback;
117 
118     /**
119      * Mapping key({@link AlertEntry#getKey}) to active HUNs({@link HeadsUpEntry}).
120      */
121     private final Map<String, HeadsUpEntry> mActiveHeadsUpNotifications = new ConcurrentHashMap<>();
122 
123     /**
124      * Set of key({@link AlertEntry#getKey}) of HUNs that are currently in the process of being
125      * dismissed.
126      */
127     private final Set<String> mDismissingHeadsUpNotifications = ConcurrentHashMap.newKeySet();
128 
129     /**
130      * Set of key({@link AlertEntry#getKey}) of HUNs that are removed by the sender.
131      */
132     private final Set<String> mHeadsUpNotificationsToBeRemoved = ConcurrentHashMap.newKeySet();
133     private final List<OnHeadsUpNotificationStateChange> mNotificationStateChangeListeners =
134             new ArrayList<>();
135     private final Map<HeadsUpEntry,
136             Pair<OnComputeInternalInsetsListener, OnGlobalFocusChangeListener>>
137             mRegisteredViewTreeListeners = new HashMap<>();
138 
139     private boolean mShouldRestrictMessagePreview;
140     private NotificationClickHandlerFactory mClickHandlerFactory;
141     private NotificationDataManager mNotificationDataManager;
142     private CarHeadsUpNotificationQueue mCarHeadsUpNotificationQueue;
143     private Clock mClock;
144 
CarHeadsUpNotificationManager(Context context, NotificationClickHandlerFactory clickHandlerFactory, CarHeadsUpNotificationContainer hunContainer)145     public CarHeadsUpNotificationManager(Context context,
146             NotificationClickHandlerFactory clickHandlerFactory,
147             CarHeadsUpNotificationContainer hunContainer) {
148         mContext = context.getApplicationContext();
149         mEnableNavigationHeadsup =
150                 context.getResources().getBoolean(R.bool.config_showNavigationHeadsup);
151         mClickHandlerFactory = clickHandlerFactory;
152         mNotificationDataManager = NotificationDataManager.getInstance();
153         mBeeper = new Beeper(mContext);
154         mDuration = mContext.getResources().getInteger(R.integer.headsup_notification_duration_ms);
155         mNotificationHeadsUpCardMarginTop = (int) mContext.getResources().getDimension(
156                 R.dimen.headsup_notification_top_margin);
157         mMinDisplayDuration = mContext.getResources().getInteger(
158                 R.integer.heads_up_notification_minimum_time);
159         mAnimationHelper = getAnimationHelper();
160 
161         mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
162         mPreprocessingManager = PreprocessingManager.getInstance(context);
163         mInflater = LayoutInflater.from(mContext);
164         mClickHandlerFactory.registerClickListener((launchResult, alertEntry) -> {
165             if (isActiveHun(alertEntry)) {
166                 dismissHun(alertEntry, /* shouldAnimate= */ true);
167             }
168         });
169         mClickHandlerFactory.setHunDismissCallback(
170                 (launchResult, alertEntry) -> dismissHun(alertEntry, /* shouldAnimate= */ true));
171         mHunContainer = hunContainer;
172         mIsSuppressAndThrottleHeadsUp = context.getResources().getBoolean(
173                 R.bool.config_suppressAndThrottleHeadsUp);
174         mClock = Clock.systemUTC();
175         mCarHeadsUpNotificationQueueCallback =
176                 new CarHeadsUpNotificationQueue.CarHeadsUpNotificationQueueCallback() {
177                     @Override
178                     public void showAsHeadsUp(AlertEntry alertEntry,
179                             NotificationListenerService.RankingMap rankingMap) {
180                         mContext.getMainExecutor().execute(() -> showHeadsUp(
181                                 mPreprocessingManager.optimizeForDriving(alertEntry),
182                                 rankingMap)
183                         );
184                     }
185 
186                     @Override
187                     public void removedFromHeadsUpQueue(AlertEntry alertEntry) {
188                         handleHeadsUpNotificationStateChanged(alertEntry,
189                                 HeadsUpState.REMOVED_FROM_QUEUE);
190                     }
191 
192                     @Override
193                     public void dismissHeadsUp(@Nullable AlertEntry alertEntry) {
194                         if (alertEntry != null) {
195                             dismissHun(alertEntry, /* shouldAnimate= */ true);
196                         }
197                     }
198 
199                     @Override
200                     public List<AlertEntry> getActiveHeadsUpNotifications() {
201                         return new ArrayList<>(mActiveHeadsUpNotifications.values());
202                     }
203                 };
204         mCarHeadsUpNotificationQueue = new CarHeadsUpNotificationQueue(context,
205                 ActivityTaskManager.getInstance(),
206                 mContext.getSystemService(NotificationManager.class),
207                 new ScheduledThreadPoolExecutor(/* corePoolSize= */ 1),
208                 mCarHeadsUpNotificationQueueCallback
209         );
210         registerHeadsUpNotificationStateChangeListener(mCarHeadsUpNotificationQueue);
211     }
212 
213     @VisibleForTesting
setNotificationDataManager(NotificationDataManager notificationDataManager)214     void setNotificationDataManager(NotificationDataManager notificationDataManager) {
215         mNotificationDataManager = notificationDataManager;
216     }
217 
getAnimationHelper()218     private HeadsUpNotificationAnimationHelper getAnimationHelper() {
219         String helperName = mContext.getResources().getString(
220                 R.string.config_headsUpNotificationAnimationHelper);
221         try {
222             Class<?> clazz = Class.forName(helperName);
223             return (HeadsUpNotificationAnimationHelper) clazz.getConstructor().newInstance();
224         } catch (Exception e) {
225             throw new IllegalArgumentException(
226                     String.format("Invalid animation helper: %s", helperName), e);
227         }
228     }
229 
230     /**
231      * Show the notification as a heads-up if it meets the criteria.
232      *
233      * <p>Return's true if the notification will be shown as a heads up, false otherwise.
234      */
maybeShowHeadsUp( AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap, Map<String, AlertEntry> activeNotifications)235     public boolean maybeShowHeadsUp(
236             AlertEntry alertEntry,
237             NotificationListenerService.RankingMap rankingMap,
238             Map<String, AlertEntry> activeNotifications) {
239         if (!shouldShowHeadsUp(alertEntry, rankingMap)) {
240             if (!isActiveHun(alertEntry)) {
241                 if (DEBUG) {
242                     Log.d(TAG, alertEntry + " is not an active heads up notification");
243                 }
244                 return false;
245             }
246             // Check if this is an update to the existing notification and if it should still show
247             // as a heads up or not.
248             HeadsUpEntry currentActiveHeadsUpNotification = getActiveHeadsUpEntry(alertEntry);
249             if (CarNotificationDiff.sameNotificationKey(currentActiveHeadsUpNotification,
250                     alertEntry)
251                     && currentActiveHeadsUpNotification.getHandler().hasMessagesOrCallbacks()) {
252                 dismissHun(alertEntry, /* shouldAnimate= */ true);
253             }
254             return false;
255         }
256         boolean containsKeyFlag = !activeNotifications.containsKey(alertEntry.getKey());
257         boolean canUpdateFlag = canUpdate(alertEntry);
258         boolean alertAgainFlag = alertAgain(alertEntry.getNotification());
259         if (DEBUG) {
260             Log.d(TAG, alertEntry + " is an active notification: " + containsKeyFlag);
261             Log.d(TAG, alertEntry + " is an updatable notification: " + canUpdateFlag);
262             Log.d(TAG, alertEntry + " is not an alert once notification: " + alertAgainFlag);
263         }
264         if (canUpdateFlag) {
265             showHeadsUp(mPreprocessingManager.optimizeForDriving(alertEntry),
266                     rankingMap);
267             return true;
268         } else if (containsKeyFlag || alertAgainFlag) {
269             if (!mIsSuppressAndThrottleHeadsUp) {
270                 showHeadsUp(mPreprocessingManager.optimizeForDriving(alertEntry),
271                         rankingMap);
272             } else {
273                 mCarHeadsUpNotificationQueue.addToQueue(alertEntry, rankingMap);
274             }
275             return true;
276         }
277         return false;
278     }
279 
280     /**
281      * This method gets called when an app wants to cancel or withdraw its notification.
282      */
maybeRemoveHeadsUp(AlertEntry alertEntry)283     public void maybeRemoveHeadsUp(AlertEntry alertEntry) {
284         if (mCarHeadsUpNotificationQueue.removeFromQueue(alertEntry)) {
285             return;
286         }
287 
288         if (!isActiveHun(alertEntry)) {
289             // If the heads up notification is already removed do nothing.
290             return;
291         }
292         tagCurrentActiveHunToBeRemoved(alertEntry);
293 
294         scheduleRemoveHeadsUp(alertEntry);
295     }
296 
297     /**
298      * Release the notifications stored in the queue.
299      */
releaseQueue()300     public void releaseQueue() {
301         mCarHeadsUpNotificationQueue.releaseQueue();
302     }
303 
304     /**
305      * Clears all local cached variables and gracefully removes any heads up notification views if
306      * present.
307      */
clearCache()308     public void clearCache() {
309         mCarHeadsUpNotificationQueue.clearCache();
310         for (AlertEntry alertEntry : mActiveHeadsUpNotifications.values()) {
311             dismissHun(alertEntry, /* shouldAnimate= */ false);
312         }
313     }
314 
scheduleRemoveHeadsUp(AlertEntry alertEntry)315     private void scheduleRemoveHeadsUp(AlertEntry alertEntry) {
316         HeadsUpEntry currentActiveHeadsUpNotification = getActiveHeadsUpEntry(alertEntry);
317 
318         long totalDisplayDuration =
319                 mClock.millis() - currentActiveHeadsUpNotification.getPostTime();
320         // ongoing notification that has passed the minimum threshold display time.
321         if (totalDisplayDuration >= mMinDisplayDuration) {
322             dismissHun(alertEntry, /* shouldAnimate= */ true);
323             return;
324         }
325 
326         long earliestRemovalTime = mMinDisplayDuration - totalDisplayDuration;
327 
328         currentActiveHeadsUpNotification.getHandler().postDelayed(
329                 () -> dismissHun(alertEntry, /* shouldAnimate= */ true), earliestRemovalTime);
330     }
331 
332     /**
333      * Registers a new {@link OnHeadsUpNotificationStateChange} to the list of listeners.
334      */
registerHeadsUpNotificationStateChangeListener( OnHeadsUpNotificationStateChange listener)335     public void registerHeadsUpNotificationStateChangeListener(
336             OnHeadsUpNotificationStateChange listener) {
337         if (!mNotificationStateChangeListeners.contains(listener)) {
338             mNotificationStateChangeListeners.add(listener);
339         }
340     }
341 
342     /**
343      * Unregisters all {@link OnHeadsUpNotificationStateChange} listeners along with other listeners
344      * registered by {@link CarHeadsUpNotificationManager}.
345      */
unregisterListeners()346     public void unregisterListeners() {
347         mNotificationStateChangeListeners.clear();
348         mCarHeadsUpNotificationQueue.unregisterListeners();
349     }
350 
351     /**
352      * Invokes all OnHeadsUpNotificationStateChange handlers registered in {@link
353      * OnHeadsUpNotificationStateChange}s array.
354      */
handleHeadsUpNotificationStateChanged(AlertEntry alertEntry, HeadsUpState headsUpState)355     private void handleHeadsUpNotificationStateChanged(AlertEntry alertEntry,
356             HeadsUpState headsUpState) {
357         mNotificationStateChangeListeners.forEach(
358                 listener -> listener.onStateChange(alertEntry, headsUpState));
359     }
360 
361     /**
362      * Returns true if the notification's flag is not set to
363      * {@link Notification#FLAG_ONLY_ALERT_ONCE}
364      */
alertAgain(Notification newNotification)365     private boolean alertAgain(Notification newNotification) {
366         return (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0;
367     }
368 
369     /**
370      * Return true if the currently displaying notification have the same key as the new added
371      * notification. In that case it will be considered as an update to the currently displayed
372      * notification.
373      */
isUpdate(AlertEntry alertEntry)374     private boolean isUpdate(AlertEntry alertEntry) {
375         return isActiveHun(alertEntry) && CarNotificationDiff.sameNotificationKey(
376                 getActiveHeadsUpEntry(alertEntry), alertEntry);
377     }
378 
379     /**
380      * Updates only when the notification is being displayed.
381      */
canUpdate(AlertEntry alertEntry)382     private boolean canUpdate(AlertEntry alertEntry) {
383         return isActiveHun(alertEntry) && (System.currentTimeMillis()
384                 - getActiveHeadsUpEntry(alertEntry).getPostTime()) < mDuration;
385     }
386 
387     /**
388      * Returns the active headsUpEntry or creates a new one while adding it to the list of
389      * mActiveHeadsUpNotifications.
390      */
addNewHeadsUpEntry(AlertEntry alertEntry)391     private HeadsUpEntry addNewHeadsUpEntry(AlertEntry alertEntry) {
392         if (!isActiveHun(alertEntry)) {
393             HeadsUpEntry newActiveHeadsUpNotification = new HeadsUpEntry(
394                     alertEntry.getStatusBarNotification());
395             handleHeadsUpNotificationStateChanged(alertEntry, HeadsUpState.SHOWN);
396             mActiveHeadsUpNotifications.put(alertEntry.getKey(),
397                     newActiveHeadsUpNotification);
398             newActiveHeadsUpNotification.mIsAlertAgain = alertAgain(
399                     alertEntry.getNotification());
400             newActiveHeadsUpNotification.mIsNewHeadsUp = true;
401             return newActiveHeadsUpNotification;
402         }
403         HeadsUpEntry currentActiveHeadsUpNotification = getActiveHeadsUpEntry(alertEntry);
404         currentActiveHeadsUpNotification.mIsNewHeadsUp = false;
405         currentActiveHeadsUpNotification.mIsAlertAgain = alertAgain(
406                 alertEntry.getNotification());
407         if (currentActiveHeadsUpNotification.mIsAlertAgain) {
408             // This is a ongoing notification which needs to be alerted again to the user. This
409             // requires for the post time to be updated.
410             currentActiveHeadsUpNotification.updatePostTime();
411         }
412         return currentActiveHeadsUpNotification;
413     }
414 
415     /**
416      * Controls three major conditions while showing heads up notification.
417      * <p>
418      * <ol>
419      * <li> When a new HUN comes in it will be displayed with animations
420      * <li> If an update to existing HUN comes in which enforces to alert the HUN again to user,
421      * then the post time will be updated to current time. This will only be done if {@link
422      * Notification#FLAG_ONLY_ALERT_ONCE} flag is not set.
423      * <li> If an update to existing HUN comes in which just updates the data and does not want to
424      * alert itself again, then the animations will not be shown and the data will get updated. This
425      * will only be done if {@link Notification#FLAG_ONLY_ALERT_ONCE} flag is not set.
426      * </ol>
427      */
428     @UiThread
showHeadsUp(AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)429     private void showHeadsUp(AlertEntry alertEntry,
430             NotificationListenerService.RankingMap rankingMap) {
431         // Show animations only when there is no active HUN and notification is new. This check
432         // needs to be done here because after this the new notification will be added to the map
433         // holding ongoing notifications.
434         boolean shouldShowAnimation = !isUpdate(alertEntry);
435         HeadsUpEntry currentNotification = addNewHeadsUpEntry(alertEntry);
436         if (currentNotification.mIsNewHeadsUp) {
437             playSound(alertEntry, rankingMap);
438             setAutoDismissViews(currentNotification, alertEntry);
439         } else if (currentNotification.mIsAlertAgain) {
440             setAutoDismissViews(currentNotification, alertEntry);
441         }
442         CarNotificationTypeItem notificationTypeItem = NotificationUtils.getNotificationViewType(
443                 alertEntry);
444         currentNotification.setClickHandlerFactory(mClickHandlerFactory);
445 
446         if (currentNotification.getNotificationView() == null) {
447             currentNotification.setNotificationView(mInflater.inflate(
448                     notificationTypeItem.getHeadsUpTemplate(),
449                     null));
450             mHunContainer.displayNotification(currentNotification.getNotificationView(),
451                     notificationTypeItem);
452             currentNotification.setViewHolder(
453                     notificationTypeItem.getViewHolder(currentNotification.getNotificationView(),
454                             mClickHandlerFactory));
455         }
456 
457         currentNotification.getViewHolder().setHideDismissButton(!isHeadsUpDismissible(alertEntry));
458 
459         if (mShouldRestrictMessagePreview && notificationTypeItem.getNotificationType()
460                 == NotificationViewType.MESSAGE) {
461             ((MessageNotificationViewHolder) currentNotification.getViewHolder()).bindRestricted(
462                     alertEntry, /* isInGroup= */ false, /* isHeadsUp= */ true, /* isSeen= */ false);
463         } else {
464             currentNotification.getViewHolder().bind(alertEntry, /* isInGroup= */false,
465                     /* isHeadsUp= */ true, /* isSeen= */ false);
466         }
467 
468         resetViewTreeListenersEntry(currentNotification);
469 
470         ViewTreeObserver viewTreeObserver =
471                 currentNotification.getNotificationView().getViewTreeObserver();
472 
473         // measure the size of the card and make that area of the screen touchable
474         OnComputeInternalInsetsListener onComputeInternalInsetsListener =
475                 info -> setInternalInsetsInfo(info, currentNotification,
476                         /* panelExpanded= */ false);
477         viewTreeObserver.addOnComputeInternalInsetsListener(onComputeInternalInsetsListener);
478         // Get the height of the notification view after onLayout() in order to animate the
479         // notification into the screen.
480         viewTreeObserver.addOnGlobalLayoutListener(
481                 new OnGlobalLayoutListener() {
482                     @Override
483                     public void onGlobalLayout() {
484                         View view = currentNotification.getNotificationView();
485                         if (shouldShowAnimation) {
486                             mAnimationHelper.resetHUNPosition(view);
487                             AnimatorSet animatorSet = mAnimationHelper.getAnimateInAnimator(
488                                     mContext, view);
489                             animatorSet.setTarget(view);
490                             animatorSet.start();
491                         }
492                         view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
493                     }
494                 });
495         // Reset the auto dismiss timeout for each rotary event.
496         OnGlobalFocusChangeListener onGlobalFocusChangeListener =
497                 (oldFocus, newFocus) -> setAutoDismissViews(currentNotification, alertEntry);
498         viewTreeObserver.addOnGlobalFocusChangeListener(onGlobalFocusChangeListener);
499 
500         mRegisteredViewTreeListeners.put(currentNotification,
501                 new Pair<>(onComputeInternalInsetsListener, onGlobalFocusChangeListener));
502 
503         attachHunViewListeners(currentNotification.getNotificationView(), alertEntry);
504     }
505 
attachHunViewListeners(View notificationView, AlertEntry alertEntry)506     private void attachHunViewListeners(View notificationView, AlertEntry alertEntry) {
507         // Add swipe gesture
508         View cardView = notificationView.findViewById(R.id.card_view);
509         cardView.setOnTouchListener(new HeadsUpNotificationOnTouchListener(cardView,
510                 isHeadsUpDismissible(alertEntry),
511                 () -> dismissHun(alertEntry, /* shouldAnimate= */ false)));
512 
513         // Add dismiss button listener
514         View dismissButton = notificationView.findViewById(
515                 R.id.dismiss_button);
516         if (dismissButton != null) {
517             dismissButton.setOnClickListener(v ->
518                     dismissHun(alertEntry, /* shouldAnimate= */ true));
519         }
520     }
521 
resetViewTreeListenersEntry(HeadsUpEntry headsUpEntry)522     private void resetViewTreeListenersEntry(HeadsUpEntry headsUpEntry) {
523         Pair<OnComputeInternalInsetsListener, OnGlobalFocusChangeListener> listeners =
524                 mRegisteredViewTreeListeners.get(headsUpEntry);
525         if (listeners == null) {
526             return;
527         }
528 
529         ViewTreeObserver observer = headsUpEntry.getNotificationView().getViewTreeObserver();
530         observer.removeOnComputeInternalInsetsListener(listeners.first);
531         observer.removeOnGlobalFocusChangeListener(listeners.second);
532         mRegisteredViewTreeListeners.remove(headsUpEntry);
533     }
534 
setInternalInsetsInfo(InternalInsetsInfo info, HeadsUpEntry currentNotification, boolean panelExpanded)535     protected void setInternalInsetsInfo(InternalInsetsInfo info,
536             HeadsUpEntry currentNotification, boolean panelExpanded) {
537         // If the panel is not on screen don't modify the touch region
538         if (!mHunContainer.isVisible()) return;
539         int[] mTmpTwoArray = new int[2];
540         View cardView = currentNotification.getNotificationView().findViewById(
541                 R.id.card_view);
542 
543         if (cardView == null) return;
544 
545         if (panelExpanded) {
546             info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
547             return;
548         }
549 
550         cardView.getLocationInWindow(mTmpTwoArray);
551         int minX = mTmpTwoArray[0];
552         int maxX = mTmpTwoArray[0] + cardView.getWidth();
553         int minY = mTmpTwoArray[1] + mNotificationHeadsUpCardMarginTop;
554         int maxY = mTmpTwoArray[1] + mNotificationHeadsUpCardMarginTop + cardView.getHeight();
555         info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
556         info.touchableRegion.set(minX, minY, maxX, maxY);
557     }
558 
playSound(AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)559     private void playSound(AlertEntry alertEntry,
560             NotificationListenerService.RankingMap rankingMap) {
561         NotificationListenerService.Ranking ranking = getRanking();
562         if (rankingMap.getRanking(alertEntry.getKey(), ranking)) {
563             NotificationChannel notificationChannel = ranking.getChannel();
564             // If sound is not set on the notification channel and default is not chosen it
565             // can be null.
566             if (notificationChannel.getSound() != null) {
567                 // make the sound
568                 mBeeper.beep(alertEntry.getStatusBarNotification().getPackageName(),
569                         notificationChannel.getSound());
570             }
571         }
572     }
573 
574     /**
575      * @return true if the {@code alertEntry} can be dismissed/swiped away.
576      */
isHeadsUpDismissible(@onNull AlertEntry alertEntry)577     public static boolean isHeadsUpDismissible(@NonNull AlertEntry alertEntry) {
578         return !(hasFullScreenIntent(alertEntry)
579                 && Objects.equals(alertEntry.getNotification().category, Notification.CATEGORY_CALL)
580                 && alertEntry.getStatusBarNotification().isOngoing());
581     }
582 
583     @VisibleForTesting
getActiveHeadsUpNotifications()584     protected Map<String, HeadsUpEntry> getActiveHeadsUpNotifications() {
585         return mActiveHeadsUpNotifications;
586     }
587 
setAutoDismissViews(HeadsUpEntry currentNotification, AlertEntry alertEntry)588     private void setAutoDismissViews(HeadsUpEntry currentNotification, AlertEntry alertEntry) {
589         // Should not auto dismiss if HUN has a full screen Intent.
590         if (hasFullScreenIntent(alertEntry)) {
591             return;
592         }
593         currentNotification.getHandler().removeCallbacksAndMessages(null);
594         currentNotification.getHandler().postDelayed(
595                 () -> dismissHun(alertEntry, /* shouldAnimate= */ true), mDuration);
596     }
597 
598     /**
599      * Returns true if AlertEntry has a full screen Intent.
600      */
hasFullScreenIntent(@onNull AlertEntry alertEntry)601     private static boolean hasFullScreenIntent(@NonNull AlertEntry alertEntry) {
602         return alertEntry.getNotification().fullScreenIntent != null;
603     }
604 
605     /**
606      * Dismisses the heads up notification and reset the views.
607      *
608      * @param shouldAnimate if set to true will animate the HUN out of the screen.
609      */
dismissHun(AlertEntry alertEntry, boolean shouldAnimate)610     private void dismissHun(AlertEntry alertEntry, boolean shouldAnimate) {
611         if (!isActiveHun(alertEntry)
612                 || mDismissingHeadsUpNotifications.contains(alertEntry.getKey())) {
613             if (DEBUG) {
614                 Log.d(TAG, "HUN not active, cannot dismiss, key: " + alertEntry.getKey());
615             }
616             return;
617         }
618         mDismissingHeadsUpNotifications.add(alertEntry.getKey());
619 
620         if (DEBUG) {
621             Log.d(TAG, "Dismissing HUN, key: " + alertEntry.getKey()
622                     + ", shouldAnimate: " + shouldAnimate);
623         }
624         resetHeadsUpEntry(alertEntry);
625         View headsUpView = getHeadsUpView(alertEntry);
626 
627         if (headsUpView == null) {
628             return;
629         }
630 
631         if (!shouldAnimate) {
632             postDismiss(alertEntry, headsUpView);
633             return;
634         }
635 
636 
637         AnimatorSet animatorSet = mAnimationHelper.getAnimateOutAnimator(mContext, headsUpView);
638         animatorSet.setTarget(headsUpView);
639         animatorSet.addListener(new AnimatorListenerAdapter() {
640             @Override
641             public void onAnimationEnd(Animator animation) {
642                 postDismiss(alertEntry, headsUpView);
643             }
644         });
645         animatorSet.start();
646 
647     }
648 
649     /**
650      * Method to be called after HUN is dismissed.
651      */
postDismiss(AlertEntry alertEntry, View headsUpView)652     private void postDismiss(AlertEntry alertEntry, View headsUpView) {
653         removeHeadsUpEntry(alertEntry, headsUpView);
654 
655         boolean isRemovedBySender =
656                 mHeadsUpNotificationsToBeRemoved.contains(alertEntry.getKey());
657         handleHeadsUpNotificationStateChanged(alertEntry,
658                 isRemovedBySender ? HeadsUpState.REMOVED_BY_SENDER
659                         : HeadsUpState.DISMISSED);
660 
661         mHeadsUpNotificationsToBeRemoved.remove(alertEntry.getKey());
662         mDismissingHeadsUpNotifications.remove(alertEntry.getKey());
663     }
664 
resetHeadsUpEntry(@onNull AlertEntry alertEntry)665     private void resetHeadsUpEntry(@NonNull AlertEntry alertEntry) {
666         if (!isActiveHun(alertEntry)) {
667             return;
668         }
669         HeadsUpEntry currentHeadsUpNotification = getActiveHeadsUpEntry(alertEntry);
670         currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(/* token= */ null);
671         resetViewTreeListenersEntry(currentHeadsUpNotification);
672     }
673 
674     @Nullable
getHeadsUpView(@onNull AlertEntry alertEntry)675     private View getHeadsUpView(@NonNull AlertEntry alertEntry) {
676         if (!isActiveHun(alertEntry)) {
677             return null;
678         }
679         return getActiveHeadsUpEntry(alertEntry).getNotificationView();
680     }
681 
removeHeadsUpEntry(@onNull AlertEntry alertEntry, @Nullable View view)682     private void removeHeadsUpEntry(@NonNull AlertEntry alertEntry, @Nullable View view) {
683         if (view != null) {
684             mHunContainer.removeNotification(view);
685         }
686         mActiveHeadsUpNotifications.remove(alertEntry.getKey());
687     }
688 
689     /**
690      * Helper method that determines whether a notification should show as a heads-up.
691      *
692      * <p> A notification will never be shown as a heads-up if:
693      * <ul>
694      * <li> Keyguard (lock screen) is showing
695      * <li> OEMs configured CATEGORY_NAVIGATION should not be shown
696      * <li> Notification is muted.
697      * </ul>
698      *
699      * <p> A notification will be shown as a heads-up if:
700      * <ul>
701      * <li> Importance >= HIGH
702      * <li> it comes from an app signed with the platform key.
703      * <li> it comes from a privileged system app.
704      * <li> is a car compatible notification.
705      * {@link com.android.car.assist.client.CarAssistUtils#isCarCompatibleMessagingNotification}
706      * <li> Notification category is one of CATEGORY_CALL or CATEGORY_NAVIGATION
707      * </ul>
708      *
709      * <p> Group alert behavior still follows API documentation.
710      *
711      * @return true if a notification should be shown as a heads-up
712      */
shouldShowHeadsUp( AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)713     private boolean shouldShowHeadsUp(
714             AlertEntry alertEntry,
715             NotificationListenerService.RankingMap rankingMap) {
716         if (mKeyguardManager.isKeyguardLocked()) {
717             if (DEBUG) {
718                 Log.d(TAG, "Unable to show as HUN: Keyguard is locked");
719             }
720             return false;
721         }
722         Notification notification = alertEntry.getNotification();
723 
724         // Navigation notification configured by OEM
725         if (!mEnableNavigationHeadsup && Notification.CATEGORY_NAVIGATION.equals(
726                 notification.category)) {
727             if (DEBUG) {
728                 Log.d(TAG, "Unable to show as HUN: OEM has disabled navigation HUN");
729             }
730             return false;
731         }
732         // Group alert behavior
733         if (notification.suppressAlertingDueToGrouping()) {
734             if (DEBUG) {
735                 Log.d(TAG, "Unable to show as HUN: Grouping notification");
736             }
737             return false;
738         }
739         // Messaging notification muted by user.
740         if (mNotificationDataManager.isMessageNotificationMuted(alertEntry)) {
741             if (DEBUG) {
742                 Log.d(TAG, "Unable to show as HUN: Messaging notification is muted by user");
743             }
744             return false;
745         }
746 
747         // Do not show if importance < HIGH
748         NotificationListenerService.Ranking ranking = getRanking();
749         if (rankingMap.getRanking(alertEntry.getKey(), ranking)) {
750             if (ranking.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
751                 if (DEBUG) {
752                     Log.d(TAG, "Unable to show as HUN: importance is not sufficient");
753                 }
754                 return false;
755             }
756         }
757 
758         if (NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, alertEntry)) {
759             if (DEBUG) {
760                 Log.d(TAG, "Show as HUN: application is system privileged or signed with "
761                         + "platform key");
762             }
763             return true;
764         }
765 
766         // Allow car messaging type.
767         if (isCarCompatibleMessagingNotification(alertEntry.getStatusBarNotification())) {
768             if (DEBUG) {
769                 Log.d(TAG, "Show as HUN: car messaging type notification");
770             }
771             return true;
772         }
773 
774         if (notification.category == null) {
775             Log.d(TAG, "category not set for: "
776                     + alertEntry.getStatusBarNotification().getPackageName());
777         }
778 
779         if (DEBUG) {
780             Log.d(TAG, "Notification category: " + notification.category);
781         }
782 
783         // Allow for Call, and nav TBT categories.
784         return Notification.CATEGORY_CALL.equals(notification.category)
785                 || Notification.CATEGORY_NAVIGATION.equals(notification.category);
786     }
787 
isActiveHun(AlertEntry alertEntry)788     private boolean isActiveHun(AlertEntry alertEntry) {
789         return mActiveHeadsUpNotifications.containsKey(alertEntry.getKey());
790     }
791 
getActiveHeadsUpEntry(AlertEntry alertEntry)792     private HeadsUpEntry getActiveHeadsUpEntry(AlertEntry alertEntry) {
793         return mActiveHeadsUpNotifications.get(alertEntry.getKey());
794     }
795 
796     /**
797      * We tag HUN that was removed by the app and hence not to be shown in the notification panel
798      * against the normal behaviour (on dismiss add to notification panel).
799      */
tagCurrentActiveHunToBeRemoved(AlertEntry alertEntry)800     private void tagCurrentActiveHunToBeRemoved(AlertEntry alertEntry) {
801         mHeadsUpNotificationsToBeRemoved.add(alertEntry.getKey());
802     }
803 
804     @VisibleForTesting
getRanking()805     protected NotificationListenerService.Ranking getRanking() {
806         return new NotificationListenerService.Ranking();
807     }
808 
809     @Override
onUxRestrictionsChanged(CarUxRestrictions restrictions)810     public void onUxRestrictionsChanged(CarUxRestrictions restrictions) {
811         mCarHeadsUpNotificationQueue.setActiveUxRestriction(
812                 restrictions.isRequiresDistractionOptimization());
813         mShouldRestrictMessagePreview =
814                 (restrictions.getActiveRestrictions()
815                         & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0;
816     }
817 
818     /**
819      * Sets the source of {@link View.OnClickListener}
820      *
821      * @param clickHandlerFactory used to generate onClickListeners
822      */
823     @VisibleForTesting
setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)824     public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) {
825         mClickHandlerFactory = clickHandlerFactory;
826     }
827 
828     @VisibleForTesting
829     CarHeadsUpNotificationQueue.CarHeadsUpNotificationQueueCallback
getCarHeadsUpNotificationQueueCallback()830             getCarHeadsUpNotificationQueueCallback() {
831         return mCarHeadsUpNotificationQueueCallback;
832     }
833 
834     @VisibleForTesting
setCarHeadsUpNotificationQueue(CarHeadsUpNotificationQueue carHeadsUpNotificationQueue)835     void setCarHeadsUpNotificationQueue(CarHeadsUpNotificationQueue carHeadsUpNotificationQueue) {
836         mCarHeadsUpNotificationQueue = carHeadsUpNotificationQueue;
837     }
838 
839     @VisibleForTesting
addActiveHeadsUpNotification(HeadsUpEntry headsUpEntry)840     void addActiveHeadsUpNotification(HeadsUpEntry headsUpEntry) {
841         mActiveHeadsUpNotifications.put(headsUpEntry.getKey(), headsUpEntry);
842     }
843 }
844