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