From bb7dc5afcb5085e4f0e733fbd0e250ec2779563f Mon Sep 17 00:00:00 2001 From: Caitlin Cassidy Date: Tue, 28 Dec 2021 16:53:34 +0000 Subject: [PATCH] [Status Bar Refactor] Pass status bar view's controller to NotificationShadeWindowViewController instead of the view itself. Bug: 209005990 Test: new NotificationShadeWindowViewControllerTest Change-Id: I6303fb934701a88f7ce52e5e73e8d3df5f18fed8 --- ...NotificationShadeWindowViewController.java | 28 +-- .../phone/PhoneStatusBarViewController.kt | 25 ++ .../systemui/statusbar/phone/StatusBar.java | 3 +- ...tificationShadeWindowViewControllerTest.kt | 238 ++++++++++++++++++ .../phone/PhoneStatusBarViewControllerTest.kt | 84 ++++++- 5 files changed, 353 insertions(+), 25 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewControllerTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java index a6980a45aa27c..879e6943dce08 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import android.app.StatusBarManager; -import android.graphics.RectF; import android.hardware.display.AmbientDisplayConfiguration; import android.media.AudioManager; import android.media.session.MediaSessionLegacyHelper; @@ -75,7 +74,7 @@ public class NotificationShadeWindowViewController { private boolean mTouchCancelled; private boolean mExpandAnimationRunning; private NotificationStackScrollLayout mStackScrollLayout; - private PhoneStatusBarView mStatusBarView; + private PhoneStatusBarViewController mStatusBarViewController; private StatusBar mService; private NotificationShadeWindowController mNotificationShadeWindowController; private DragDownHelper mDragDownHelper; @@ -86,8 +85,6 @@ public class NotificationShadeWindowViewController { private final NotificationPanelViewController mNotificationPanelViewController; private final PanelExpansionStateManager mPanelExpansionStateManager; - // Used for determining view / touch intersection - private final RectF mTempRect = new RectF(); private boolean mIsTrackingBarGesture = false; @Inject @@ -175,7 +172,7 @@ public class NotificationShadeWindowViewController { mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() { @Override public Boolean handleDispatchTouchEvent(MotionEvent ev) { - if (mStatusBarView == null) { + if (mStatusBarViewController == null) { // Fix for b/192490822 Log.w(TAG, "Ignoring touch while statusBarView not yet set."); return false; } @@ -243,27 +240,27 @@ public class NotificationShadeWindowViewController { expandingBelowNotch = true; } if (expandingBelowNotch) { - return mStatusBarView.dispatchTouchEvent(ev); + return mStatusBarViewController.sendTouchToView(ev); } if (!mIsTrackingBarGesture && isDown && mNotificationPanelViewController.isFullyCollapsed()) { float x = ev.getRawX(); float y = ev.getRawY(); - if (isIntersecting(mStatusBarView, x, y)) { + if (mStatusBarViewController.touchIsWithinView(x, y)) { if (mService.isSameStatusBarState(WINDOW_STATE_SHOWING)) { mIsTrackingBarGesture = true; - return mStatusBarView.dispatchTouchEvent(ev); + return mStatusBarViewController.sendTouchToView(ev); } else { // it's hidden or hiding, don't send to notification shade. return true; } } } else if (mIsTrackingBarGesture) { - final boolean sendToNotification = mStatusBarView.dispatchTouchEvent(ev); + final boolean sendToStatusBar = mStatusBarViewController.sendTouchToView(ev); if (isUp || isCancel) { mIsTrackingBarGesture = false; } - return sendToNotification; + return sendToStatusBar; } return null; @@ -442,8 +439,8 @@ public class NotificationShadeWindowViewController { } } - public void setStatusBarView(PhoneStatusBarView statusBarView) { - mStatusBarView = statusBarView; + public void setStatusBarViewController(PhoneStatusBarViewController statusBarViewController) { + mStatusBarViewController = statusBarViewController; } public void setService(StatusBar statusBar, NotificationShadeWindowController controller) { @@ -455,11 +452,4 @@ public class NotificationShadeWindowViewController { void setDragDownHelper(DragDownHelper dragDownHelper) { mDragDownHelper = dragDownHelper; } - - private boolean isIntersecting(View view, float x, float y) { - int[] mTempLocation = view.getLocationOnScreen(); - mTempRect.set(mTempLocation[0], mTempLocation[1], mTempLocation[0] + view.getWidth(), - mTempLocation[1] + view.getHeight()); - return mTempRect.contains(x, y); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index b9386bd01e039..1cb19ab727aaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone import android.content.res.Configuration import android.graphics.Point +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver @@ -92,6 +93,30 @@ class PhoneStatusBarViewController private constructor( mView.importantForAccessibility = mode } + /** + * Sends a touch event to the status bar view. + * + * This is required in certain cases because the status bar view is in a separate window from + * the rest of SystemUI, and other windows may decide that their touch should instead be treated + * as a status bar window touch. + */ + fun sendTouchToView(ev: MotionEvent): Boolean { + return mView.dispatchTouchEvent(ev) + } + + /** + * Returns true if the given (x, y) point (in screen coordinates) is within the status bar + * view's range and false otherwise. + */ + fun touchIsWithinView(x: Float, y: Float): Boolean { + val left = mView.locationOnScreen[0] + val top = mView.locationOnScreen[1] + return left <= x && + x <= left + mView.width && + top <= y && + y <= top + mView.height + } + class StatusBarViewsCenterProvider : UnfoldMoveFromCenterAnimator.ViewCenterProvider { override fun getViewCenter(view: View, outPoint: Point) = when (view.id) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 07914cf1d9c4b..8fe03e46e25dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -1133,7 +1133,8 @@ public class StatusBar extends CoreStartable implements mStatusBarView = statusBarView; mPhoneStatusBarViewController = statusBarViewController; mStatusBarTransitions = statusBarTransitions; - mNotificationShadeWindowViewController.setStatusBarView(mStatusBarView); + mNotificationShadeWindowViewController + .setStatusBarViewController(mPhoneStatusBarViewController); // Ensure we re-propagate panel expansion values to the panel controller and // any listeners it may have, such as PanelBar. This will also ensure we // re-display the notification panel if necessary (for example, if diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewControllerTest.kt new file mode 100644 index 0000000000000..adb76e16507db --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewControllerTest.kt @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2021 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.statusbar.phone + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.MotionEvent +import androidx.test.filters.SmallTest +import com.android.keyguard.LockIconViewController +import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingCollectorFake +import com.android.systemui.dock.DockManager +import com.android.systemui.statusbar.LockscreenShadeTransitionController +import com.android.systemui.statusbar.NotificationShadeDepthController +import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.phone.NotificationShadeWindowView.InteractionEventHandler +import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager +import com.android.systemui.tuner.TunerService +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.Mockito.anyFloat +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +@SmallTest +class NotificationShadeWindowViewControllerTest : SysuiTestCase() { + private lateinit var mController: NotificationShadeWindowViewController + + @Mock + private lateinit var mView: NotificationShadeWindowView + @Mock + private lateinit var mTunerService: TunerService + @Mock + private lateinit var mStatusBarStateController: SysuiStatusBarStateController + @Mock + private lateinit var mStatusBar: StatusBar + @Mock + private lateinit var mDockManager: DockManager + @Mock + private lateinit var mNotificationPanelViewController: NotificationPanelViewController + @Mock + private lateinit var mNotificationShadeDepthController: NotificationShadeDepthController + @Mock + private lateinit var mNotificationShadeWindowController: NotificationShadeWindowController + @Mock + private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController + @Mock + private lateinit var mStatusBarKeyguardViewManager: StatusBarKeyguardViewManager + @Mock + private lateinit var mLockscreenShadeTransitionController: LockscreenShadeTransitionController + @Mock + private lateinit var mLockIconViewController: LockIconViewController + @Mock + private lateinit var mPhoneStatusBarViewController: PhoneStatusBarViewController + + private lateinit var mInteractionEventHandlerCaptor: ArgumentCaptor + private lateinit var mInteractionEventHandler: InteractionEventHandler + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(mView.bottom).thenReturn(VIEW_BOTTOM) + + mController = NotificationShadeWindowViewController( + mLockscreenShadeTransitionController, + FalsingCollectorFake(), + mTunerService, + mStatusBarStateController, + mDockManager, + mNotificationShadeDepthController, + mView, + mNotificationPanelViewController, + PanelExpansionStateManager(), + stackScrollLayoutController, + mStatusBarKeyguardViewManager, + mLockIconViewController + ) + mController.setupExpandedStatusBar() + mController.setService(mStatusBar, mNotificationShadeWindowController) + + mInteractionEventHandlerCaptor = + ArgumentCaptor.forClass(InteractionEventHandler::class.java) + verify(mView).setInteractionEventHandler(mInteractionEventHandlerCaptor.capture()) + mInteractionEventHandler = mInteractionEventHandlerCaptor.value + } + + // Note: So far, these tests only cover interactions with the status bar view controller. More + // tests need to be added to test the rest of handleDispatchTouchEvent. + + @Test + fun handleDispatchTouchEvent_nullStatusBarViewController_returnsFalse() { + mController.setStatusBarViewController(null) + + val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv) + + assertThat(returnVal).isFalse() + } + + @Test + fun handleDispatchTouchEvent_downTouchBelowView_sendsTouchToSb() { + mController.setStatusBarViewController(mPhoneStatusBarViewController) + val ev = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0) + whenever(mPhoneStatusBarViewController.sendTouchToView(ev)).thenReturn(true) + + val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(ev) + + verify(mPhoneStatusBarViewController).sendTouchToView(ev) + assertThat(returnVal).isTrue() + } + + @Test + fun handleDispatchTouchEvent_downTouchBelowViewThenAnotherTouch_sendsTouchToSb() { + mController.setStatusBarViewController(mPhoneStatusBarViewController) + val downEvBelow = MotionEvent.obtain( + 0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0 + ) + mInteractionEventHandler.handleDispatchTouchEvent(downEvBelow) + + val nextEvent = MotionEvent.obtain( + 0L, 0L, MotionEvent.ACTION_MOVE, 0f, VIEW_BOTTOM + 5f, 0 + ) + whenever(mPhoneStatusBarViewController.sendTouchToView(nextEvent)).thenReturn(true) + + val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(nextEvent) + + verify(mPhoneStatusBarViewController).sendTouchToView(nextEvent) + assertThat(returnVal).isTrue() + } + + @Test + fun handleDispatchTouchEvent_downAndPanelCollapsedAndInSbBoundAndSbWindowShow_sendsTouchToSb() { + mController.setStatusBarViewController(mPhoneStatusBarViewController) + whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(true) + whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(true) + whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) + .thenReturn(true) + whenever(mPhoneStatusBarViewController.sendTouchToView(downEv)).thenReturn(true) + + val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv) + + verify(mPhoneStatusBarViewController).sendTouchToView(downEv) + assertThat(returnVal).isTrue() + } + + @Test + fun handleDispatchTouchEvent_panelNotCollapsed_returnsNull() { + mController.setStatusBarViewController(mPhoneStatusBarViewController) + whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(true) + whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) + .thenReturn(true) + // Item we're testing + whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(false) + + val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv) + + verify(mPhoneStatusBarViewController, never()).sendTouchToView(downEv) + assertThat(returnVal).isNull() + } + + @Test + fun handleDispatchTouchEvent_touchNotInSbBounds_returnsNull() { + mController.setStatusBarViewController(mPhoneStatusBarViewController) + whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(true) + whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(true) + // Item we're testing + whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) + .thenReturn(false) + + val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv) + + verify(mPhoneStatusBarViewController, never()).sendTouchToView(downEv) + assertThat(returnVal).isNull() + } + + @Test + fun handleDispatchTouchEvent_sbWindowNotShowing_noSendTouchToSbAndReturnsTrue() { + mController.setStatusBarViewController(mPhoneStatusBarViewController) + whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(true) + whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) + .thenReturn(true) + // Item we're testing + whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(false) + + val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv) + + verify(mPhoneStatusBarViewController, never()).sendTouchToView(downEv) + assertThat(returnVal).isTrue() + } + + @Test + fun handleDispatchTouchEvent_downEventSentToSbThenAnotherEvent_sendsTouchToSb() { + mController.setStatusBarViewController(mPhoneStatusBarViewController) + whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(true) + whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(true) + whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat())) + .thenReturn(true) + + // Down event first + mInteractionEventHandler.handleDispatchTouchEvent(downEv) + + // Then another event + val nextEvent = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) + whenever(mPhoneStatusBarViewController.sendTouchToView(nextEvent)).thenReturn(true) + + val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(nextEvent) + + verify(mPhoneStatusBarViewController).sendTouchToView(nextEvent) + assertThat(returnVal).isTrue() + } +} + +private val downEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) +private const val VIEW_BOTTOM = 100 diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 235de1e7cb7a7..c65a6b6cde1af 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -77,9 +77,10 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { val parent = FrameLayout(mContext) // add parent to keep layout params view = LayoutInflater.from(mContext) .inflate(R.layout.status_bar, parent, false) as PhoneStatusBarView + view.setLeftTopRightBottom(VIEW_LEFT, VIEW_TOP, VIEW_RIGHT, VIEW_BOTTOM) } - controller = createController(view) + controller = createAndInitController(view) } @Test @@ -99,8 +100,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { val view = createViewMock() val argumentCaptor = ArgumentCaptor.forClass(OnPreDrawListener::class.java) unfoldConfig.isEnabled = true - controller = createController(view) - controller.init() + controller = createAndInitController(view) verify(view.viewTreeObserver).addOnPreDrawListener(argumentCaptor.capture()) argumentCaptor.value.onPreDraw() @@ -108,6 +108,64 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { verify(moveFromCenterAnimation).onViewsReady(any()) } + @Test + fun touchIsWithinView_inBounds_returnsTrue() { + val view = createViewMockWithScreenLocation() + controller = createAndInitController(view) + + assertThat(controller.touchIsWithinView(VIEW_LEFT + 1f, VIEW_TOP + 1f)).isTrue() + } + + @Test + fun touchIsWithinView_onTopLeftCorner_returnsTrue() { + val view = createViewMockWithScreenLocation() + controller = createAndInitController(view) + + assertThat(controller.touchIsWithinView(VIEW_LEFT.toFloat(), VIEW_TOP.toFloat())).isTrue() + } + + @Test + fun touchIsWithinView_onBottomRightCorner_returnsTrue() { + val view = createViewMockWithScreenLocation() + controller = createAndInitController(view) + + assertThat(controller.touchIsWithinView( + VIEW_RIGHT.toFloat(), VIEW_BOTTOM.toFloat()) + ).isTrue() + } + + @Test + fun touchIsWithinView_xTooSmall_returnsFalse() { + val view = createViewMockWithScreenLocation() + controller = createAndInitController(view) + + assertThat(controller.touchIsWithinView(VIEW_LEFT - 1f, VIEW_TOP + 1f)).isFalse() + } + + @Test + fun touchIsWithinView_xTooLarge_returnsFalse() { + val view = createViewMockWithScreenLocation() + controller = createAndInitController(view) + + assertThat(controller.touchIsWithinView(VIEW_RIGHT + 1f, VIEW_TOP + 1f)).isFalse() + } + + @Test + fun touchIsWithinView_yTooSmall_returnsFalse() { + val view = createViewMockWithScreenLocation() + controller = createAndInitController(view) + + assertThat(controller.touchIsWithinView(VIEW_LEFT + 1f, VIEW_TOP - 1f)).isFalse() + } + + @Test + fun touchIsWithinView_yTooLarge_returnsFalse() { + val view = createViewMockWithScreenLocation() + controller = createAndInitController(view) + + assertThat(controller.touchIsWithinView(VIEW_LEFT + 1f, VIEW_BOTTOM + 1f)).isFalse() + } + private fun createViewMock(): PhoneStatusBarView { val view = spy(view) val viewTreeObserver = mock(ViewTreeObserver::class.java) @@ -116,12 +174,23 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { return view } - private fun createController(view: PhoneStatusBarView): PhoneStatusBarViewController { + private fun createViewMockWithScreenLocation(): PhoneStatusBarView { + val view = spy(view) + val location = IntArray(2) + location[0] = VIEW_LEFT + location[1] = VIEW_TOP + `when`(view.locationOnScreen).thenReturn(location) + return view + } + + private fun createAndInitController(view: PhoneStatusBarView): PhoneStatusBarViewController { return PhoneStatusBarViewController.Factory( Optional.of(sysuiUnfoldComponent), Optional.of(progressProvider), configurationController - ).create(view, touchEventHandler) + ).create(view, touchEventHandler).also { + it.init() + } } private class UnfoldConfig : UnfoldTransitionConfig { @@ -142,3 +211,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { } } } + +private const val VIEW_LEFT = 30 +private const val VIEW_RIGHT = 100 +private const val VIEW_TOP = 40 +private const val VIEW_BOTTOM = 100