From df741ac8508c21f84acaa033645caa1cae316eb2 Mon Sep 17 00:00:00 2001 From: Heemin Seog Date: Fri, 27 Mar 2020 09:38:26 -0700 Subject: [PATCH] Generalize notifications logic into a subclass of OverlayViewController Bug: 147826363 Test: manual, atest OverlayPanelViewControllerTest Change-Id: Ifad23f6ff639390077301eafa737fea8517a48d9 --- .../layout/notification_center_activity.xml | 2 - .../layout/notification_panel_container.xml | 3 +- .../res/layout/sysui_overlay_window.xml | 4 +- .../car/CarDeviceProvisionedController.java | 2 +- .../NotificationPanelViewController.java | 544 ++++------------- .../NotificationPanelViewMediator.java | 8 +- .../window/OverlayPanelViewController.java | 563 ++++++++++++++++++ .../car/CarNavigationBarTest.java | 3 +- .../OverlayPanelViewControllerTest.java | 431 ++++++++++++++ .../window/OverlayViewControllerTest.java | 8 +- 10 files changed, 1111 insertions(+), 457 deletions(-) create mode 100644 packages/CarSystemUI/src/com/android/systemui/window/OverlayPanelViewController.java create mode 100644 packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayPanelViewControllerTest.java diff --git a/packages/CarSystemUI/res/layout/notification_center_activity.xml b/packages/CarSystemUI/res/layout/notification_center_activity.xml index 4fef48918c972..0af74c4462a67 100644 --- a/packages/CarSystemUI/res/layout/notification_center_activity.xml +++ b/packages/CarSystemUI/res/layout/notification_center_activity.xml @@ -20,8 +20,6 @@ android:id="@+id/notification_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="invisible" - android:layout_marginBottom="@dimen/navigation_bar_height" android:background="@color/notification_shade_background_color"> + android:layout_height="match_parent" + android:visibility="invisible"/> diff --git a/packages/CarSystemUI/res/layout/sysui_overlay_window.xml b/packages/CarSystemUI/res/layout/sysui_overlay_window.xml index 1b0a211b733d1..067e359e0e49b 100644 --- a/packages/CarSystemUI/res/layout/sysui_overlay_window.xml +++ b/packages/CarSystemUI/res/layout/sysui_overlay_window.xml @@ -22,10 +22,12 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + android:layout="@layout/notification_panel_container" + android:layout_marginBottom="@dimen/navigation_bar_height"/> { - if (!isInflated()) { - getOverlayViewGlobalStateController().inflateView(this); - } - if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) { - return true; - } - - boolean consumed = openGestureDetector.onTouchEvent(event); - if (consumed) { - return true; - } - maybeCompleteAnimation(event); - return true; - }; - - mNavBarNotificationTouchListener = - (v, event) -> { - if (!isInflated()) { - return true; - } - boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event); - if (consumed) { - return true; - } - maybeCompleteAnimation(event); - return true; - }; } @Override @@ -252,14 +167,13 @@ public class NotificationPanelViewController extends OverlayViewController { private void onNotificationViewInflated() { // Find views. mNotificationView = getLayout().findViewById(R.id.notification_view); - View glassPane = mNotificationView.findViewById(R.id.glass_pane); - mHandleBar = mNotificationView.findViewById(R.id.handle_bar); - mNotificationList = mNotificationView.findViewById(R.id.notifications); + setupHandleBar(); + setupNotificationPanel(); mNotificationClickHandlerFactory.registerClickListener((launchResult, alertEntry) -> { if (launchResult == ActivityManager.START_TASK_TO_FRONT || launchResult == ActivityManager.START_SUCCESS) { - animateCollapsePanels(); + animateCollapsePanel(); } }); @@ -269,39 +183,52 @@ public class NotificationPanelViewController extends OverlayViewController { mNotificationDataManager.getUnseenNotificationCount()); } }); + mNotificationClickHandlerFactory.setNotificationDataManager(mNotificationDataManager); mNotificationView.setClickHandlerFactory(mNotificationClickHandlerFactory); mNotificationView.setNotificationDataManager(mNotificationDataManager); - mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - if (!mNotificationList.canScrollVertically(1)) { - mNotificationListAtBottom = true; - return; - } - mNotificationListAtBottom = false; - mIsSwipingVerticallyToClose = false; - mNotificationListAtBottomAtTimeOfTouch = false; - } - }); + mCarServiceProvider.addListener(car -> { + CarUxRestrictionsManager carUxRestrictionsManager = + (CarUxRestrictionsManager) + car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE); + mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager( + carUxRestrictionsManager); - // Attached to the notification ui to detect close request of the notification shade. + mNotificationViewController = new NotificationViewController( + mNotificationView, + PreprocessingManager.getInstance(mContext), + mCarNotificationListener, + mCarUxRestrictionManagerWrapper, + mNotificationDataManager); + mNotificationViewController.enable(); + }); + } + + private void setupHandleBar() { + mHandleBar = mNotificationView.findViewById(R.id.handle_bar); + GestureDetector handleBarCloseNotificationGestureDetector = new GestureDetector(mContext, + new HandleBarCloseGestureListener()); + mHandleBar.setOnTouchListener((v, event) -> { + handleBarCloseNotificationGestureDetector.onTouchEvent(event); + maybeCompleteAnimation(event); + return true; + }); + } + + private void setupNotificationPanel() { + View glassPane = mNotificationView.findViewById(R.id.glass_pane); + mNotificationList = mNotificationView.findViewById(R.id.notifications); GestureDetector closeGestureDetector = new GestureDetector(mContext, - new CloseNotificationGestureListener() { + new CloseGestureListener() { @Override protected void close() { - if (mPanelExpanded) { - animateCollapsePanels(); + if (isPanelExpanded()) { + animateCollapsePanel(); } } }); - // Attached to the Handle bar to close the notification shade - GestureDetector handleBarCloseNotificationGestureDetector = new GestureDetector(mContext, - new HandleBarCloseNotificationGestureListener()); - // The glass pane is used to view touch events before passed to the notification list. // This allows us to initialize gesture listeners and detect when to close the notifications glassPane.setOnTouchListener((v, event) -> { @@ -320,6 +247,21 @@ public class NotificationPanelViewController extends OverlayViewController { return false; }); + mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + // Check if we can scroll vertically downwards. + if (!mNotificationList.canScrollVertically(/* direction= */ 1)) { + mNotificationListAtBottom = true; + return; + } + mNotificationListAtBottom = false; + mIsSwipingVerticallyToClose = false; + mNotificationListAtBottomAtTimeOfTouch = false; + } + }); + mNotificationList.setOnTouchListener((v, event) -> { mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX()) > SWIPE_MAX_OFF_PATH; @@ -341,19 +283,19 @@ public class NotificationPanelViewController extends OverlayViewController { boolean handled = closeGestureDetector.onTouchEvent(event); boolean isTracking = mIsTracking; - Rect rect = mNotificationView.getClipBounds(); + Rect rect = getLayout().getClipBounds(); float clippedHeight = 0; if (rect != null) { clippedHeight = rect.bottom; } if (!handled && event.getActionMasked() == MotionEvent.ACTION_UP && mIsSwipingVerticallyToClose) { - if (mSettleClosePercentage < mPercentageFromBottom && isTracking) { - animateNotificationPanel(DEFAULT_FLING_VELOCITY, false); - } else if (clippedHeight != mNotificationView.getHeight() && isTracking) { + if (getSettleClosePercentage() < getPercentageFromBottom() && isTracking) { + animatePanel(DEFAULT_FLING_VELOCITY, false); + } else if (clippedHeight != getLayout().getHeight() && isTracking) { // this can be caused when user is at the end of the list and trying to // fling to top of the list by scrolling down. - animateNotificationPanel(DEFAULT_FLING_VELOCITY, true); + animatePanel(DEFAULT_FLING_VELOCITY, true); } } @@ -365,28 +307,6 @@ public class NotificationPanelViewController extends OverlayViewController { } return handled || isTracking; }); - - mCarServiceProvider.addListener(car -> { - CarUxRestrictionsManager carUxRestrictionsManager = - (CarUxRestrictionsManager) - car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE); - mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager( - carUxRestrictionsManager); - - mNotificationViewController = new NotificationViewController( - mNotificationView, - PreprocessingManager.getInstance(mContext), - mCarNotificationListener, - mCarUxRestrictionManagerWrapper, - mNotificationDataManager); - mNotificationViewController.enable(); - }); - - mHandleBar.setOnTouchListener((v, event) -> { - handleBarCloseNotificationGestureDetector.onTouchEvent(event); - maybeCompleteAnimation(event); - return true; - }); } /** Called when the car power state is changed to ON. */ @@ -397,139 +317,40 @@ public class NotificationPanelViewController extends OverlayViewController { mNotificationDataManager.clearAll(); } - View.OnTouchListener getTopNavBarNotificationTouchListener() { - return mTopNavBarNotificationTouchListener; + @Override + protected boolean shouldAnimateCollapsePanel() { + return true; } - View.OnTouchListener getNavBarNotificationTouchListener() { - return mNavBarNotificationTouchListener; + @Override + protected void onAnimateCollapsePanel() { + // No op. } - private void maybeCompleteAnimation(MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_UP - && mNotificationView.getVisibility() == View.VISIBLE) { - if (mSettleClosePercentage < mPercentageFromBottom) { - animateNotificationPanel(DEFAULT_FLING_VELOCITY, false); - } else { - animateNotificationPanel(DEFAULT_FLING_VELOCITY, true); - } - } + @Override + protected boolean shouldAnimateExpandPanel() { + return mCommandQueue.panelsEnabled(); } - /** - * Animates the notification shade from one position to other. This is used to either open or - * close the notification shade completely with a velocity. If the animation is to close the - * notification shade this method also makes the view invisible after animation ends. - */ - private void animateNotificationPanel(float velocity, boolean isClosing) { - float to = 0; - if (!isClosing) { - to = mNotificationView.getHeight(); - } - - Rect rect = mNotificationView.getClipBounds(); - if (rect != null && rect.bottom != to) { - float from = rect.bottom; - animate(from, to, velocity, isClosing); - return; - } - - // We will only be here if the shade is being opened programmatically or via button when - // height of the layout was not calculated. - ViewTreeObserver notificationTreeObserver = mNotificationView.getViewTreeObserver(); - notificationTreeObserver.addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - ViewTreeObserver obs = mNotificationView.getViewTreeObserver(); - obs.removeOnGlobalLayoutListener(this); - float to = mNotificationView.getHeight(); - animate(/* from= */ 0, to, velocity, isClosing); - } - }); - } - - private void animateCollapsePanels() { - if (!mPanelExpanded || mNotificationView.getVisibility() == View.INVISIBLE) { - return; - } - getOverlayViewGlobalStateController().setWindowFocusable(false); - animateNotificationPanel(mClosingVelocity, true); - } - - private void animateExpandNotificationsPanel() { - if (!mCommandQueue.panelsEnabled() - || !mCarDeviceProvisionedController.isCurrentUserFullySetup()) { - return; - } - // scroll to top + @Override + protected void onAnimateExpandPanel() { mNotificationList.scrollToPosition(0); - setPanelVisible(true); - mNotificationView.setVisibility(View.VISIBLE); - animateNotificationPanel(mOpeningVelocity, false); - - setPanelExpanded(true); } - private void animate(float from, float to, float velocity, boolean isClosing) { - if (mIsNotificationAnimating) { - return; - } - mIsNotificationAnimating = true; - mIsTracking = true; - ValueAnimator animator = ValueAnimator.ofFloat(from, to); - animator.addUpdateListener( - animation -> { - float animatedValue = (Float) animation.getAnimatedValue(); - setNotificationViewClipBounds((int) animatedValue); - }); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mIsNotificationAnimating = false; - mIsTracking = false; - mOpeningVelocity = DEFAULT_FLING_VELOCITY; - mClosingVelocity = DEFAULT_FLING_VELOCITY; - if (isClosing) { - setPanelVisible(false); - mNotificationView.setVisibility(View.INVISIBLE); - mNotificationView.setClipBounds(null); - mNotificationViewController.onVisibilityChanged(false); - // let the status bar know that the panel is closed - setPanelExpanded(false); - } else { - mNotificationViewController.onVisibilityChanged(true); - // let the status bar know that the panel is open - mNotificationView.setVisibleNotificationsAsSeen(); - setPanelExpanded(true); - } - } - }); - mFlingAnimationUtils.apply(animator, from, to, Math.abs(velocity)); - animator.start(); + @Override + protected void onCollapseAnimationEnd() { + mNotificationViewController.onVisibilityChanged(false); } - /** - * Set the panel view to be visible. - */ - public void setPanelVisible(boolean visible) { - if (visible && !getOverlayViewGlobalStateController().isWindowVisible()) { - getOverlayViewGlobalStateController().setWindowVisible(true); - } - if (!visible && getOverlayViewGlobalStateController().isWindowVisible()) { - getOverlayViewGlobalStateController().setWindowVisible(false); - } - getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE); - getOverlayViewGlobalStateController().setWindowFocusable(visible); + @Override + protected void onExpandAnimationEnd() { + mNotificationViewController.onVisibilityChanged(true); + mNotificationView.setVisibleNotificationsAsSeen(); } - /** - * Set the panel state to expanded. This will expand or collapse the overlay window if - * necessary. - */ - public void setPanelExpanded(boolean expand) { - mPanelExpanded = expand; + @Override + protected void onPanelExpanded(boolean expand) { + super.onPanelExpanded(expand); if (expand && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) { if (DEBUG) { @@ -550,19 +371,19 @@ public class NotificationPanelViewController extends OverlayViewController { } } - private void setNotificationViewClipBounds(int height) { - if (height > mNotificationView.getHeight()) { - height = mNotificationView.getHeight(); - } - Rect clipBounds = new Rect(); - clipBounds.set(0, 0, mNotificationView.getWidth(), height); - // Sets the clip region on the notification list view. - mNotificationView.setClipBounds(clipBounds); + @Override + protected void onOpenScrollStart() { + mNotificationList.scrollToPosition(0); + } + + @Override + protected void onScroll(int height) { if (mHandleBar != null) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mHandleBar.getLayoutParams(); mHandleBar.setTranslationY(height - mHandleBar.getHeight() - lp.bottomMargin); } + if (mNotificationView.getHeight() > 0) { Drawable background = mNotificationView.getBackground().mutate(); background.setAlpha((int) (getBackgroundAlpha(height) * 255)); @@ -570,6 +391,13 @@ public class NotificationPanelViewController extends OverlayViewController { } } + @Override + protected boolean shouldAllowClosingScroll() { + // Unless the notification list is at the bottom, the panel shouldn't be allowed to + // collapse on scroll. + return mNotificationListAtBottomAtTimeOfTouch; + } + /** * Calculates the alpha value for the background based on how much of the notification * shade is visible to the user. When the notification shade is completely open then @@ -580,30 +408,6 @@ public class NotificationPanelViewController extends OverlayViewController { + ((float) height / mNotificationView.getHeight() * mBackgroundAlphaDiff); } - private void calculatePercentageFromBottom(float height) { - if (mNotificationView.getHeight() > 0) { - mPercentageFromBottom = (int) Math.abs( - height / mNotificationView.getHeight() * 100); - } - } - - /** Toggles the visibility of the notification panel. */ - public void toggle() { - if (!isInflated()) { - getOverlayViewGlobalStateController().inflateView(this); - } - if (mPanelExpanded) { - animateCollapsePanels(); - } else { - animateExpandNotificationsPanel(); - } - } - - /** Returns {@code true} if the notification panel is expanded. */ - public boolean isPanelExpanded() { - return mPanelExpanded; - } - /** Sets the unseen count listener. */ public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) { mUnseenCountUpdateListener = listener; @@ -618,155 +422,10 @@ public class NotificationPanelViewController extends OverlayViewController { void onUnseenCountUpdate(int unseenNotificationCount); } - /** - * Only responsible for open hooks. Since once the panel opens it covers all elements - * there is no need to merge with close. - */ - private abstract class OpenNotificationGestureListener extends - GestureDetector.SimpleOnGestureListener { - - @Override - public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, - float distanceY) { - - if (mNotificationView.getVisibility() == View.INVISIBLE) { - // when the on-scroll is called for the first time to open. - mNotificationList.scrollToPosition(0); - } - setPanelVisible(true); - mNotificationView.setVisibility(View.VISIBLE); - - // clips the view for the notification shade when the user scrolls to open. - setNotificationViewClipBounds((int) event2.getRawY()); - - // Initially the scroll starts with height being zero. This checks protects from divide - // by zero error. - calculatePercentageFromBottom(event2.getRawY()); - - mIsTracking = true; - return true; - } - - - @Override - public boolean onFling(MotionEvent event1, MotionEvent event2, - float velocityX, float velocityY) { - if (velocityY > SWIPE_THRESHOLD_VELOCITY) { - mOpeningVelocity = velocityY; - openNotification(); - return true; - } - animateNotificationPanel(DEFAULT_FLING_VELOCITY, true); - - return false; - } - - protected abstract void openNotification(); - } - - /** - * To be installed on the open panel notification panel - */ - private abstract class CloseNotificationGestureListener extends - GestureDetector.SimpleOnGestureListener { - - @Override - public boolean onSingleTapUp(MotionEvent motionEvent) { - if (mPanelExpanded) { - animateNotificationPanel(DEFAULT_FLING_VELOCITY, true); - } - return true; - } - - @Override - public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, - float distanceY) { - // should not clip while scroll to the bottom of the list. - if (!mNotificationListAtBottomAtTimeOfTouch) { - return false; - } - float actualNotificationHeight = - mNotificationView.getHeight() - (event1.getRawY() - event2.getRawY()); - if (actualNotificationHeight > mNotificationView.getHeight()) { - actualNotificationHeight = mNotificationView.getHeight(); - } - if (mNotificationView.getHeight() > 0) { - mPercentageFromBottom = (int) Math.abs( - actualNotificationHeight / mNotificationView.getHeight() * 100); - boolean isUp = distanceY > 0; - - // This check is to figure out if onScroll was called while swiping the card at - // bottom of the list. At that time we should not allow notification shade to - // close. We are also checking for the upwards swipe gesture here because it is - // possible if a user is closing the notification shade and while swiping starts - // to open again but does not fling. At that time we should allow the - // notification shade to close fully or else it would stuck in between. - if (Math.abs(mNotificationView.getHeight() - actualNotificationHeight) - > SWIPE_DOWN_MIN_DISTANCE && isUp) { - setNotificationViewClipBounds((int) actualNotificationHeight); - mIsTracking = true; - } else if (!isUp) { - setNotificationViewClipBounds((int) actualNotificationHeight); - } - } - // if we return true the items in RV won't be scrollable. - return false; - } - - - @Override - public boolean onFling(MotionEvent event1, MotionEvent event2, - float velocityX, float velocityY) { - // should not fling if the touch does not start when view is at the bottom of the list. - if (!mNotificationListAtBottomAtTimeOfTouch) { - return false; - } - if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH - || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) { - // swipe was not vertical or was not fast enough - return false; - } - boolean isUp = velocityY < 0; - if (isUp) { - close(); - return true; - } else { - // we should close the shade - animateNotificationPanel(velocityY, false); - } - return false; - } - - protected abstract void close(); - } - - /** - * To be installed on the nav bars. - */ - private abstract class NavBarCloseNotificationGestureListener extends - CloseNotificationGestureListener { - @Override - public boolean onSingleTapUp(MotionEvent e) { - mClosingVelocity = DEFAULT_FLING_VELOCITY; - if (mPanelExpanded) { - close(); - } - return super.onSingleTapUp(e); - } - - @Override - public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, - float distanceY) { - calculatePercentageFromBottom(event2.getRawY()); - setNotificationViewClipBounds((int) event2.getRawY()); - return true; - } - } - /** * To be installed on the handle bar. */ - private class HandleBarCloseNotificationGestureListener extends + private class HandleBarCloseGestureListener extends GestureDetector.SimpleOnGestureListener { @Override @@ -777,9 +436,8 @@ public class NotificationPanelViewController extends OverlayViewController { // the handle bar we should calculate the height using the diff of event1 and event2. // This will help the notification shade to clip smoothly as the event2 value changes // as event1 value will be fixed. - int clipHeight = - mNotificationView.getHeight() - (int) (event1.getRawY() - event2.getRawY()); - setNotificationViewClipBounds(clipHeight); + int clipHeight = getLayout().getHeight() - (int) (event1.getRawY() - event2.getRawY()); + setViewClipBounds(clipHeight); return true; } } diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java index 110c2ee8854ba..9d71797794b88 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewMediator.java @@ -59,13 +59,13 @@ public class NotificationPanelViewMediator implements OverlayViewMediator, @Override public void registerListeners() { mCarNavigationBarController.registerTopBarTouchListener( - mNotificationPanelViewController.getTopNavBarNotificationTouchListener()); + mNotificationPanelViewController.getDragOpenTouchListener()); mCarNavigationBarController.registerBottomBarTouchListener( - mNotificationPanelViewController.getNavBarNotificationTouchListener()); + mNotificationPanelViewController.getDragCloseTouchListener()); mCarNavigationBarController.registerLeftBarTouchListener( - mNotificationPanelViewController.getNavBarNotificationTouchListener()); + mNotificationPanelViewController.getDragCloseTouchListener()); mCarNavigationBarController.registerRightBarTouchListener( - mNotificationPanelViewController.getNavBarNotificationTouchListener()); + mNotificationPanelViewController.getDragCloseTouchListener()); mCarNavigationBarController.registerNotificationController( new CarNavigationBarController.NotificationsShadeController() { diff --git a/packages/CarSystemUI/src/com/android/systemui/window/OverlayPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/window/OverlayPanelViewController.java new file mode 100644 index 0000000000000..58022f12e58ca --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/window/OverlayPanelViewController.java @@ -0,0 +1,563 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.window; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewTreeObserver; + +import androidx.annotation.CallSuper; + +import com.android.systemui.R; +import com.android.systemui.car.CarDeviceProvisionedController; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.statusbar.FlingAnimationUtils; + +/** + * The {@link OverlayPanelViewController} provides additional dragging animation capabilities to + * {@link OverlayViewController}. + */ +public abstract class OverlayPanelViewController extends OverlayViewController { + + private static final boolean DEBUG = true; + private static final String TAG = "OverlayPanelViewController"; + + // used to calculate how fast to open or close the window + protected static final float DEFAULT_FLING_VELOCITY = 0; + // max time a fling animation takes + protected static final float FLING_ANIMATION_MAX_TIME = 0.5f; + // acceleration rate for the fling animation + protected static final float FLING_SPEED_UP_FACTOR = 0.6f; + + protected static final int SWIPE_DOWN_MIN_DISTANCE = 25; + protected static final int SWIPE_MAX_OFF_PATH = 75; + protected static final int SWIPE_THRESHOLD_VELOCITY = 200; + + private final FlingAnimationUtils mFlingAnimationUtils; + private final CarDeviceProvisionedController mCarDeviceProvisionedController; + private final View.OnTouchListener mDragOpenTouchListener; + private final View.OnTouchListener mDragCloseTouchListener; + + private final int mSettleClosePercentage; + private int mPercentageFromBottom; + + private boolean mPanelVisible; + private boolean mPanelExpanded; + + private float mOpeningVelocity = DEFAULT_FLING_VELOCITY; + private float mClosingVelocity = DEFAULT_FLING_VELOCITY; + + private boolean mIsAnimating; + private boolean mIsTracking; + + public OverlayPanelViewController( + Context context, + @Main Resources resources, + int stubId, + OverlayViewGlobalStateController overlayViewGlobalStateController, + FlingAnimationUtils.Builder flingAnimationUtilsBuilder, + CarDeviceProvisionedController carDeviceProvisionedController + ) { + super(stubId, overlayViewGlobalStateController); + + mFlingAnimationUtils = flingAnimationUtilsBuilder + .setMaxLengthSeconds(FLING_ANIMATION_MAX_TIME) + .setSpeedUpFactor(FLING_SPEED_UP_FACTOR) + .build(); + mCarDeviceProvisionedController = carDeviceProvisionedController; + + mSettleClosePercentage = resources.getInteger( + R.integer.notification_settle_close_percentage); + + // Attached to the top navigation bar (i.e. status bar) to detect pull down of the + // notification shade. + GestureDetector openGestureDetector = new GestureDetector(context, + new OpenGestureListener() { + @Override + protected void open() { + animateExpandPanel(); + } + }); + + // Attached to the NavBars to close the notification shade + GestureDetector navBarCloseNotificationGestureDetector = new GestureDetector(context, + new SystemBarCloseGestureListener() { + @Override + protected void close() { + if (isPanelExpanded()) { + animateCollapsePanel(); + } + } + }); + + mDragOpenTouchListener = (v, event) -> { + if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) { + return true; + } + if (!isInflated()) { + getOverlayViewGlobalStateController().inflateView(this); + } + + boolean consumed = openGestureDetector.onTouchEvent(event); + if (consumed) { + return true; + } + maybeCompleteAnimation(event); + return true; + }; + + mDragCloseTouchListener = (v, event) -> { + if (!isInflated()) { + return true; + } + boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event); + if (consumed) { + return true; + } + maybeCompleteAnimation(event); + return true; + }; + } + + /** Toggles the visibility of the panel. */ + public void toggle() { + if (!isInflated()) { + getOverlayViewGlobalStateController().inflateView(this); + } + if (isPanelExpanded()) { + animateCollapsePanel(); + } else { + animateExpandPanel(); + } + } + + /* ***************************************************************************************** * + * Panel Animation + * ***************************************************************************************** */ + + /** Animates the closing of the panel. */ + protected void animateCollapsePanel() { + if (!shouldAnimateCollapsePanel()) { + return; + } + + if (!isPanelExpanded() || !isPanelVisible()) { + return; + } + + onAnimateCollapsePanel(); + getOverlayViewGlobalStateController().setWindowFocusable(false); + animatePanel(mClosingVelocity, /* isClosing= */ true); + } + + /** Determines whether {@link #animateCollapsePanel()} should collapse the panel. */ + protected abstract boolean shouldAnimateCollapsePanel(); + + /** Called when the panel is beginning to collapse. */ + protected abstract void onAnimateCollapsePanel(); + + /** Animates the expansion of the panel. */ + protected void animateExpandPanel() { + if (!shouldAnimateExpandPanel()) { + return; + } + + if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) { + return; + } + + onAnimateExpandPanel(); + setPanelVisible(true); + animatePanel(mOpeningVelocity, /* isClosing= */ false); + + setPanelExpanded(true); + } + + /** Determines whether {@link #animateExpandPanel()}} should expand the panel. */ + protected abstract boolean shouldAnimateExpandPanel(); + + /** Called when the panel is beginning to expand. */ + protected abstract void onAnimateExpandPanel(); + + /** + * Depending on certain conditions, determines whether to fully expand or collapse the panel. + */ + protected void maybeCompleteAnimation(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_UP + && isPanelVisible()) { + if (mSettleClosePercentage < mPercentageFromBottom) { + animatePanel(DEFAULT_FLING_VELOCITY, false); + } else { + animatePanel(DEFAULT_FLING_VELOCITY, true); + } + } + } + + /** + * Animates the panel from one position to other. This is used to either open or + * close the panel completely with a velocity. If the animation is to close the + * panel this method also makes the view invisible after animation ends. + */ + protected void animatePanel(float velocity, boolean isClosing) { + float to = 0; + if (!isClosing) { + to = getLayout().getHeight(); + } + + Rect rect = getLayout().getClipBounds(); + if (rect != null && rect.bottom != to) { + float from = rect.bottom; + animate(from, to, velocity, isClosing); + return; + } + + // We will only be here if the shade is being opened programmatically or via button when + // height of the layout was not calculated. + ViewTreeObserver notificationTreeObserver = getLayout().getViewTreeObserver(); + notificationTreeObserver.addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + ViewTreeObserver obs = getLayout().getViewTreeObserver(); + obs.removeOnGlobalLayoutListener(this); + float to = getLayout().getHeight(); + animate(/* from= */ 0, to, velocity, isClosing); + } + }); + } + + private void animate(float from, float to, float velocity, boolean isClosing) { + if (mIsAnimating) { + return; + } + mIsAnimating = true; + mIsTracking = true; + ValueAnimator animator = ValueAnimator.ofFloat(from, to); + animator.addUpdateListener( + animation -> { + float animatedValue = (Float) animation.getAnimatedValue(); + setViewClipBounds((int) animatedValue); + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mIsAnimating = false; + mIsTracking = false; + mOpeningVelocity = DEFAULT_FLING_VELOCITY; + mClosingVelocity = DEFAULT_FLING_VELOCITY; + if (isClosing) { + setPanelVisible(false); + getLayout().setClipBounds(null); + onCollapseAnimationEnd(); + setPanelExpanded(false); + } else { + onExpandAnimationEnd(); + setPanelExpanded(true); + } + } + }); + getFlingAnimationUtils().apply(animator, from, to, Math.abs(velocity)); + animator.start(); + } + + /** + * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is + * closing. + */ + protected abstract void onCollapseAnimationEnd(); + + /** + * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is + * opening. + */ + protected abstract void onExpandAnimationEnd(); + + /* ***************************************************************************************** * + * Panel Visibility + * ***************************************************************************************** */ + + /** Set the panel view to be visible. */ + protected final void setPanelVisible(boolean visible) { + mPanelVisible = visible; + onPanelVisible(visible); + } + + /** Returns {@code true} if panel is visible. */ + public final boolean isPanelVisible() { + return mPanelVisible; + } + + /** Business logic run when panel visibility is set. */ + @CallSuper + protected void onPanelVisible(boolean visible) { + if (DEBUG) { + Log.e(TAG, "onPanelVisible: " + visible); + } + + if (visible && !getOverlayViewGlobalStateController().isWindowVisible()) { + getOverlayViewGlobalStateController().setWindowVisible(true); + } + if (!visible && getOverlayViewGlobalStateController().isWindowVisible()) { + getOverlayViewGlobalStateController().setWindowVisible(false); + } + getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + getOverlayViewGlobalStateController().setWindowFocusable(visible); + } + + /* ***************************************************************************************** * + * Panel Expansion + * ***************************************************************************************** */ + + /** + * Set the panel state to expanded. This will expand or collapse the overlay window if + * necessary. + */ + protected final void setPanelExpanded(boolean expand) { + mPanelExpanded = expand; + onPanelExpanded(expand); + } + + /** Returns {@code true} if panel is expanded. */ + public final boolean isPanelExpanded() { + return mPanelExpanded; + } + + @CallSuper + protected void onPanelExpanded(boolean expand) { + if (DEBUG) { + Log.e(TAG, "onPanelExpanded: " + expand); + } + } + + /* ***************************************************************************************** * + * Misc + * ***************************************************************************************** */ + + protected void calculatePercentageFromBottom(float height) { + if (getLayout().getHeight() > 0) { + mPercentageFromBottom = (int) Math.abs( + height / getLayout().getHeight() * 100); + } + } + + protected void setViewClipBounds(int height) { + if (height > getLayout().getHeight()) { + height = getLayout().getHeight(); + } + Rect clipBounds = new Rect(); + clipBounds.set(0, 0, getLayout().getWidth(), height); + getLayout().setClipBounds(clipBounds); + onScroll(height); + } + + /** Called while scrolling. */ + protected abstract void onScroll(int height); + + /* ***************************************************************************************** * + * Getters + * ***************************************************************************************** */ + + /** Returns the open touch listener. */ + public final View.OnTouchListener getDragOpenTouchListener() { + return mDragOpenTouchListener; + } + + /** Returns the close touch listener. */ + public final View.OnTouchListener getDragCloseTouchListener() { + return mDragCloseTouchListener; + } + + /** Gets the fling animation utils used for animating this panel. */ + protected final FlingAnimationUtils getFlingAnimationUtils() { + return mFlingAnimationUtils; + } + + /** Returns {@code true} if the panel is currently tracking. */ + protected final boolean isTracking() { + return mIsTracking; + } + + /** Returns {@code true} if the panel is currently animating. */ + protected final boolean isAnimating() { + return mIsAnimating; + } + + /** Returns the percentage of the panel that is open from the bottom. */ + protected final int getPercentageFromBottom() { + return mPercentageFromBottom; + } + + /** Returns the percentage at which we've determined whether to open or close the panel. */ + protected final int getSettleClosePercentage() { + return mSettleClosePercentage; + } + + /* ***************************************************************************************** * + * Gesture Listeners + * ***************************************************************************************** */ + + /** Called when the user is beginning to scroll down the panel. */ + protected abstract void onOpenScrollStart(); + + /** + * Only responsible for open hooks. Since once the panel opens it covers all elements + * there is no need to merge with close. + */ + protected abstract class OpenGestureListener extends + GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, + float distanceY) { + + if (!isPanelVisible()) { + onOpenScrollStart(); + } + setPanelVisible(true); + + // clips the view for the notification shade when the user scrolls to open. + setViewClipBounds((int) event2.getRawY()); + + // Initially the scroll starts with height being zero. This checks protects from divide + // by zero error. + calculatePercentageFromBottom(event2.getRawY()); + + mIsTracking = true; + return true; + } + + + @Override + public boolean onFling(MotionEvent event1, MotionEvent event2, + float velocityX, float velocityY) { + if (velocityY > SWIPE_THRESHOLD_VELOCITY) { + mOpeningVelocity = velocityY; + open(); + return true; + } + animatePanel(DEFAULT_FLING_VELOCITY, true); + + return false; + } + + protected abstract void open(); + } + + /** Determines whether the scroll event should allow closing of the panel. */ + protected abstract boolean shouldAllowClosingScroll(); + + protected abstract class CloseGestureListener extends + GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onSingleTapUp(MotionEvent motionEvent) { + if (isPanelExpanded()) { + animatePanel(DEFAULT_FLING_VELOCITY, true); + } + return true; + } + + @Override + public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, + float distanceY) { + // should not clip while scroll to the bottom of the list. + if (!shouldAllowClosingScroll()) { + return false; + } + float actualNotificationHeight = + getLayout().getHeight() - (event1.getRawY() - event2.getRawY()); + if (actualNotificationHeight > getLayout().getHeight()) { + actualNotificationHeight = getLayout().getHeight(); + } + if (getLayout().getHeight() > 0) { + mPercentageFromBottom = (int) Math.abs( + actualNotificationHeight / getLayout().getHeight() * 100); + boolean isUp = distanceY > 0; + + // This check is to figure out if onScroll was called while swiping the card at + // bottom of the list. At that time we should not allow notification shade to + // close. We are also checking for the upwards swipe gesture here because it is + // possible if a user is closing the notification shade and while swiping starts + // to open again but does not fling. At that time we should allow the + // notification shade to close fully or else it would stuck in between. + if (Math.abs(getLayout().getHeight() - actualNotificationHeight) + > SWIPE_DOWN_MIN_DISTANCE && isUp) { + setViewClipBounds((int) actualNotificationHeight); + mIsTracking = true; + } else if (!isUp) { + setViewClipBounds((int) actualNotificationHeight); + } + } + // if we return true the items in RV won't be scrollable. + return false; + } + + + @Override + public boolean onFling(MotionEvent event1, MotionEvent event2, + float velocityX, float velocityY) { + // should not fling if the touch does not start when view is at the bottom of the list. + if (!shouldAllowClosingScroll()) { + return false; + } + if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH + || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) { + // swipe was not vertical or was not fast enough + return false; + } + boolean isUp = velocityY < 0; + if (isUp) { + close(); + return true; + } else { + // we should close the shade + animatePanel(velocityY, false); + } + return false; + } + + protected abstract void close(); + } + + protected abstract class SystemBarCloseGestureListener extends CloseGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + mClosingVelocity = DEFAULT_FLING_VELOCITY; + if (isPanelExpanded()) { + close(); + } + return super.onSingleTapUp(e); + } + + @Override + public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, + float distanceY) { + calculatePercentageFromBottom(event2.getRawY()); + setViewClipBounds((int) event2.getRawY()); + return true; + } + } +} diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java index f2748b89c95c5..76557fda69268 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java @@ -20,13 +20,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.Handler; -import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableResources; import android.view.LayoutInflater; import android.view.WindowManager; +import androidx.test.filters.SmallTest; + import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.car.CarDeviceProvisionedController; diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayPanelViewControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayPanelViewControllerTest.java new file mode 100644 index 0000000000000..04f2d06ca71c0 --- /dev/null +++ b/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayPanelViewControllerTest.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.window; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.car.CarDeviceProvisionedController; +import com.android.systemui.statusbar.FlingAnimationUtils; +import com.android.systemui.tests.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class OverlayPanelViewControllerTest extends SysuiTestCase { + private TestOverlayPanelViewController mOverlayPanelViewController; + private ViewGroup mBaseLayout; + + @Mock + private OverlayViewGlobalStateController mOverlayViewGlobalStateController; + @Mock + private FlingAnimationUtils.Builder mFlingAnimationUtilsBuilder; + @Mock + private FlingAnimationUtils mFlingAnimationUtils; + @Mock + private CarDeviceProvisionedController mCarDeviceProvisionedController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate( + R.layout.overlay_view_controller_test, /* root= */ null); + + when(mFlingAnimationUtilsBuilder.setMaxLengthSeconds(anyFloat())).thenReturn( + mFlingAnimationUtilsBuilder); + when(mFlingAnimationUtilsBuilder.setSpeedUpFactor(anyFloat())).thenReturn( + mFlingAnimationUtilsBuilder); + when(mFlingAnimationUtilsBuilder.build()).thenReturn(mFlingAnimationUtils); + mOverlayPanelViewController = new TestOverlayPanelViewController( + getContext(), + getContext().getOrCreateTestableResources().getResources(), + R.id.overlay_view_controller_stub, + mOverlayViewGlobalStateController, + mFlingAnimationUtilsBuilder, + mCarDeviceProvisionedController); + } + + @Test + public void toggle_notInflated_inflates() { + assertThat(mOverlayPanelViewController.isInflated()).isFalse(); + + mOverlayPanelViewController.toggle(); + + verify(mOverlayViewGlobalStateController).inflateView(mOverlayPanelViewController); + } + + @Test + public void toggle_inflated_doesNotInflate() { + mOverlayPanelViewController.inflate(mBaseLayout); + assertThat(mOverlayPanelViewController.isInflated()).isTrue(); + + mOverlayPanelViewController.toggle(); + + verify(mOverlayViewGlobalStateController, never()).inflateView(mOverlayPanelViewController); + } + + @Test + public void toggle_notExpanded_panelExpands() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setPanelExpanded(false); + + mOverlayPanelViewController.toggle(); + + assertThat(mOverlayPanelViewController.mAnimateExpandPanelCalled).isTrue(); + } + + @Test + public void toggle_expanded_panelCollapses() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setPanelExpanded(true); + + mOverlayPanelViewController.toggle(); + + assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue(); + } + + @Test + public void animateCollapsePanel_shouldNotAnimateCollapsePanel_doesNotCollapse() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateCollapsePanel(false); + + mOverlayPanelViewController.animateCollapsePanel(); + + assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue(); + assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isFalse(); + } + + @Test + public void animateCollapsePanel_isNotExpanded_doesNotCollapse() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateCollapsePanel(true); + mOverlayPanelViewController.setPanelExpanded(false); + + mOverlayPanelViewController.animateCollapsePanel(); + + assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue(); + assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isFalse(); + } + + @Test + public void animateCollapsePanel_isNotVisible_doesNotCollapse() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateCollapsePanel(true); + mOverlayPanelViewController.setPanelExpanded(true); + mOverlayPanelViewController.setPanelVisible(false); + + mOverlayPanelViewController.animateCollapsePanel(); + + assertThat(mOverlayPanelViewController.mAnimateCollapsePanelCalled).isTrue(); + assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isFalse(); + } + + @Test + public void animateCollapsePanel_collapses() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateCollapsePanel(true); + mOverlayPanelViewController.setPanelExpanded(true); + mOverlayPanelViewController.setPanelVisible(true); + + mOverlayPanelViewController.animateCollapsePanel(); + + assertThat(mOverlayPanelViewController.mOnAnimateCollapsePanelCalled).isTrue(); + } + + @Test + public void animateCollapsePanel_removesWindowFocus() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateCollapsePanel(true); + mOverlayPanelViewController.setPanelExpanded(true); + mOverlayPanelViewController.setPanelVisible(true); + + mOverlayPanelViewController.animateCollapsePanel(); + + verify(mOverlayViewGlobalStateController).setWindowFocusable(false); + } + + @Test + public void animateExpandPanel_shouldNotAnimateExpandPanel_doesNotExpand() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateExpandPanel(false); + + mOverlayPanelViewController.animateExpandPanel(); + + assertThat(mOverlayPanelViewController.mAnimateExpandPanelCalled).isTrue(); + assertThat(mOverlayPanelViewController.mOnAnimateExpandPanelCalled).isFalse(); + } + + @Test + public void animateExpandPanel_userNotSetup_doesNotExpand() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateExpandPanel(true); + when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(false); + + mOverlayPanelViewController.animateExpandPanel(); + + assertThat(mOverlayPanelViewController.mAnimateExpandPanelCalled).isTrue(); + assertThat(mOverlayPanelViewController.mOnAnimateExpandPanelCalled).isFalse(); + } + + @Test + public void animateExpandPanel_expands() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateExpandPanel(true); + when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true); + + mOverlayPanelViewController.animateExpandPanel(); + + assertThat(mOverlayPanelViewController.mOnAnimateExpandPanelCalled).isTrue(); + } + + @Test + public void animateExpandPanel_setsPanelVisible() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateExpandPanel(true); + when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true); + + mOverlayPanelViewController.animateExpandPanel(); + + assertThat(mOverlayPanelViewController.isPanelVisible()).isTrue(); + } + + @Test + public void animateExpandPanel_setsPanelExpanded() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setShouldAnimateExpandPanel(true); + when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true); + + mOverlayPanelViewController.animateExpandPanel(); + + assertThat(mOverlayPanelViewController.isPanelExpanded()).isTrue(); + } + + @Test + public void setPanelVisible_setTrue_windowNotVisible_setsWindowVisible() { + mOverlayPanelViewController.inflate(mBaseLayout); + when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(false); + + mOverlayPanelViewController.setPanelVisible(true); + + verify(mOverlayViewGlobalStateController).setWindowVisible(true); + } + + @Test + public void setPanelVisible_setTrue_windowVisible_doesNotSetWindowVisible() { + mOverlayPanelViewController.inflate(mBaseLayout); + when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(true); + + mOverlayPanelViewController.setPanelVisible(true); + + verify(mOverlayViewGlobalStateController, never()).setWindowVisible(true); + } + + @Test + public void setPanelVisible_setTrue_setLayoutVisible() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.getLayout().setVisibility(View.INVISIBLE); + + mOverlayPanelViewController.setPanelVisible(true); + + assertThat(mOverlayPanelViewController.getLayout().getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void setPanelVisible_setTrue_setWindowFocusable() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.setPanelVisible(true); + + verify(mOverlayViewGlobalStateController).setWindowFocusable(true); + } + + @Test + public void setPanelVisible_setFalse_windowVisible_setsWindowNotVisible() { + mOverlayPanelViewController.inflate(mBaseLayout); + when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(true); + + mOverlayPanelViewController.setPanelVisible(false); + + verify(mOverlayViewGlobalStateController).setWindowVisible(false); + } + + @Test + public void setPanelVisible_setFalse_windowNotVisible_doesNotSetWindowNotVisible() { + mOverlayPanelViewController.inflate(mBaseLayout); + when(mOverlayViewGlobalStateController.isWindowVisible()).thenReturn(false); + + mOverlayPanelViewController.setPanelVisible(false); + + verify(mOverlayViewGlobalStateController, never()).setWindowVisible(false); + } + + @Test + public void setPanelVisible_setFalse_setLayoutInvisible() { + mOverlayPanelViewController.inflate(mBaseLayout); + mOverlayPanelViewController.getLayout().setVisibility(View.VISIBLE); + + mOverlayPanelViewController.setPanelVisible(false); + + assertThat(mOverlayPanelViewController.getLayout().getVisibility()).isEqualTo( + View.INVISIBLE); + } + + @Test + public void setPanelVisible_setFalse_setWindowNotFocusable() { + mOverlayPanelViewController.inflate(mBaseLayout); + + mOverlayPanelViewController.setPanelVisible(false); + + verify(mOverlayViewGlobalStateController).setWindowFocusable(false); + } + + @Test + public void dragOpenTouchListener_isNotInflated_inflatesView() { + when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true); + assertThat(mOverlayPanelViewController.isInflated()).isFalse(); + + mOverlayPanelViewController.getDragOpenTouchListener().onTouch(/* v= */ null, + MotionEvent.obtain(/* downTime= */ 200, /* eventTime= */ 300, + MotionEvent.ACTION_MOVE, /* x= */ 0, /* y= */ 0, /* metaState= */ 0)); + + verify(mOverlayViewGlobalStateController).inflateView(mOverlayPanelViewController); + } + + private static class TestOverlayPanelViewController extends OverlayPanelViewController { + + private boolean mShouldAnimateCollapsePanel; + private boolean mShouldAnimateExpandPanel; + private boolean mShouldAllowClosingScroll; + + boolean mOnAnimateCollapsePanelCalled; + boolean mAnimateCollapsePanelCalled; + boolean mOnAnimateExpandPanelCalled; + boolean mAnimateExpandPanelCalled; + boolean mOnCollapseAnimationEndCalled; + boolean mOnExpandAnimationEndCalled; + boolean mOnOpenScrollStartEnd; + List mOnScrollHeights; + + TestOverlayPanelViewController( + Context context, + Resources resources, + int stubId, + OverlayViewGlobalStateController overlayViewGlobalStateController, + FlingAnimationUtils.Builder flingAnimationUtilsBuilder, + CarDeviceProvisionedController carDeviceProvisionedController) { + super(context, resources, stubId, overlayViewGlobalStateController, + flingAnimationUtilsBuilder, + carDeviceProvisionedController); + + mOnScrollHeights = new ArrayList<>(); + } + + public void setShouldAnimateCollapsePanel(boolean shouldAnimate) { + mShouldAnimateCollapsePanel = shouldAnimate; + } + + @Override + protected boolean shouldAnimateCollapsePanel() { + return mShouldAnimateCollapsePanel; + } + + @Override + protected void animateCollapsePanel() { + super.animateCollapsePanel(); + mAnimateCollapsePanelCalled = true; + } + + @Override + protected void onAnimateCollapsePanel() { + mOnAnimateCollapsePanelCalled = true; + } + + public void setShouldAnimateExpandPanel(boolean shouldAnimate) { + mShouldAnimateExpandPanel = shouldAnimate; + } + + @Override + protected boolean shouldAnimateExpandPanel() { + return mShouldAnimateExpandPanel; + } + + @Override + protected void animateExpandPanel() { + super.animateExpandPanel(); + mAnimateExpandPanelCalled = true; + } + + @Override + protected void onAnimateExpandPanel() { + mOnAnimateExpandPanelCalled = true; + } + + @Override + protected void onCollapseAnimationEnd() { + mOnCollapseAnimationEndCalled = true; + } + + @Override + protected void onExpandAnimationEnd() { + mOnExpandAnimationEndCalled = true; + } + + @Override + protected void onScroll(int height) { + mOnScrollHeights.add(height); + } + + @Override + protected void onOpenScrollStart() { + mOnOpenScrollStartEnd = true; + } + + public void setShouldAllowClosingScroll(boolean shouldAllow) { + mShouldAllowClosingScroll = shouldAllow; + } + + @Override + protected boolean shouldAllowClosingScroll() { + return mShouldAllowClosingScroll; + } + } +} diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayViewControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayViewControllerTest.java index 3be962627f622..331326168ba4b 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayViewControllerTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/window/OverlayViewControllerTest.java @@ -43,7 +43,7 @@ import org.mockito.MockitoAnnotations; @TestableLooper.RunWithLooper @SmallTest public class OverlayViewControllerTest extends SysuiTestCase { - private MockOverlayViewController mOverlayViewController; + private TestOverlayViewController mOverlayViewController; private ViewGroup mBaseLayout; @Mock @@ -56,7 +56,7 @@ public class OverlayViewControllerTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(/* testClass= */ this); - mOverlayViewController = new MockOverlayViewController(R.id.overlay_view_controller_stub, + mOverlayViewController = new TestOverlayViewController(R.id.overlay_view_controller_stub, mOverlayViewGlobalStateController); mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate( @@ -130,12 +130,12 @@ public class OverlayViewControllerTest extends SysuiTestCase { assertThat(mOverlayViewController.mHideInternalCalled).isFalse(); } - private static class MockOverlayViewController extends OverlayViewController { + private static class TestOverlayViewController extends OverlayViewController { boolean mOnFinishInflateCalled = false; boolean mShowInternalCalled = false; boolean mHideInternalCalled = false; - MockOverlayViewController(int stubId, + TestOverlayViewController(int stubId, OverlayViewGlobalStateController overlayViewGlobalStateController) { super(stubId, overlayViewGlobalStateController); }