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 com.android.car.assist.client.CarAssistUtils.isCarCompatibleMessagingNotification;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.AnimatorSet;
23 import android.animation.ObjectAnimator;
24 import android.app.KeyguardManager;
25 import android.app.Notification;
26 import android.app.NotificationChannel;
27 import android.app.NotificationManager;
28 import android.car.drivingstate.CarUxRestrictions;
29 import android.car.drivingstate.CarUxRestrictionsManager;
30 import android.car.userlib.CarUserManagerHelper;
31 import android.content.Context;
32 import android.graphics.PixelFormat;
33 import android.os.Bundle;
34 import android.service.notification.NotificationListenerService;
35 import android.service.notification.StatusBarNotification;
36 import android.util.Log;
37 import android.view.Gravity;
38 import android.view.LayoutInflater;
39 import android.view.View;
40 import android.view.ViewTreeObserver;
41 import android.view.WindowManager;
42 import android.view.animation.AnimationUtils;
43 import android.view.animation.Interpolator;
44 import android.widget.FrameLayout;
45 
46 import androidx.annotation.VisibleForTesting;
47 
48 import com.android.car.notification.template.BasicNotificationViewHolder;
49 import com.android.car.notification.template.CallNotificationViewHolder;
50 import com.android.car.notification.template.EmergencyNotificationViewHolder;
51 import com.android.car.notification.template.InboxNotificationViewHolder;
52 import com.android.car.notification.template.MessageNotificationViewHolder;
53 import com.android.car.notification.template.NavigationNotificationViewHolder;
54 
55 import java.util.HashMap;
56 import java.util.Map;
57 
58 /**
59  * Notification Manager for heads-up notifications in car.
60  */
61 public class CarHeadsUpNotificationManager
62         implements CarUxRestrictionsManager.OnUxRestrictionsChangedListener {
63     private static final String TAG = CarHeadsUpNotificationManager.class.getSimpleName();
64 
65     private final Beeper mBeeper;
66     private final Context mContext;
67     private final boolean mEnableNavigationHeadsup;
68     private final long mDuration;
69     private final long mMinDisplayDuration;
70     private final long mEnterAnimationDuration;
71     private final long mAlphaEnterAnimationDuration;
72     private final long mExitAnimationDuration;
73     private final int mNotificationHeadsUpCardMarginTop;
74 
75     private final KeyguardManager mKeyguardManager;
76     private final CarUserManagerHelper mCarUserManagerHelper;
77     private final PreprocessingManager mPreprocessingManager;
78     private final WindowManager mWindowManager;
79     private final LayoutInflater mInflater;
80 
81     private boolean mShouldRestrictMessagePreview;
82     private NotificationClickHandlerFactory mClickHandlerFactory;
83     private NotificationDataManager mNotificationDataManager;
84 
85     // key for the map is the statusbarnotification key
86     private final Map<String, HeadsUpEntry> mActiveHeadsUpNotifications;
87     // view that contains scrim and notification content
88     protected final View mHeadsUpPanel;
89     // framelayout that notification content should be added to.
90     protected final FrameLayout mHeadsUpContentFrame;
91 
CarHeadsUpNotificationManager(Context context, NotificationClickHandlerFactory clickHandlerFactory, NotificationDataManager notificationDataManager)92     public CarHeadsUpNotificationManager(Context context,
93             NotificationClickHandlerFactory clickHandlerFactory,
94             NotificationDataManager notificationDataManager) {
95         mContext = context.getApplicationContext();
96         mEnableNavigationHeadsup =
97                 context.getResources().getBoolean(R.bool.config_showNavigationHeadsup);
98         mClickHandlerFactory = clickHandlerFactory;
99         mNotificationDataManager = notificationDataManager;
100         mBeeper = new Beeper(mContext);
101         mDuration = mContext.getResources().getInteger(R.integer.headsup_notification_duration_ms);
102         mNotificationHeadsUpCardMarginTop = (int) mContext.getResources().getDimension(
103                 R.dimen.headsup_notification_top_margin);
104         mMinDisplayDuration = mContext.getResources().getInteger(
105                 R.integer.heads_up_notification_minimum_time);
106         mEnterAnimationDuration =
107                 mContext.getResources().getInteger(R.integer.headsup_total_enter_duration_ms);
108         mAlphaEnterAnimationDuration =
109                 mContext.getResources().getInteger(R.integer.headsup_alpha_enter_duration_ms);
110         mExitAnimationDuration =
111                 mContext.getResources().getInteger(R.integer.headsup_exit_duration_ms);
112         mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
113         mPreprocessingManager = PreprocessingManager.getInstance(context);
114         mWindowManager =
115                 (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
116         mInflater = LayoutInflater.from(mContext);
117         mActiveHeadsUpNotifications = new HashMap<>();
118         mHeadsUpPanel = createHeadsUpPanel();
119         mHeadsUpContentFrame = mHeadsUpPanel.findViewById(R.id.headsup_content);
120         mCarUserManagerHelper = new CarUserManagerHelper(mContext);
121         addHeadsUpPanelToDisplay();
122     }
123 
124     /**
125      * Construct and return the heads up panel.
126      *
127      * @return view that contains R.id.headsup_content
128      */
createHeadsUpPanel()129     protected View createHeadsUpPanel() {
130         return mInflater.inflate(R.layout.headsup_container, null);
131     }
132 
133     /**
134      * Attach the heads up panel to the display
135      */
addHeadsUpPanelToDisplay()136     protected void addHeadsUpPanelToDisplay() {
137         WindowManager.LayoutParams wrapperParams = new WindowManager.LayoutParams(
138                 WindowManager.LayoutParams.MATCH_PARENT,
139                 WindowManager.LayoutParams.MATCH_PARENT,
140                 // This type allows covering status bar and receiving touch input
141                 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
142                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
143                         | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
144                 PixelFormat.TRANSLUCENT);
145         wrapperParams.gravity = Gravity.TOP;
146         mHeadsUpPanel.setVisibility(View.INVISIBLE);
147         mWindowManager.addView(mHeadsUpPanel, wrapperParams);
148     }
149 
150     /**
151      * Set the Heads Up view to visible
152      */
setHeadsUpVisible()153     protected void setHeadsUpVisible() {
154         mHeadsUpPanel.setVisibility(View.VISIBLE);
155     }
156 
157     /**
158      * Show the notification as a heads-up if it meets the criteria.
159      */
maybeShowHeadsUp( StatusBarNotification statusBarNotification, NotificationListenerService.RankingMap rankingMap, Map<String, StatusBarNotification> activeNotifications)160     public void maybeShowHeadsUp(
161             StatusBarNotification statusBarNotification,
162             NotificationListenerService.RankingMap rankingMap,
163             Map<String, StatusBarNotification> activeNotifications) {
164         if (!shouldShowHeadsUp(statusBarNotification, rankingMap)) {
165             // check if this is a update to the existing notification and if it should still show
166             // as a heads up or not.
167             HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
168                     statusBarNotification.getKey());
169             if (currentActiveHeadsUpNotification == null) {
170                 activeNotifications.put(statusBarNotification.getKey(), statusBarNotification);
171                 return;
172             }
173             if (CarNotificationDiff.sameNotificationKey(
174                     currentActiveHeadsUpNotification.getStatusBarNotification(),
175                     statusBarNotification)
176                     && currentActiveHeadsUpNotification.getHandler().hasMessagesOrCallbacks()) {
177                 animateOutHUN(statusBarNotification);
178             }
179             activeNotifications.put(statusBarNotification.getKey(), statusBarNotification);
180             return;
181         }
182         if (!activeNotifications.containsKey(statusBarNotification.getKey()) || canUpdate(
183                 statusBarNotification) || alertAgain(statusBarNotification.getNotification())) {
184             showHeadsUp(mPreprocessingManager.optimizeForDriving(statusBarNotification),
185                     rankingMap);
186         }
187         activeNotifications.put(statusBarNotification.getKey(), statusBarNotification);
188     }
189 
190     /**
191      * This method gets called when an app wants to cancel or withdraw its notification.
192      */
maybeRemoveHeadsUp(StatusBarNotification statusBarNotification)193     public void maybeRemoveHeadsUp(StatusBarNotification statusBarNotification) {
194         HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
195                 statusBarNotification.getKey());
196         // if the heads up notification is already removed do nothing.
197         if (currentActiveHeadsUpNotification == null) {
198             return;
199         }
200 
201         long totalDisplayDuration =
202                 System.currentTimeMillis() - currentActiveHeadsUpNotification.getPostTime();
203         // ongoing notification that has passed the minimum threshold display time.
204         if (totalDisplayDuration >= mMinDisplayDuration) {
205             animateOutHUN(statusBarNotification);
206             return;
207         }
208 
209         long earliestRemovalTime = mMinDisplayDuration - totalDisplayDuration;
210 
211         currentActiveHeadsUpNotification.getHandler().postDelayed(() ->
212                 animateOutHUN(statusBarNotification), earliestRemovalTime);
213     }
214 
215     /**
216      * Returns true if the notification's flag is not set to
217      * {@link Notification#FLAG_ONLY_ALERT_ONCE}
218      */
alertAgain(Notification newNotification)219     private boolean alertAgain(Notification newNotification) {
220         return (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0;
221     }
222 
223     /**
224      * Return true if the currently displaying notification have the same key as the new added
225      * notification. In that case it will be considered as an update to the currently displayed
226      * notification.
227      */
isUpdate(StatusBarNotification statusBarNotification)228     private boolean isUpdate(StatusBarNotification statusBarNotification) {
229         HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
230                 statusBarNotification.getKey());
231         if (currentActiveHeadsUpNotification == null) {
232             return false;
233         }
234         return CarNotificationDiff.sameNotificationKey(
235                 currentActiveHeadsUpNotification.getStatusBarNotification(),
236                 statusBarNotification);
237     }
238 
239     /**
240      * Updates only when the notification is being displayed.
241      */
canUpdate(StatusBarNotification statusBarNotification)242     private boolean canUpdate(StatusBarNotification statusBarNotification) {
243         HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
244                 statusBarNotification.getKey());
245         return currentActiveHeadsUpNotification != null && System.currentTimeMillis() -
246                 currentActiveHeadsUpNotification.getPostTime() < mDuration;
247     }
248 
249     /**
250      * Returns the active headsUpEntry or creates a new one while adding it to the list of
251      * mActiveHeadsUpNotifications.
252      */
addNewHeadsUpEntry(StatusBarNotification statusBarNotification)253     private HeadsUpEntry addNewHeadsUpEntry(StatusBarNotification statusBarNotification) {
254         HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get(
255                 statusBarNotification.getKey());
256         if (currentActiveHeadsUpNotification == null) {
257             currentActiveHeadsUpNotification = new HeadsUpEntry(statusBarNotification);
258             mActiveHeadsUpNotifications.put(statusBarNotification.getKey(),
259                     currentActiveHeadsUpNotification);
260             currentActiveHeadsUpNotification.isAlertAgain = alertAgain(
261                     statusBarNotification.getNotification());
262             currentActiveHeadsUpNotification.isNewHeadsUp = true;
263             return currentActiveHeadsUpNotification;
264         }
265         currentActiveHeadsUpNotification.isNewHeadsUp = false;
266         currentActiveHeadsUpNotification.isAlertAgain = alertAgain(
267                 statusBarNotification.getNotification());
268         if (currentActiveHeadsUpNotification.isAlertAgain) {
269             // This is a ongoing notification which needs to be alerted again to the user. This
270             // requires for the post time to be updated.
271             currentActiveHeadsUpNotification.updatePostTime();
272         }
273         return currentActiveHeadsUpNotification;
274     }
275 
276     /**
277      * Controls three major conditions while showing heads up notification.
278      * <p>
279      * <ol>
280      * <li> When a new HUN comes in it will be displayed with animations
281      * <li> If an update to existing HUN comes in which enforces to alert the HUN again to user,
282      * then the post time will be updated to current time. This will only be done if {@link
283      * Notification#FLAG_ONLY_ALERT_ONCE} flag is not set.
284      * <li> If an update to existing HUN comes in which just updates the data and does not want to
285      * alert itself again, then the animations will not be shown and the data will get updated. This
286      * will only be done if {@link Notification#FLAG_ONLY_ALERT_ONCE} flag is not set.
287      * </ol>
288      */
showHeadsUp(StatusBarNotification statusBarNotification, NotificationListenerService.RankingMap rankingMap)289     private void showHeadsUp(StatusBarNotification statusBarNotification,
290             NotificationListenerService.RankingMap rankingMap) {
291         // Show animations only when there is no active HUN and notification is new. This check
292         // needs to be done here because after this the new notification will be added to the map
293         // holding ongoing notifications.
294         boolean shouldShowAnimation = !isUpdate(statusBarNotification);
295         HeadsUpEntry currentNotification = addNewHeadsUpEntry(statusBarNotification);
296         if (currentNotification.isNewHeadsUp) {
297             playSound(statusBarNotification, rankingMap);
298             setHeadsUpVisible();
299             setAutoDismissViews(currentNotification, statusBarNotification);
300         } else if (currentNotification.isAlertAgain) {
301             setAutoDismissViews(currentNotification, statusBarNotification);
302         }
303         @NotificationViewType int viewType = getNotificationViewType(statusBarNotification);
304         mClickHandlerFactory.setHeadsUpNotificationCallBack(
305                 () -> animateOutHUN(statusBarNotification));
306         currentNotification.setClickHandlerFactory(mClickHandlerFactory);
307         switch (viewType) {
308             case NotificationViewType.CAR_EMERGENCY_HEADSUP: {
309                 if (currentNotification.getNotificationView() == null) {
310                     currentNotification.setNotificationView(mInflater.inflate(
311                             R.layout.car_emergency_headsup_notification_template,
312                             null));
313                     mHeadsUpContentFrame.addView(currentNotification.getNotificationView());
314                     currentNotification.setViewHolder(
315                             new EmergencyNotificationViewHolder(
316                                     currentNotification.getNotificationView(),
317                                     mClickHandlerFactory));
318                 }
319                 currentNotification.getViewHolder().bind(statusBarNotification,
320                         /* isInGroup= */ false, /* isHeadsUp= */ true);
321                 break;
322             }
323             case NotificationViewType.NAVIGATION: {
324                 if (currentNotification.getNotificationView() == null) {
325                     currentNotification.setNotificationView(mInflater.inflate(
326                             R.layout.navigation_headsup_notification_template,
327                             null));
328                     mHeadsUpContentFrame.addView(currentNotification.getNotificationView());
329                     currentNotification.setViewHolder(
330                             new NavigationNotificationViewHolder(
331                                     currentNotification.getNotificationView(),
332                                     mClickHandlerFactory));
333                 }
334                 currentNotification.getViewHolder().bind(statusBarNotification,
335                         /* isInGroup= */ false, /* isHeadsUp= */ true);
336                 break;
337             }
338             case NotificationViewType.CALL: {
339                 if (currentNotification.getNotificationView() == null) {
340                     currentNotification.setNotificationView(mInflater.inflate(
341                             R.layout.call_headsup_notification_template,
342                             null));
343                     mHeadsUpContentFrame.addView(currentNotification.getNotificationView());
344                     currentNotification.setViewHolder(
345                             new CallNotificationViewHolder(
346                                     currentNotification.getNotificationView(),
347                                     mClickHandlerFactory));
348                 }
349                 currentNotification.getViewHolder().bind(statusBarNotification,
350                         /* isInGroup= */ false, /* isHeadsUp= */ true);
351                 break;
352             }
353             case NotificationViewType.CAR_WARNING_HEADSUP: {
354                 if (currentNotification.getNotificationView() == null) {
355                     currentNotification.setNotificationView(mInflater.inflate(
356                             R.layout.car_warning_headsup_notification_template,
357                             null));
358                     mHeadsUpContentFrame.addView(currentNotification.getNotificationView());
359                     // Using the basic view holder because they share the same view binding logic
360                     // OEMs should create view holders if needed
361                     currentNotification.setViewHolder(
362                             new BasicNotificationViewHolder(
363                                     currentNotification.getNotificationView(),
364                                     mClickHandlerFactory));
365                 }
366                 currentNotification.getViewHolder().bind(statusBarNotification, /* isInGroup= */
367                         false, /* isHeadsUp= */ true);
368                 break;
369             }
370             case NotificationViewType.CAR_INFORMATION_HEADSUP: {
371                 if (currentNotification.getNotificationView() == null) {
372                     currentNotification.setNotificationView(mInflater.inflate(
373                             R.layout.car_information_headsup_notification_template,
374                             null));
375                     mHeadsUpContentFrame.addView(currentNotification.getNotificationView());
376                     // Using the basic view holder because they share the same view binding logic
377                     // OEMs should create view holders if needed
378                     currentNotification.setViewHolder(
379                             new BasicNotificationViewHolder(
380                                     currentNotification.getNotificationView(),
381                                     mClickHandlerFactory));
382                 }
383                 currentNotification.getViewHolder().bind(statusBarNotification,
384                         /* isInGroup= */ false, /* isHeadsUp= */ true);
385                 break;
386             }
387             case NotificationViewType.MESSAGE_HEADSUP: {
388                 if (currentNotification.getNotificationView() == null) {
389                     currentNotification.setNotificationView(mInflater.inflate(
390                             R.layout.message_headsup_notification_template,
391                             null));
392                     mHeadsUpContentFrame.addView(currentNotification.getNotificationView());
393                     currentNotification.setViewHolder(
394                             new MessageNotificationViewHolder(
395                                     currentNotification.getNotificationView(),
396                                     mClickHandlerFactory));
397                 }
398                 if (mShouldRestrictMessagePreview) {
399                     ((MessageNotificationViewHolder) currentNotification.getViewHolder())
400                             .bindRestricted(statusBarNotification, /* isInGroup= */
401                                     false, /* isHeadsUp= */ true);
402                 } else {
403                     currentNotification.getViewHolder().bind(statusBarNotification, /* isInGroup= */
404                             false, /* isHeadsUp= */ true);
405                 }
406                 break;
407             }
408             case NotificationViewType.INBOX_HEADSUP: {
409                 if (currentNotification.getNotificationView() == null) {
410                     currentNotification.setNotificationView(mInflater.inflate(
411                             R.layout.inbox_headsup_notification_template,
412                             null));
413                     mHeadsUpContentFrame.addView(currentNotification.getNotificationView());
414                     currentNotification.setViewHolder(
415                             new InboxNotificationViewHolder(
416                                     currentNotification.getNotificationView(),
417                                     mClickHandlerFactory));
418                 }
419                 currentNotification.getViewHolder().bind(statusBarNotification,
420                         /* isInGroup= */ false, /* isHeadsUp= */ true);
421                 break;
422             }
423             case NotificationViewType.BASIC_HEADSUP:
424             default: {
425                 if (currentNotification.getNotificationView() == null) {
426                     currentNotification.setNotificationView(mInflater.inflate(
427                             R.layout.basic_headsup_notification_template,
428                             null));
429                     mHeadsUpContentFrame.addView(currentNotification.getNotificationView());
430                     currentNotification.setViewHolder(
431                             new BasicNotificationViewHolder(
432                                     currentNotification.getNotificationView(),
433                                     mClickHandlerFactory));
434                 }
435                 currentNotification.getViewHolder().bind(statusBarNotification,
436                         /* isInGroup= */ false, /* isHeadsUp= */ true);
437                 break;
438             }
439         }
440 
441         // measure the size of the card and make that area of the screen touchable
442         currentNotification.getNotificationView().getViewTreeObserver()
443                 .addOnComputeInternalInsetsListener(
444                         info -> setInternalInsetsInfo(info,
445                                 currentNotification, /* panelExpanded= */false));
446         // Get the height of the notification view after onLayout()
447         // in order animate the notification in
448         currentNotification.getNotificationView().getViewTreeObserver().addOnGlobalLayoutListener(
449                 new ViewTreeObserver.OnGlobalLayoutListener() {
450                     @Override
451                     public void onGlobalLayout() {
452                         int notificationHeight =
453                                 currentNotification.getNotificationView().getHeight();
454 
455                         if (shouldShowAnimation) {
456                             currentNotification.getNotificationView().setY(0 - notificationHeight);
457                             currentNotification.getNotificationView().setAlpha(0f);
458 
459                             Interpolator yPositionInterpolator = AnimationUtils.loadInterpolator(
460                                     mContext,
461                                     R.interpolator.heads_up_entry_direction_interpolator);
462                             Interpolator alphaInterpolator = AnimationUtils.loadInterpolator(
463                                     mContext,
464                                     R.interpolator.heads_up_entry_alpha_interpolator);
465 
466                             ObjectAnimator moveY = ObjectAnimator.ofFloat(
467                                     currentNotification.getNotificationView(), "y", 0f);
468                             moveY.setDuration(mEnterAnimationDuration);
469                             moveY.setInterpolator(yPositionInterpolator);
470 
471                             ObjectAnimator alpha = ObjectAnimator.ofFloat(
472                                     currentNotification.getNotificationView(), "alpha", 1f);
473                             alpha.setDuration(mAlphaEnterAnimationDuration);
474                             alpha.setInterpolator(alphaInterpolator);
475 
476                             AnimatorSet animatorSet = new AnimatorSet();
477                             animatorSet.playTogether(moveY, alpha);
478                             animatorSet.start();
479 
480                         }
481                         currentNotification.getNotificationView().getViewTreeObserver()
482                                 .removeOnGlobalLayoutListener(this);
483                     }
484                 });
485 
486         if (currentNotification.isNewHeadsUp) {
487             boolean shouldDismissOnSwipe = true;
488             if (shouldDismissOnSwipe(statusBarNotification)) {
489                 shouldDismissOnSwipe = false;
490             }
491             // Add swipe gesture
492             View cardView = currentNotification.getNotificationView().findViewById(R.id.card_view);
493             cardView.setOnTouchListener(
494                     new HeadsUpNotificationOnTouchListener(cardView, shouldDismissOnSwipe,
495                             () -> resetView(statusBarNotification)));
496         }
497     }
498 
setInternalInsetsInfo(ViewTreeObserver.InternalInsetsInfo info, HeadsUpEntry currentNotification, boolean panelExpanded)499     protected void setInternalInsetsInfo(ViewTreeObserver.InternalInsetsInfo info,
500             HeadsUpEntry currentNotification, boolean panelExpanded) {
501         // If the panel is not on screen don't modify the touch region
502         if (mHeadsUpPanel.getVisibility() != View.VISIBLE) return;
503         int[] mTmpTwoArray = new int[2];
504         View cardView = currentNotification.getNotificationView().findViewById(
505                 R.id.card_view);
506 
507         if (cardView == null) return;
508 
509         if (panelExpanded) {
510             info.setTouchableInsets(
511                     ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
512             return;
513         }
514 
515         cardView.getLocationOnScreen(mTmpTwoArray);
516         int minX = mTmpTwoArray[0];
517         int maxX = mTmpTwoArray[0] + cardView.getWidth();
518         int height = cardView.getHeight();
519         info.setTouchableInsets(
520                 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
521         info.touchableRegion.set(minX, mNotificationHeadsUpCardMarginTop, maxX,
522                 height + mNotificationHeadsUpCardMarginTop);
523     }
524 
playSound(StatusBarNotification statusBarNotification, NotificationListenerService.RankingMap rankingMap)525     private void playSound(StatusBarNotification statusBarNotification,
526             NotificationListenerService.RankingMap rankingMap) {
527         NotificationListenerService.Ranking ranking = getRanking();
528         if (rankingMap.getRanking(statusBarNotification.getKey(), ranking)) {
529             NotificationChannel notificationChannel = ranking.getChannel();
530             // If sound is not set on the notification channel and default is not chosen it
531             // can be null.
532             if (notificationChannel.getSound() != null) {
533                 // make the sound
534                 mBeeper.beep(statusBarNotification.getPackageName(),
535                         notificationChannel.getSound());
536             }
537         }
538     }
539 
shouldDismissOnSwipe(StatusBarNotification statusBarNotification)540     private boolean shouldDismissOnSwipe(StatusBarNotification statusBarNotification) {
541         return hasFullScreenIntent(statusBarNotification)
542                 && statusBarNotification.getNotification().category.equals(
543                 Notification.CATEGORY_CALL) && statusBarNotification.isOngoing();
544     }
545 
546 
547     @VisibleForTesting
getActiveHeadsUpNotifications()548     protected Map<String, HeadsUpEntry> getActiveHeadsUpNotifications() {
549         return mActiveHeadsUpNotifications;
550     }
551 
setAutoDismissViews(HeadsUpEntry currentNotification, StatusBarNotification statusBarNotification)552     private void setAutoDismissViews(HeadsUpEntry currentNotification,
553             StatusBarNotification statusBarNotification) {
554         // Should not auto dismiss if HUN has a full screen Intent.
555         if (hasFullScreenIntent(statusBarNotification)) {
556             return;
557         }
558         currentNotification.getHandler().removeCallbacksAndMessages(null);
559         currentNotification.getHandler().postDelayed(() -> animateOutHUN(statusBarNotification),
560                 mDuration);
561     }
562 
563     /**
564      * Returns true if StatusBarNotification has a full screen Intent.
565      */
hasFullScreenIntent(StatusBarNotification sbn)566     private boolean hasFullScreenIntent(StatusBarNotification sbn) {
567         return sbn.getNotification().fullScreenIntent != null;
568     }
569 
570     /**
571      * Animates the heads up notification out of the screen and reset the views.
572      */
animateOutHUN(StatusBarNotification statusBarNotification)573     private void animateOutHUN(StatusBarNotification statusBarNotification) {
574         Log.d(TAG, "clearViews for Heads Up Notification: ");
575         // get the current notification to perform animations and remove it immediately from the
576         // active notification maps and cancel all other call backs if any.
577         HeadsUpEntry currentHeadsUpNotification = mActiveHeadsUpNotifications.get(
578                 statusBarNotification.getKey());
579         // view can also be removed when swipped away.
580         if (currentHeadsUpNotification == null) {
581             return;
582         }
583         currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null);
584         currentHeadsUpNotification.getClickHandlerFactory().setHeadsUpNotificationCallBack(null);
585 
586         Interpolator exitInterpolator = AnimationUtils.loadInterpolator(mContext,
587                 R.interpolator.heads_up_exit_direction_interpolator);
588         Interpolator alphaInterpolator = AnimationUtils.loadInterpolator(mContext,
589                 R.interpolator.heads_up_exit_alpha_interpolator);
590 
591         ObjectAnimator moveY = ObjectAnimator.ofFloat(
592                 currentHeadsUpNotification.getNotificationView(), "y",
593                 -1 * currentHeadsUpNotification.getNotificationView().getHeight());
594         moveY.setDuration(mExitAnimationDuration);
595         moveY.setInterpolator(exitInterpolator);
596 
597         ObjectAnimator alpha = ObjectAnimator.ofFloat(
598                 currentHeadsUpNotification.getNotificationView(), "alpha", 1f);
599         alpha.setDuration(mExitAnimationDuration);
600         alpha.setInterpolator(alphaInterpolator);
601 
602         AnimatorSet animatorSet = new AnimatorSet();
603         animatorSet.playTogether(moveY, alpha);
604         animatorSet.addListener(new AnimatorListenerAdapter() {
605             @Override
606             public void onAnimationEnd(Animator animation) {
607                 removeNotificationFromPanel(currentHeadsUpNotification);
608 
609                 // Remove HUN after the animation ends to prevent accidental touch on the card
610                 // triggering another remove call.
611                 mActiveHeadsUpNotifications.remove(statusBarNotification.getKey());
612             }
613         });
614         animatorSet.start();
615     }
616 
617     /**
618      * Remove notification from the screen. If it was the last notification hide the heads up panel.
619      *
620      * @param currentHeadsUpNotification The notification to remove
621      */
removeNotificationFromPanel(HeadsUpEntry currentHeadsUpNotification)622     protected void removeNotificationFromPanel(HeadsUpEntry currentHeadsUpNotification) {
623         mHeadsUpContentFrame.removeView(currentHeadsUpNotification.getNotificationView());
624         if (mHeadsUpContentFrame.getChildCount() == 0) {
625             mHeadsUpPanel.setVisibility(View.INVISIBLE);
626         }
627     }
628 
629 
630     /**
631      * Removes the view for the active heads up notification and also removes the HUN from the map
632      * of active Notifications.
633      */
resetView(StatusBarNotification statusBarNotification)634     private void resetView(StatusBarNotification statusBarNotification) {
635         HeadsUpEntry currentHeadsUpNotification = mActiveHeadsUpNotifications.get(
636                 statusBarNotification.getKey());
637         if (currentHeadsUpNotification == null) return;
638 
639         currentHeadsUpNotification.getClickHandlerFactory().setHeadsUpNotificationCallBack(null);
640         currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null);
641         removeNotificationFromPanel(currentHeadsUpNotification);
642         mActiveHeadsUpNotifications.remove(statusBarNotification.getKey());
643     }
644 
645     /**
646      * Choose a correct notification layout for this heads-up notification.
647      * Note that the layout chosen can be different for the same notification
648      * in the notification center.
649      */
650     @NotificationViewType
getNotificationViewType(StatusBarNotification statusBarNotification)651     private static int getNotificationViewType(StatusBarNotification statusBarNotification) {
652         String category = statusBarNotification.getNotification().category;
653         if (category != null) {
654             switch (category) {
655                 case Notification.CATEGORY_CAR_EMERGENCY:
656                     return NotificationViewType.CAR_EMERGENCY_HEADSUP;
657                 case Notification.CATEGORY_NAVIGATION:
658                     return NotificationViewType.NAVIGATION;
659                 case Notification.CATEGORY_CALL:
660                     return NotificationViewType.CALL;
661                 case Notification.CATEGORY_CAR_WARNING:
662                     return NotificationViewType.CAR_WARNING_HEADSUP;
663                 case Notification.CATEGORY_CAR_INFORMATION:
664                     return NotificationViewType.CAR_INFORMATION_HEADSUP;
665                 case Notification.CATEGORY_MESSAGE:
666                     return NotificationViewType.MESSAGE_HEADSUP;
667                 default:
668                     break;
669             }
670         }
671         Bundle extras = statusBarNotification.getNotification().extras;
672         if (extras.containsKey(Notification.EXTRA_BIG_TEXT)
673                 && extras.containsKey(Notification.EXTRA_SUMMARY_TEXT)) {
674             return NotificationViewType.INBOX_HEADSUP;
675         }
676         // progress, media, big text, big picture, and basic templates
677         return NotificationViewType.BASIC_HEADSUP;
678     }
679 
680     /**
681      * Helper method that determines whether a notification should show as a heads-up.
682      *
683      * <p> A notification will never be shown as a heads-up if:
684      * <ul>
685      * <li> Keyguard (lock screen) is showing
686      * <li> OEMs configured CATEGORY_NAVIGATION should not be shown
687      * <li> Notification is muted.
688      * </ul>
689      *
690      * <p> A notification will be shown as a heads-up if:
691      * <ul>
692      * <li> Importance >= HIGH
693      * <li> it comes from an app signed with the platform key.
694      * <li> it comes from a privileged system app.
695      * <li> is a car compatible notification.
696      * {@link com.android.car.assist.client.CarAssistUtils#isCarCompatibleMessagingNotification}
697      * <li> Notification category is one of CATEGORY_CALL or CATEGORY_NAVIGATION
698      * </ul>
699      *
700      * <p> Group alert behavior still follows API documentation.
701      *
702      * @return true if a notification should be shown as a heads-up
703      */
shouldShowHeadsUp( StatusBarNotification statusBarNotification, NotificationListenerService.RankingMap rankingMap)704     private boolean shouldShowHeadsUp(
705             StatusBarNotification statusBarNotification,
706             NotificationListenerService.RankingMap rankingMap) {
707         if (mKeyguardManager.isKeyguardLocked()) {
708             return false;
709         }
710         Notification notification = statusBarNotification.getNotification();
711 
712         // Navigation notification configured by OEM
713         if (!mEnableNavigationHeadsup && Notification.CATEGORY_NAVIGATION.equals(
714                 notification.category)) {
715             return false;
716         }
717         // Group alert behavior
718         if (notification.suppressAlertingDueToGrouping()) {
719             return false;
720         }
721         // Messaging notification muted by user.
722         if (mNotificationDataManager.isMessageNotificationMuted(statusBarNotification)) {
723             return false;
724         }
725 
726         // Do not show if importance < HIGH
727         NotificationListenerService.Ranking ranking = getRanking();
728         if (rankingMap.getRanking(statusBarNotification.getKey(), ranking)) {
729             if (ranking.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
730                 return false;
731             }
732         }
733 
734         if (NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext,
735                 statusBarNotification)) {
736             return true;
737         }
738 
739         // Allow car messaging type.
740         if (isCarCompatibleMessagingNotification(statusBarNotification)) {
741             return true;
742         }
743 
744         if (notification.category == null) {
745             Log.d(TAG, "category not set for: " + statusBarNotification.getPackageName());
746         }
747 
748         // Allow for Call, and nav TBT categories.
749         if (Notification.CATEGORY_CALL.equals(notification.category)
750                 || Notification.CATEGORY_NAVIGATION.equals(notification.category)) {
751             return true;
752         }
753         return false;
754     }
755 
756     @VisibleForTesting
getRanking()757     protected NotificationListenerService.Ranking getRanking() {
758         return new NotificationListenerService.Ranking();
759     }
760 
761     @Override
onUxRestrictionsChanged(CarUxRestrictions restrictions)762     public void onUxRestrictionsChanged(CarUxRestrictions restrictions) {
763         mShouldRestrictMessagePreview =
764                 (restrictions.getActiveRestrictions()
765                         & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0;
766     }
767 
768     /**
769      * Sets the source of {@link View.OnClickListener}
770      *
771      * @param clickHandlerFactory used to generate onClickListeners
772      */
773     @VisibleForTesting
setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)774     public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) {
775         mClickHandlerFactory = clickHandlerFactory;
776     }
777 
778     /**
779      * Callback that will be issued after a heads up notification is clicked
780      */
781     public interface Callback {
782         /**
783          * Clears Heads up notification on click.
784          */
clearHeadsUpNotification()785         void clearHeadsUpNotification();
786     }
787 }
788