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