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 
332         mCarServiceProvider.addListener(car -> {
333             CarUxRestrictionsManager carUxRestrictionsManager =
334                     (CarUxRestrictionsManager)
335                             car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
336             mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager(
337                     carUxRestrictionsManager);
338 
339             PreprocessingManager preprocessingManager = PreprocessingManager.getInstance(mContext);
340 
341             preprocessingManager.setCarUxRestrictionManagerWrapper(mCarUxRestrictionManagerWrapper);
342 
343             mNotificationViewController = new NotificationViewController(
344                     mNotificationView,
345                     preprocessingManager,
346                     mCarNotificationListener,
347                     mCarUxRestrictionManagerWrapper);
348             mNotificationViewController.enable();
349         });
350     }
351 
setupNotificationPanel()352     private void setupNotificationPanel() {
353         View glassPane = mNotificationView.findViewById(R.id.glass_pane);
354         mNotificationList = mNotificationView.findViewById(R.id.notifications);
355         GestureDetector closeGestureDetector = new GestureDetector(mContext,
356                 new CloseGestureListener() {
357                     @Override
358                     protected void close() {
359                         if (isPanelExpanded()) {
360                             animateCollapsePanel();
361                         }
362                     }
363                 });
364 
365         // The glass pane is used to view touch events before passed to the notification list.
366         // This allows us to initialize gesture listeners and detect when to close the notifications
367         glassPane.setOnTouchListener((v, event) -> {
368             if (isClosingAction(event)) {
369                 mNotificationListAtEndAtTimeOfTouch = false;
370             }
371             if (isOpeningAction(event)) {
372                 mFirstTouchDownOnGlassPane = event.getRawX();
373                 mNotificationListAtEndAtTimeOfTouch = mNotificationListAtEnd;
374                 // Reset the tracker when there is a touch down on the glass pane.
375                 setIsTracking(false);
376                 // Pass the down event to gesture detector so that it knows where the touch event
377                 // started.
378                 closeGestureDetector.onTouchEvent(event);
379             }
380             return false;
381         });
382 
383         mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() {
384             @Override
385             public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
386                 super.onScrolled(recyclerView, dx, dy);
387                 // Check if we can scroll vertically in the animation direction.
388                 if (!mNotificationList.canScrollVertically(mAnimateDirection)) {
389                     mNotificationListAtEnd = true;
390                     return;
391                 }
392                 mNotificationListAtEnd = false;
393                 mIsSwipingVerticallyToClose = false;
394                 mNotificationListAtEndAtTimeOfTouch = false;
395             }
396         });
397 
398         mNotificationList.setOnTouchListener((v, event) -> {
399             mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX())
400                     > SWIPE_MAX_OFF_PATH;
401             if (mNotificationListAtEndAtTimeOfTouch && mNotificationListAtEnd) {
402                 // We need to save the state here as if notification card is swiping we will
403                 // change the mNotificationListAtEndAtTimeOfTouch. This is to protect
404                 // closing the notification shade while the notification card is being swiped.
405                 mIsSwipingVerticallyToClose = true;
406             }
407 
408             // If the card is swiping we should not allow the notification shade to close.
409             // Hence setting mNotificationListAtEndAtTimeOfTouch to false will stop that
410             // for us. We are also checking for isTracking() because while swiping the
411             // notification shade to close if the user goes a bit horizontal while swiping
412             // upwards then also this should close.
413             if (mIsNotificationCardSwiping && !isTracking()) {
414                 mNotificationListAtEndAtTimeOfTouch = false;
415             }
416 
417             boolean handled = closeGestureDetector.onTouchEvent(event);
418             boolean isTracking = isTracking();
419             Rect rect = getLayout().getClipBounds();
420             float clippedHeight = 0;
421             if (rect != null) {
422                 clippedHeight = rect.bottom;
423             }
424             if (!handled && isClosingAction(event) && mIsSwipingVerticallyToClose) {
425                 if (getSettleClosePercentage() < getPercentageFromEndingEdge() && isTracking) {
426                     animatePanel(DEFAULT_FLING_VELOCITY, false);
427                 } else if (clippedHeight != getLayout().getHeight() && isTracking) {
428                     // this can be caused when user is at the end of the list and trying to
429                     // fling to top of the list by scrolling down.
430                     animatePanel(DEFAULT_FLING_VELOCITY, true);
431                 }
432             }
433 
434             // Updating the mNotificationListAtEndAtTimeOfTouch state has to be done after
435             // the event has been passed to the closeGestureDetector above, such that the
436             // closeGestureDetector sees the up event before the state has changed.
437             if (isClosingAction(event)) {
438                 mNotificationListAtEndAtTimeOfTouch = false;
439             }
440             return handled || isTracking;
441         });
442     }
443 
444     /** Called when the car power state is changed to ON. */
onCarPowerStateOn()445     public void onCarPowerStateOn() {
446         if (mNotificationClickHandlerFactory != null) {
447             mNotificationClickHandlerFactory.clearAllNotifications();
448         }
449         mNotificationDataManager.clearAll();
450     }
451 
452     // OverlayPanelViewController
453 
454     @Override
shouldAnimateCollapsePanel()455     protected boolean shouldAnimateCollapsePanel() {
456         return true;
457     }
458 
459     @Override
onAnimateCollapsePanel()460     protected void onAnimateCollapsePanel() {
461         // no-op
462     }
463 
464     @Override
shouldAnimateExpandPanel()465     protected boolean shouldAnimateExpandPanel() {
466         return mCommandQueue.panelsEnabled();
467     }
468 
469     @Override
onAnimateExpandPanel()470     protected void onAnimateExpandPanel() {
471         mNotificationList.scrollToPosition(0);
472     }
473 
474     @Override
getSettleClosePercentage()475     protected int getSettleClosePercentage() {
476         return mResources.getInteger(R.integer.notification_settle_close_percentage);
477     }
478 
479     @Override
onCollapseAnimationEnd()480     protected void onCollapseAnimationEnd() {
481         mNotificationViewController.onVisibilityChanged(false);
482     }
483 
484     @Override
onExpandAnimationEnd()485     protected void onExpandAnimationEnd() {
486         mNotificationView.setVisibleNotificationsAsSeen();
487         mNotificationViewController.onVisibilityChanged(true);
488     }
489 
490     @Override
onPanelVisible(boolean visible)491     protected void onPanelVisible(boolean visible) {
492         super.onPanelVisible(visible);
493         mUiBgExecutor.execute(() -> {
494             try {
495                 if (visible) {
496                     // When notification panel is open even just a bit, we want to clear
497                     // notification effects.
498                     boolean clearNotificationEffects =
499                             mStatusBarStateController.getState() != StatusBarState.KEYGUARD;
500                     mBarService.onPanelRevealed(clearNotificationEffects,
501                             mNotificationDataManager.getVisibleNotifications().size());
502                 } else {
503                     mBarService.onPanelHidden();
504                 }
505             } catch (RemoteException ex) {
506                 // Won't fail unless the world has ended.
507                 Log.e(TAG, String.format(
508                         "Unable to notify StatusBarService of panel visibility: %s", visible));
509             }
510         });
511 
512     }
513 
514     @Override
onPanelExpanded(boolean expand)515     protected void onPanelExpanded(boolean expand) {
516         super.onPanelExpanded(expand);
517 
518         if (expand && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
519             if (DEBUG) {
520                 Log.v(TAG, "clearing notification effects from setExpandedHeight");
521             }
522             clearNotificationEffects();
523         }
524         if (!expand) {
525             mNotificationVisibilityLogger.log(isPanelExpanded());
526         }
527     }
528 
529     /**
530      * Clear Buzz/Beep/Blink.
531      */
clearNotificationEffects()532     private void clearNotificationEffects() {
533         try {
534             mBarService.clearNotificationEffects();
535         } catch (RemoteException e) {
536             // Won't fail unless the world has ended.
537         }
538     }
539 
540     @Override
onOpenScrollStart()541     protected void onOpenScrollStart() {
542         mNotificationList.scrollToPosition(0);
543     }
544 
545     @Override
onScroll(int y)546     protected void onScroll(int y) {
547         super.onScroll(y);
548 
549         if (mNotificationView.getHeight() > 0) {
550             Drawable background = mNotificationView.getBackground().mutate();
551             background.setAlpha((int) (getBackgroundAlpha(y) * 255));
552             mNotificationView.setBackground(background);
553         }
554     }
555 
556     @Override
shouldAllowClosingScroll()557     protected boolean shouldAllowClosingScroll() {
558         // Unless the notification list is at the end, the panel shouldn't be allowed to
559         // collapse on scroll.
560         return mNotificationListAtEndAtTimeOfTouch;
561     }
562 
563     @Override
getHandleBarViewId()564     protected Integer getHandleBarViewId() {
565         return R.id.handle_bar;
566     }
567 
568     /**
569      * Calculates the alpha value for the background based on how much of the notification
570      * shade is visible to the user. When the notification shade is completely open then
571      * alpha value will be 1.
572      */
getBackgroundAlpha(int y)573     private float getBackgroundAlpha(int y) {
574         float fractionCovered =
575                 ((float) (mAnimateDirection > 0 ? y : mNotificationView.getHeight() - y))
576                         / mNotificationView.getHeight();
577         return mInitialBackgroundAlpha + fractionCovered * mBackgroundAlphaDiff;
578     }
579 
580     /** Sets the unseen count listener. */
setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener)581     public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) {
582         mUnseenCountUpdateListener = listener;
583     }
584 
585     /** Listener that is updated when the number of unseen notifications changes. */
586     public interface OnUnseenCountUpdateListener {
587         /**
588          * This method is automatically called whenever there is an update to the number of unseen
589          * notifications. This method can be extended by OEMs to customize the desired logic.
590          */
onUnseenCountUpdate(int unseenNotificationCount)591         void onUnseenCountUpdate(int unseenNotificationCount);
592     }
593 }
594