1 /*
2  * Copyright (C) 2020 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 
17 package com.android.systemui.car.notification;
18 
19 import android.app.ActivityManager;
20 import android.car.Car;
21 import android.car.drivingstate.CarUxRestrictionsManager;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Rect;
25 import android.graphics.drawable.Drawable;
26 import android.inputmethodservice.InputMethodService;
27 import android.os.IBinder;
28 import android.os.RemoteException;
29 import android.util.Log;
30 import android.view.GestureDetector;
31 import android.view.KeyEvent;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.WindowInsets;
36 
37 import androidx.annotation.NonNull;
38 import androidx.recyclerview.widget.RecyclerView;
39 
40 import com.android.car.notification.CarNotificationListener;
41 import com.android.car.notification.CarNotificationView;
42 import com.android.car.notification.CarUxRestrictionManagerWrapper;
43 import com.android.car.notification.NotificationClickHandlerFactory;
44 import com.android.car.notification.NotificationClickHandlerFactory.OnNotificationClickListener;
45 import com.android.car.notification.NotificationDataManager;
46 import com.android.car.notification.NotificationViewController;
47 import com.android.car.notification.PreprocessingManager;
48 import com.android.internal.statusbar.IStatusBarService;
49 import com.android.systemui.R;
50 import com.android.systemui.car.CarDeviceProvisionedController;
51 import com.android.systemui.car.CarServiceProvider;
52 import com.android.systemui.car.CarServiceProvider.CarServiceOnConnectedListener;
53 import com.android.systemui.car.window.OverlayPanelViewController;
54 import com.android.systemui.car.window.OverlayViewController;
55 import com.android.systemui.car.window.OverlayViewGlobalStateController;
56 import com.android.systemui.dagger.SysUISingleton;
57 import com.android.systemui.dagger.qualifiers.Main;
58 import com.android.systemui.dagger.qualifiers.UiBackground;
59 import com.android.systemui.plugins.statusbar.StatusBarStateController;
60 import com.android.systemui.statusbar.CommandQueue;
61 import com.android.systemui.statusbar.StatusBarState;
62 import com.android.wm.shell.animation.FlingAnimationUtils;
63 
64 import java.util.concurrent.Executor;
65 
66 import javax.inject.Inject;
67 
68 /** View controller for the notification panel. */
69 @SysUISingleton
70 public class NotificationPanelViewController extends OverlayPanelViewController
71         implements CommandQueue.Callbacks {
72 
73     private static final boolean DEBUG = true;
74     private static final String TAG = "NotificationPanelViewController";
75 
76     private final Context mContext;
77     private final Resources mResources;
78     private final CarServiceProvider mCarServiceProvider;
79     private final IStatusBarService mBarService;
80     private final CommandQueue mCommandQueue;
81     private final Executor mUiBgExecutor;
82     private final NotificationDataManager mNotificationDataManager;
83     private final CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper;
84     private final CarNotificationListener mCarNotificationListener;
85     private final NotificationClickHandlerFactory mNotificationClickHandlerFactory;
86     private final StatusBarStateController mStatusBarStateController;
87     private final boolean mEnableHeadsUpNotificationWhenNotificationPanelOpen;
88     private final NotificationVisibilityLogger mNotificationVisibilityLogger;
89 
90     private final boolean mFitTopSystemBarInset;
91     private final boolean mFitBottomSystemBarInset;
92     private final boolean mFitLeftSystemBarInset;
93     private final boolean mFitRightSystemBarInset;
94 
95     private float mInitialBackgroundAlpha;
96     private float mBackgroundAlphaDiff;
97 
98     private CarNotificationView mNotificationView;
99     private RecyclerView mNotificationList;
100     private NotificationViewController mNotificationViewController;
101 
102     private boolean mNotificationListAtEnd;
103     private float mFirstTouchDownOnGlassPane;
104     private boolean mNotificationListAtEndAtTimeOfTouch;
105     private boolean mIsSwipingVerticallyToClose;
106     private boolean mIsNotificationCardSwiping;
107     private boolean mImeVisible = false;
108     private boolean mOnConnectListenerAdded;
109 
110     private OnUnseenCountUpdateListener mUnseenCountUpdateListener;
111     private OnNotificationClickListener mOnNotificationClickListener =
112             (launchResult, alertEntry) -> {
113                 if (launchResult == ActivityManager.START_TASK_TO_FRONT
114                         || launchResult == ActivityManager.START_SUCCESS
115                         || launchResult == ActivityManager.START_DELIVERED_TO_TOP) {
116                     animateCollapsePanel();
117                 }
118             };
119 
120     private CarServiceOnConnectedListener mCarConnectedListener =
121             new CarServiceOnConnectedListener() {
122                 @Override
123                 public void onConnected(Car car) {
124                     CarUxRestrictionsManager carUxRestrictionsManager =
125                             (CarUxRestrictionsManager)
126                                     car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
127                     mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager(
128                             carUxRestrictionsManager);
129 
130                     PreprocessingManager preprocessingManager =
131                             PreprocessingManager.getInstance(mContext);
132                     preprocessingManager.setCarUxRestrictionManagerWrapper(
133                             mCarUxRestrictionManagerWrapper);
134 
135                     mNotificationViewController.enable();
136                 }
137             };
138 
139     @Inject
NotificationPanelViewController( Context context, @Main Resources resources, OverlayViewGlobalStateController overlayViewGlobalStateController, FlingAnimationUtils.Builder flingAnimationUtilsBuilder, @UiBackground Executor uiBgExecutor, CarServiceProvider carServiceProvider, CarDeviceProvisionedController carDeviceProvisionedController, IStatusBarService barService, CommandQueue commandQueue, NotificationDataManager notificationDataManager, CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper, CarNotificationListener carNotificationListener, NotificationClickHandlerFactory notificationClickHandlerFactory, NotificationVisibilityLogger notificationVisibilityLogger, StatusBarStateController statusBarStateController )140     public NotificationPanelViewController(
141             Context context,
142             @Main Resources resources,
143             OverlayViewGlobalStateController overlayViewGlobalStateController,
144             FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
145             @UiBackground Executor uiBgExecutor,
146 
147             /* Other things */
148             CarServiceProvider carServiceProvider,
149             CarDeviceProvisionedController carDeviceProvisionedController,
150 
151             /* Things needed for notifications */
152             IStatusBarService barService,
153             CommandQueue commandQueue,
154             NotificationDataManager notificationDataManager,
155             CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper,
156             CarNotificationListener carNotificationListener,
157             NotificationClickHandlerFactory notificationClickHandlerFactory,
158             NotificationVisibilityLogger notificationVisibilityLogger,
159 
160             /* Things that need to be replaced */
161             StatusBarStateController statusBarStateController
162     ) {
163         super(context, resources, R.id.notification_panel_stub, overlayViewGlobalStateController,
164                 flingAnimationUtilsBuilder, carDeviceProvisionedController);
165         mContext = context;
166         mResources = resources;
167         mCarServiceProvider = carServiceProvider;
168         mBarService = barService;
169         mCommandQueue = commandQueue;
170         mUiBgExecutor = uiBgExecutor;
171         mNotificationDataManager = notificationDataManager;
172         mCarUxRestrictionManagerWrapper = carUxRestrictionManagerWrapper;
173         mCarNotificationListener = carNotificationListener;
174         mNotificationClickHandlerFactory = notificationClickHandlerFactory;
175         mStatusBarStateController = statusBarStateController;
176         mNotificationVisibilityLogger = notificationVisibilityLogger;
177 
178         mCommandQueue.addCallback(this);
179 
180         // Notification background setup.
181         mInitialBackgroundAlpha = (float) mResources.getInteger(
182                 R.integer.config_initialNotificationBackgroundAlpha) / 100;
183         if (mInitialBackgroundAlpha < 0 || mInitialBackgroundAlpha > 100) {
184             throw new RuntimeException(
185                     "Unable to setup notification bar due to incorrect initial background alpha"
186                             + " percentage");
187         }
188         float finalBackgroundAlpha = Math.max(
189                 mInitialBackgroundAlpha,
190                 (float) mResources.getInteger(
191                         R.integer.config_finalNotificationBackgroundAlpha) / 100);
192         if (finalBackgroundAlpha < 0 || finalBackgroundAlpha > 100) {
193             throw new RuntimeException(
194                     "Unable to setup notification bar due to incorrect final background alpha"
195                             + " percentage");
196         }
197         mBackgroundAlphaDiff = finalBackgroundAlpha - mInitialBackgroundAlpha;
198 
199         mEnableHeadsUpNotificationWhenNotificationPanelOpen = mResources.getBoolean(
200                 com.android.car.notification.R.bool
201                         .config_enableHeadsUpNotificationWhenNotificationPanelOpen);
202 
203         mFitTopSystemBarInset = mResources.getBoolean(
204                 R.bool.config_notif_panel_inset_by_top_systembar);
205         mFitBottomSystemBarInset = mResources.getBoolean(
206                 R.bool.config_notif_panel_inset_by_bottom_systembar);
207         mFitLeftSystemBarInset = mResources.getBoolean(
208                 R.bool.config_notif_panel_inset_by_left_systembar);
209         mFitRightSystemBarInset = mResources.getBoolean(
210                 R.bool.config_notif_panel_inset_by_right_systembar);
211 
212         // Inflate view on instantiation to properly initialize listeners even if panel has
213         // not been opened.
214         getOverlayViewGlobalStateController().inflateView(this);
215     }
216 
217     // CommandQueue.Callbacks
218 
219     @Override
animateExpandNotificationsPanel()220     public void animateExpandNotificationsPanel() {
221         if (!isPanelExpanded()) {
222             toggle();
223         }
224     }
225 
226     @Override
animateCollapsePanels(int flags, boolean force)227     public void animateCollapsePanels(int flags, boolean force) {
228         if (isPanelExpanded()) {
229             toggle();
230         }
231     }
232 
233     @Override
setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition, boolean showImeSwitcher)234     public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
235             boolean showImeSwitcher) {
236         if (mContext.getDisplayId() != displayId) {
237             return;
238         }
239         mImeVisible = (vis & InputMethodService.IME_VISIBLE) != 0;
240     }
241 
242     // OverlayViewController
243 
244     @Override
onFinishInflate()245     protected void onFinishInflate() {
246         reinflate();
247     }
248 
249     @Override
hideInternal()250     protected void hideInternal() {
251         super.hideInternal();
252         mNotificationVisibilityLogger.stop();
253     }
254 
255     @Override
getFocusAreaViewId()256     protected int getFocusAreaViewId() {
257         return R.id.notification_container;
258     }
259 
260     @Override
shouldShowNavigationBarInsets()261     protected boolean shouldShowNavigationBarInsets() {
262         return true;
263     }
264 
265     @Override
shouldShowStatusBarInsets()266     protected boolean shouldShowStatusBarInsets() {
267         return true;
268     }
269 
270     @Override
getInsetSidesToFit()271     protected int getInsetSidesToFit() {
272         int insetSidesToFit = OverlayViewController.NO_INSET_SIDE;
273 
274         if (mFitTopSystemBarInset) {
275             insetSidesToFit = insetSidesToFit | WindowInsets.Side.TOP;
276         }
277 
278         if (mFitBottomSystemBarInset) {
279             insetSidesToFit = insetSidesToFit | WindowInsets.Side.BOTTOM;
280         }
281 
282         if (mFitLeftSystemBarInset) {
283             insetSidesToFit = insetSidesToFit | WindowInsets.Side.LEFT;
284         }
285 
286         if (mFitRightSystemBarInset) {
287             insetSidesToFit = insetSidesToFit | WindowInsets.Side.RIGHT;
288         }
289 
290         return insetSidesToFit;
291     }
292 
293     @Override
shouldShowHUN()294     protected boolean shouldShowHUN() {
295         return mEnableHeadsUpNotificationWhenNotificationPanelOpen;
296     }
297 
298     @Override
shouldUseStableInsets()299     protected boolean shouldUseStableInsets() {
300         // When IME is visible, then the inset from the nav bar should not be applied.
301         return !mImeVisible;
302     }
303 
304     /** Reinflates the view. */
reinflate()305     public void reinflate() {
306         // Do not reinflate the view if it has not been inflated at all.
307         if (!isInflated()) return;
308 
309         mNotificationClickHandlerFactory.unregisterClickListener(mOnNotificationClickListener);
310 
311         if (mOnConnectListenerAdded) {
312             mCarServiceProvider.removeListener(mCarConnectedListener);
313             mOnConnectListenerAdded = false;
314         }
315 
316         ViewGroup container = (ViewGroup) getLayout();
317         container.removeView(mNotificationView);
318 
319         mNotificationView = (CarNotificationView) LayoutInflater.from(mContext).inflate(
320                 R.layout.notification_center_activity, container,
321                 /* attachToRoot= */ false);
322         mNotificationView.setKeyEventHandler(
323                 event -> {
324                     if (event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
325                         return false;
326                     }
327 
328                     if (event.getAction() == KeyEvent.ACTION_UP && isPanelExpanded()) {
329                         toggle();
330                     }
331                     return true;
332                 });
333 
334         container.addView(mNotificationView);
335         onNotificationViewInflated();
336     }
337 
onNotificationViewInflated()338     private void onNotificationViewInflated() {
339         // Find views.
340         mNotificationView = getLayout().findViewById(R.id.notification_view);
341         setUpHandleBar();
342         setupNotificationPanel();
343 
344         mNotificationClickHandlerFactory.registerClickListener(mOnNotificationClickListener);
345 
346         mNotificationDataManager.setOnUnseenCountUpdateListener(() -> {
347             if (mUnseenCountUpdateListener != null) {
348                 // Don't show unseen markers for <= LOW importance notifications to be consistent
349                 // with how these notifications are handled on phones
350                 int unseenCount =
351                         mNotificationDataManager.getNonLowImportanceUnseenNotificationCount(
352                                 mCarNotificationListener.getCurrentRanking());
353                 mUnseenCountUpdateListener.onUnseenCountUpdate(unseenCount);
354             }
355             if (isPanelExpanded()) {
356                 // only report the seen notifications when the panel is expanded
357                 mCarNotificationListener.setNotificationsShown(
358                         mNotificationDataManager.getSeenNotifications());
359             }
360             // This logs both when the notification panel is expanded and when the notification
361             // panel is scrolled.
362             mNotificationVisibilityLogger.log(isPanelExpanded());
363         });
364 
365         mNotificationView.setClickHandlerFactory(mNotificationClickHandlerFactory);
366         mNotificationViewController = new NotificationViewController(
367                 mNotificationView,
368                 PreprocessingManager.getInstance(mContext),
369                 mCarNotificationListener,
370                 mCarUxRestrictionManagerWrapper);
371 
372         if (!mOnConnectListenerAdded) {
373             mCarServiceProvider.addListener(mCarConnectedListener);
374             mOnConnectListenerAdded = true;
375         }
376     }
377 
setupNotificationPanel()378     private void setupNotificationPanel() {
379         View glassPane = mNotificationView.findViewById(R.id.glass_pane);
380         mNotificationList = mNotificationView.findViewById(R.id.notifications);
381         GestureDetector closeGestureDetector = new GestureDetector(mContext,
382                 new CloseGestureListener() {
383                     @Override
384                     protected void close() {
385                         if (isPanelExpanded()) {
386                             animateCollapsePanel();
387                         }
388                     }
389                 });
390 
391         // The glass pane is used to view touch events before passed to the notification list.
392         // This allows us to initialize gesture listeners and detect when to close the notifications
393         glassPane.setOnTouchListener((v, event) -> {
394             if (isClosingAction(event)) {
395                 mNotificationListAtEndAtTimeOfTouch = false;
396             }
397             if (isOpeningAction(event)) {
398                 mFirstTouchDownOnGlassPane = event.getRawX();
399                 mNotificationListAtEndAtTimeOfTouch = mNotificationListAtEnd;
400                 // Reset the tracker when there is a touch down on the glass pane.
401                 setIsTracking(false);
402                 // Pass the down event to gesture detector so that it knows where the touch event
403                 // started.
404                 closeGestureDetector.onTouchEvent(event);
405             }
406             return false;
407         });
408 
409         mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() {
410             @Override
411             public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
412                 super.onScrolled(recyclerView, dx, dy);
413                 // Check if we can scroll vertically in the animation direction.
414                 if (!mNotificationList.canScrollVertically(mAnimateDirection)) {
415                     mNotificationListAtEnd = true;
416                     return;
417                 }
418                 mNotificationListAtEnd = false;
419                 mIsSwipingVerticallyToClose = false;
420                 mNotificationListAtEndAtTimeOfTouch = false;
421             }
422         });
423 
424         mNotificationList.setOnTouchListener((v, event) -> {
425             mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX())
426                     > SWIPE_MAX_OFF_PATH;
427             if (mNotificationListAtEndAtTimeOfTouch && mNotificationListAtEnd) {
428                 // We need to save the state here as if notification card is swiping we will
429                 // change the mNotificationListAtEndAtTimeOfTouch. This is to protect
430                 // closing the notification shade while the notification card is being swiped.
431                 mIsSwipingVerticallyToClose = true;
432             }
433 
434             // If the card is swiping we should not allow the notification shade to close.
435             // Hence setting mNotificationListAtEndAtTimeOfTouch to false will stop that
436             // for us. We are also checking for isTracking() because while swiping the
437             // notification shade to close if the user goes a bit horizontal while swiping
438             // upwards then also this should close.
439             if (mIsNotificationCardSwiping && !isTracking()) {
440                 mNotificationListAtEndAtTimeOfTouch = false;
441             }
442 
443             boolean handled = closeGestureDetector.onTouchEvent(event);
444             boolean isTracking = isTracking();
445             Rect rect = getLayout().getClipBounds();
446             float clippedHeight = 0;
447             if (rect != null) {
448                 clippedHeight = rect.bottom;
449             }
450             if (!handled && isClosingAction(event) && mIsSwipingVerticallyToClose) {
451                 if (getSettleClosePercentage() < getPercentageFromEndingEdge() && isTracking) {
452                     animatePanel(DEFAULT_FLING_VELOCITY, false);
453                 } else if (clippedHeight != getLayout().getHeight() && isTracking) {
454                     // this can be caused when user is at the end of the list and trying to
455                     // fling to top of the list by scrolling down.
456                     animatePanel(DEFAULT_FLING_VELOCITY, true);
457                 }
458             }
459 
460             // Updating the mNotificationListAtEndAtTimeOfTouch state has to be done after
461             // the event has been passed to the closeGestureDetector above, such that the
462             // closeGestureDetector sees the up event before the state has changed.
463             if (isClosingAction(event)) {
464                 mNotificationListAtEndAtTimeOfTouch = false;
465             }
466             return handled || isTracking;
467         });
468     }
469 
470     /** Called when the car power state is changed to ON. */
onCarPowerStateOn()471     public void onCarPowerStateOn() {
472         if (mNotificationClickHandlerFactory != null) {
473             mNotificationClickHandlerFactory.clearAllNotifications(mContext);
474         }
475         mNotificationDataManager.clearAll();
476     }
477 
478     /**
479      * Forwards the call to clear all Notification cache.
480      * Note: This is a blocking call so should not execute any long-running or time-consuming tasks
481      * like storing cache.
482      */
clearCache()483     public void clearCache() {
484         mCarNotificationListener.clearCache();
485     }
486 
487     // OverlayPanelViewController
488 
489     @Override
shouldAnimateCollapsePanel()490     protected boolean shouldAnimateCollapsePanel() {
491         return true;
492     }
493 
494     @Override
onAnimateCollapsePanel()495     protected void onAnimateCollapsePanel() {
496         // no-op
497     }
498 
499     @Override
shouldAnimateExpandPanel()500     protected boolean shouldAnimateExpandPanel() {
501         return mCommandQueue.panelsEnabled();
502     }
503 
504     @Override
onAnimateExpandPanel()505     protected void onAnimateExpandPanel() {
506         mNotificationList.scrollToPosition(0);
507     }
508 
509     @Override
getSettleClosePercentage()510     protected int getSettleClosePercentage() {
511         return mResources.getInteger(R.integer.notification_settle_close_percentage);
512     }
513 
514     @Override
onCollapseAnimationEnd()515     protected void onCollapseAnimationEnd() {
516         mNotificationViewController.onVisibilityChanged(false);
517     }
518 
519     @Override
onExpandAnimationEnd()520     protected void onExpandAnimationEnd() {
521         mNotificationView.setVisibleNotificationsAsSeen();
522         mNotificationViewController.onVisibilityChanged(true);
523     }
524 
525     @Override
onPanelVisible(boolean visible)526     protected void onPanelVisible(boolean visible) {
527         super.onPanelVisible(visible);
528         mUiBgExecutor.execute(() -> {
529             try {
530                 if (visible) {
531                     // When notification panel is open even just a bit, we want to clear
532                     // notification effects.
533                     boolean clearNotificationEffects =
534                             mStatusBarStateController.getState() != StatusBarState.KEYGUARD;
535                     mBarService.onPanelRevealed(clearNotificationEffects,
536                             mNotificationDataManager.getVisibleNotifications().size());
537                 } else {
538                     mBarService.onPanelHidden();
539                 }
540             } catch (RemoteException ex) {
541                 // Won't fail unless the world has ended.
542                 Log.e(TAG, String.format(
543                         "Unable to notify StatusBarService of panel visibility: %s", visible));
544             }
545         });
546 
547     }
548 
549     @Override
onPanelExpanded(boolean expand)550     protected void onPanelExpanded(boolean expand) {
551         super.onPanelExpanded(expand);
552 
553         if (expand && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
554             if (DEBUG) {
555                 Log.v(TAG, "clearing notification effects from setExpandedHeight");
556             }
557             clearNotificationEffects();
558         }
559         if (!expand) {
560             mNotificationVisibilityLogger.log(isPanelExpanded());
561         }
562     }
563 
564     /**
565      * Clear Buzz/Beep/Blink.
566      */
clearNotificationEffects()567     private void clearNotificationEffects() {
568         try {
569             mBarService.clearNotificationEffects();
570         } catch (RemoteException e) {
571             // Won't fail unless the world has ended.
572         }
573     }
574 
575     @Override
onOpenScrollStart()576     protected void onOpenScrollStart() {
577         mNotificationList.scrollToPosition(0);
578     }
579 
580     @Override
onScroll(int y)581     protected void onScroll(int y) {
582         super.onScroll(y);
583 
584         if (mNotificationView.getHeight() > 0) {
585             Drawable background = mNotificationView.getBackground().mutate();
586             background.setAlpha((int) (getBackgroundAlpha(y) * 255));
587             mNotificationView.setBackground(background);
588         }
589     }
590 
591     @Override
shouldAllowClosingScroll()592     protected boolean shouldAllowClosingScroll() {
593         // Unless the notification list is at the end, the panel shouldn't be allowed to
594         // collapse on scroll.
595         return mNotificationListAtEndAtTimeOfTouch;
596     }
597 
598     @Override
getHandleBarViewId()599     protected Integer getHandleBarViewId() {
600         return R.id.handle_bar;
601     }
602 
603     /**
604      * Calculates the alpha value for the background based on how much of the notification
605      * shade is visible to the user. When the notification shade is completely open then
606      * alpha value will be 1.
607      */
getBackgroundAlpha(int y)608     private float getBackgroundAlpha(int y) {
609         float fractionCovered =
610                 ((float) (mAnimateDirection > 0 ? y : mNotificationView.getHeight() - y))
611                         / mNotificationView.getHeight();
612         return mInitialBackgroundAlpha + fractionCovered * mBackgroundAlphaDiff;
613     }
614 
615     /** Sets the unseen count listener. */
setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener)616     public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) {
617         mUnseenCountUpdateListener = listener;
618     }
619 
620     /** Listener that is updated when the number of unseen notifications changes. */
621     public interface OnUnseenCountUpdateListener {
622         /**
623          * This method is automatically called whenever there is an update to the number of unseen
624          * notifications. This method can be extended by OEMs to customize the desired logic.
625          */
onUnseenCountUpdate(int unseenNotificationCount)626         void onUnseenCountUpdate(int unseenNotificationCount);
627     }
628 }
629