diff options
17 files changed, 840 insertions, 510 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java index 04c4efbf7c78..fefe5a011358 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java @@ -149,7 +149,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { mUiEventLogger); when(mScrimManager.getCurrentController()).thenReturn(mScrimController); - when(mCentralSurfaces.isBouncerShowing()).thenReturn(false); when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator); when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker); when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn(Float.MAX_VALUE); @@ -193,11 +192,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { 2)).isTrue(); } - private enum Direction { - DOWN, - UP, - } - @Test public void testSwipeUp_whenBouncerInitiallyShowing_reduceHeightWithExclusionRects() { mTouchHandler.getTouchInitiationRegion(SCREEN_BOUNDS, mRegion, @@ -210,7 +204,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { SCREEN_HEIGHT_PX * MIN_BOUNCER_HEIGHT; final int minAllowableBottom = SCREEN_HEIGHT_PX - Math.round(minBouncerHeight); - expected.set(0, minAllowableBottom , SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX); + expected.set(0, minAllowableBottom, SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX); assertThat(bounds).isEqualTo(expected); @@ -278,69 +272,11 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { } /** - * Makes sure swiping up when bouncer initially showing doesn't change the expansion amount. - */ - @DisableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING) - @Test - public void testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - - mTouchHandler.onSessionStart(mTouchSession); - ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = - ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); - verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture()); - - final OnGestureListener gestureListener = gestureListenerCaptor.getValue(); - - final float percent = .3f; - final float distanceY = SCREEN_HEIGHT_PX * percent; - - // Swiping up near the top of the screen where the touch initiation region is. - final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, distanceY, 0); - final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, 0, 0); - - assertThat(gestureListener.onScroll(event1, event2, 0, distanceY)).isTrue(); - - verify(mScrimController, never()).expand(any()); - } - - /** - * Makes sure swiping up when bouncer initially showing doesn't change the expansion amount. - */ - @Test - @EnableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING) - public void testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion_directionFiltering() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - - mTouchHandler.onSessionStart(mTouchSession); - ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = - ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); - verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture()); - - final OnGestureListener gestureListener = gestureListenerCaptor.getValue(); - - final float percent = .3f; - final float distanceY = SCREEN_HEIGHT_PX * percent; - - // Swiping up near the top of the screen where the touch initiation region is. - final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, distanceY, 0); - final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, 0, 0); - - assertThat(gestureListener.onScroll(event1, event2, 0, distanceY)).isFalse(); - - verify(mScrimController, never()).expand(any()); - } - - /** - * Makes sure swiping down when bouncer initially hidden doesn't change the expansion amount. + * Makes sure swiping down doesn't change the expansion amount. */ @Test @DisableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING) - public void testSwipeDown_whenBouncerInitiallyHidden_doesNotSetExpansion() { + public void testSwipeDown_doesNotSetExpansion() { mTouchHandler.onSessionStart(mTouchSession); ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); @@ -401,34 +337,8 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final OnGestureListener gestureListener = gestureListenerCaptor.getValue(); - verifyScroll(.3f, Direction.UP, false, gestureListener); - - // Ensure that subsequent gestures are treated as expanding even if the bouncer state - // changes. - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - verifyScroll(.7f, Direction.UP, false, gestureListener); - } - - /** - * Makes sure the expansion amount is proportional to scroll. - */ - @Test - public void testSwipeDown_setsCorrectExpansionAmount() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - - mTouchHandler.onSessionStart(mTouchSession); - ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = - ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); - verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture()); - - final OnGestureListener gestureListener = gestureListenerCaptor.getValue(); - - verifyScroll(.3f, Direction.DOWN, true, gestureListener); - - // Ensure that subsequent gestures are treated as collapsing even if the bouncer state - // changes. - when(mCentralSurfaces.isBouncerShowing()).thenReturn(false); - verifyScroll(.7f, Direction.DOWN, true, gestureListener); + verifyScroll(.3f, gestureListener); + verifyScroll(.7f, gestureListener); } /** @@ -493,25 +403,24 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { verify(mCentralSurfaces, never()).awakenDreams(); } - private void verifyScroll(float percent, Direction direction, - boolean isBouncerInitiallyShowing, GestureDetector.OnGestureListener gestureListener) { + private void verifyScroll(float percent, + OnGestureListener gestureListener) { final float distanceY = SCREEN_HEIGHT_PX * percent; final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0); + 0, SCREEN_HEIGHT_PX, 0); final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0); + 0, SCREEN_HEIGHT_PX - distanceY, 0); reset(mScrimController); assertThat(gestureListener.onScroll(event1, event2, 0, - direction == Direction.UP ? distanceY : -distanceY)) + distanceY)) .isTrue(); // Ensure only called once verify(mScrimController).expand(any()); - final float expansion = isBouncerInitiallyShowing ? percent : 1 - percent; - final float dragDownAmount = event2.getY() - event1.getY(); + final float expansion = 1 - percent; // Ensure correct expansion passed in. ShadeExpansionChangeEvent event = @@ -529,7 +438,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float expansion = 1 - swipeUpPercentage; // The upward velocity is ignored. final float velocityY = -1; - swipeToPosition(swipeUpPercentage, Direction.UP, velocityY); + swipeToPosition(swipeUpPercentage, velocityY); verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncerConstants.EXPANSION_HIDDEN)); @@ -552,7 +461,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float expansion = 1 - swipeUpPercentage; // The downward velocity is ignored. final float velocityY = 1; - swipeToPosition(swipeUpPercentage, Direction.UP, velocityY); + swipeToPosition(swipeUpPercentage, velocityY); verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncerConstants.EXPANSION_VISIBLE)); @@ -573,57 +482,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { } /** - * Tests that ending a downward swipe above the set threshold will continue the expansion, - * but will not trigger logging of the DREAM_SWIPED event. - */ - @Test - public void testSwipeDownPositionAboveThreshold_expandsBouncer_doesNotLog() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - - final float swipeDownPercentage = .3f; - // The downward velocity is ignored. - final float velocityY = 1; - swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY); - - verify(mValueAnimatorCreator).create(eq(swipeDownPercentage), - eq(KeyguardBouncerConstants.EXPANSION_VISIBLE)); - verify(mValueAnimator, never()).addListener(any()); - - verify(mFlingAnimationUtils).apply(eq(mValueAnimator), - eq(SCREEN_HEIGHT_PX * swipeDownPercentage), - eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE), - eq(velocityY), eq((float) SCREEN_HEIGHT_PX)); - verify(mValueAnimator).start(); - verify(mUiEventLogger, never()).log(any()); - } - - /** - * Tests that swiping down with a speed above the set threshold leads to bouncer collapsing - * down. - */ - @Test - public void testSwipeDownVelocityAboveMin_collapsesBouncer() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn((float) 0); - - // The ending position above the set threshold is ignored. - final float swipeDownPercentage = .3f; - final float velocityY = 1; - swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY); - - verify(mValueAnimatorCreator).create(eq(swipeDownPercentage), - eq(KeyguardBouncerConstants.EXPANSION_HIDDEN)); - verify(mValueAnimator, never()).addListener(any()); - - verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator), - eq(SCREEN_HEIGHT_PX * swipeDownPercentage), - eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN), - eq(velocityY), eq((float) SCREEN_HEIGHT_PX)); - verify(mValueAnimator).start(); - verify(mUiEventLogger, never()).log(any()); - } - - /** * Tests that swiping up with a speed above the set threshold will continue the expansion. */ @Test @@ -634,7 +492,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float swipeUpPercentage = .3f; final float expansion = 1 - swipeUpPercentage; final float velocityY = -1; - swipeToPosition(swipeUpPercentage, Direction.UP, velocityY); + swipeToPosition(swipeUpPercentage, velocityY); verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncerConstants.EXPANSION_VISIBLE)); @@ -654,26 +512,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_BOUNCER_FULLY_VISIBLE); } - /** - * Ensures {@link CentralSurfaces} - */ - @Test - public void testInformBouncerShowingOnExpand() { - swipeToPosition(1f, Direction.UP, 0); - } - - /** - * Ensures {@link CentralSurfaces} - */ - @Test - public void testInformBouncerHidingOnCollapse() { - // Must swipe up to set initial state. - swipeToPosition(1f, Direction.UP, 0); - Mockito.clearInvocations(mCentralSurfaces); - - swipeToPosition(0f, Direction.DOWN, 0); - } - @Test public void testTouchSessionOnRemovedCalledTwice() { mTouchHandler.onSessionStart(mTouchSession); @@ -684,7 +522,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { onRemovedCallbackCaptor.getValue().onRemoved(); } - private void swipeToPosition(float percent, Direction direction, float velocityY) { + private void swipeToPosition(float percent, float velocityY) { Mockito.clearInvocations(mTouchSession); mTouchHandler.onSessionStart(mTouchSession); ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = @@ -699,12 +537,12 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float distanceY = SCREEN_HEIGHT_PX * percent; final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0); + 0, SCREEN_HEIGHT_PX, 0); final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0); + 0, SCREEN_HEIGHT_PX - distanceY, 0); assertThat(gestureListenerCaptor.getValue().onScroll(event1, event2, 0, - direction == Direction.UP ? distanceY : -distanceY)) + distanceY)) .isTrue(); final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java index 27bffd0818e7..11a42413c4ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java @@ -18,8 +18,10 @@ package com.android.systemui.ambient.touch; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.view.GestureDetector; import android.view.MotionEvent; @@ -28,7 +30,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -36,6 +37,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -49,66 +51,89 @@ public class ShadeTouchHandlerTest extends SysuiTestCase { CentralSurfaces mCentralSurfaces; @Mock - ShadeViewController mShadeViewController; - - @Mock TouchHandler.TouchSession mTouchSession; ShadeTouchHandler mTouchHandler; + @Captor + ArgumentCaptor<GestureDetector.OnGestureListener> mGestureListenerCaptor; + @Captor + ArgumentCaptor<InputChannelCompat.InputEventListener> mInputListenerCaptor; + private static final int TOUCH_HEIGHT = 20; @Before public void setup() { MockitoAnnotations.initMocks(this); - mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController, - TOUCH_HEIGHT); + + mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), TOUCH_HEIGHT); } - /** - * Verify that touches aren't handled when the bouncer is showing. - */ + // Verifies that a swipe down in the gesture region is captured by the shade touch handler. @Test - public void testInactiveOnBouncer() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - mTouchHandler.onSessionStart(mTouchSession); - verify(mTouchSession).pop(); + public void testSwipeDown_captured() { + final boolean captured = swipe(Direction.DOWN); + + assertThat(captured).isTrue(); } - /** - * Make sure {@link ShadeTouchHandler} - */ + // Verifies that a swipe in the upward direction is not catpured. @Test - public void testTouchPilferingOnScroll() { - final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class); - final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class); + public void testSwipeUp_notCaptured() { + final boolean captured = swipe(Direction.UP); - final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor = - ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); + // Motion events not captured as the swipe is going in the wrong direction. + assertThat(captured).isFalse(); + } - mTouchHandler.onSessionStart(mTouchSession); - verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture()); + // Verifies that a swipe down forwards captured touches to the shade window for handling. + @Test + public void testSwipeDown_sentToShadeWindow() { + swipe(Direction.DOWN); - assertThat(gestureListenerArgumentCaptor.getValue() - .onScroll(motionEvent1, motionEvent2, 1, 1)) - .isTrue(); + // Both motion events are sent for the shade window to process. + verify(mCentralSurfaces, times(2)).handleExternalShadeWindowTouch(any()); } - /** - * Ensure touches are propagated to the {@link ShadeViewController}. - */ + // Verifies that a swipe down is not forwarded to the shade window. @Test - public void testEventPropagation() { - final MotionEvent motionEvent = Mockito.mock(MotionEvent.class); + public void testSwipeUp_touchesNotSent() { + swipe(Direction.UP); - final ArgumentCaptor<InputChannelCompat.InputEventListener> - inputEventListenerArgumentCaptor = - ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); + // Motion events are not sent for the shade window to process as the swipe is going in the + // wrong direction. + verify(mCentralSurfaces, never()).handleExternalShadeWindowTouch(any()); + } + /** + * Simulates a swipe in the given direction and returns true if the touch was intercepted by the + * touch handler's gesture listener. + * <p> + * Swipe down starts from a Y coordinate of 0 and goes downward. Swipe up starts from the edge + * of the gesture region, {@link #TOUCH_HEIGHT}, and goes upward to 0. + */ + private boolean swipe(Direction direction) { + Mockito.clearInvocations(mTouchSession); mTouchHandler.onSessionStart(mTouchSession); - verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture()); - inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent); - verify(mShadeViewController).handleExternalTouch(motionEvent); + + verify(mTouchSession).registerGestureListener(mGestureListenerCaptor.capture()); + verify(mTouchSession).registerInputListener(mInputListenerCaptor.capture()); + + final float startY = direction == Direction.UP ? TOUCH_HEIGHT : 0; + final float endY = direction == Direction.UP ? 0 : TOUCH_HEIGHT; + + // Send touches to the input and gesture listener. + final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, startY, 0); + final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, endY, 0); + mInputListenerCaptor.getValue().onInputEvent(event1); + mInputListenerCaptor.getValue().onInputEvent(event2); + final boolean captured = mGestureListenerCaptor.getValue().onScroll(event1, event2, 0, + startY - endY); + + return captured; } + private enum Direction { + DOWN, UP, + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index 6a8ab39c997d..bdb0c9aeb6ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -17,41 +17,52 @@ package com.android.systemui.dreams import android.content.ComponentName import android.content.Intent -import android.os.RemoteException import android.service.dreams.IDreamOverlay import android.service.dreams.IDreamOverlayCallback import android.service.dreams.IDreamOverlayClient import android.service.dreams.IDreamOverlayClientCallback +import android.testing.TestableLooper import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.WindowManagerImpl import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState import com.android.internal.logging.UiEventLogger import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.ambient.touch.scrim.ScrimController import com.android.systemui.ambient.touch.scrim.ScrimManager -import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.complication.ComplicationHostViewController import com.android.systemui.complication.ComplicationLayoutEngine import com.android.systemui.complication.dagger.ComplicationComponent import com.android.systemui.dreams.complication.HideComplicationTouchHandler import com.android.systemui.dreams.dagger.DreamOverlayComponent +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.android.systemui.touch.TouchInsetManager import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runCurrent import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -59,20 +70,24 @@ import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import org.mockito.invocation.InvocationOnMock +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class DreamOverlayServiceTest : SysuiTestCase() { private val mFakeSystemClock = FakeSystemClock() private val mMainExecutor = FakeExecutor(mFakeSystemClock) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope @Mock lateinit var mLifecycleOwner: DreamOverlayLifecycleOwner - @Mock lateinit var mLifecycleRegistry: LifecycleRegistry + private lateinit var lifecycleRegistry: FakeLifecycleRegistry - lateinit var mWindowParams: WindowManager.LayoutParams + private lateinit var mWindowParams: WindowManager.LayoutParams @Mock lateinit var mDreamOverlayCallback: IDreamOverlayCallback @@ -124,22 +139,29 @@ class DreamOverlayServiceTest : SysuiTestCase() { @Mock lateinit var mScrimController: ScrimController - @Mock lateinit var mCommunalInteractor: CommunalInteractor - @Mock lateinit var mSystemDialogsCloser: SystemDialogsCloser @Mock lateinit var mDreamOverlayCallbackController: DreamOverlayCallbackController + private lateinit var bouncerRepository: FakeKeyguardBouncerRepository + private lateinit var communalRepository: FakeCommunalRepository + @Captor var mViewCaptor: ArgumentCaptor<View>? = null - var mService: DreamOverlayService? = null + private lateinit var mService: DreamOverlayService + @Before fun setup() { MockitoAnnotations.initMocks(this) + + lifecycleRegistry = FakeLifecycleRegistry(mLifecycleOwner) + bouncerRepository = kosmos.fakeKeyguardBouncerRepository + communalRepository = kosmos.fakeCommunalRepository + whenever(mDreamOverlayComponent.getDreamOverlayContainerViewController()) .thenReturn(mDreamOverlayContainerViewController) whenever(mComplicationComponent.getComplicationHostViewController()) .thenReturn(mComplicationHostViewController) - whenever(mLifecycleOwner.registry).thenReturn(mLifecycleRegistry) + whenever(mLifecycleOwner.registry).thenReturn(lifecycleRegistry) whenever(mComplicationComponentFactory.create(any(), any(), any(), any())) .thenReturn(mComplicationComponent) whenever(mComplicationComponent.getVisibilityController()) @@ -170,26 +192,29 @@ class DreamOverlayServiceTest : SysuiTestCase() { mStateController, mKeyguardUpdateMonitor, mScrimManager, - mCommunalInteractor, + kosmos.communalInteractor, mSystemDialogsCloser, mUiEventLogger, mTouchInsetManager, LOW_LIGHT_COMPONENT, HOME_CONTROL_PANEL_DREAM_COMPONENT, mDreamOverlayCallbackController, + kosmos.keyguardInteractor, WINDOW_NAME ) } - @get:Throws(RemoteException::class) - val client: IDreamOverlayClient + private val client: IDreamOverlayClient get() { - val proxy = mService!!.onBind(Intent()) + mService.onCreate() + TestableLooper.get(this).processAllMessages() + + val proxy = mService.onBind(Intent()) val overlay = IDreamOverlay.Stub.asInterface(proxy) val callback = Mockito.mock(IDreamOverlayClientCallback::class.java) overlay.getClient(callback) val clientCaptor = ArgumentCaptor.forClass(IDreamOverlayClient::class.java) - Mockito.verify(callback).onDreamOverlayClient(clientCaptor.capture()) + verify(callback).onDreamOverlayClient(clientCaptor.capture()) return clientCaptor.value } @@ -205,9 +230,8 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mUiEventLogger) - .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START) - Mockito.verify(mUiEventLogger) + verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START) + verify(mUiEventLogger) .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START) } @@ -223,7 +247,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mWindowManager).addView(any(), any()) + verify(mWindowManager).addView(any(), any()) } // Validates that {@link DreamOverlayService} properly handles the case where the dream's @@ -242,14 +266,14 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mWindowManager).addView(any(), any()) - Mockito.verify(mStateController).setOverlayActive(false) - Mockito.verify(mStateController).setLowLightActive(false) - Mockito.verify(mStateController).setEntryAnimationsFinished(false) - Mockito.verify(mStateController, Mockito.never()).setOverlayActive(true) - Mockito.verify(mUiEventLogger, Mockito.never()) + verify(mWindowManager).addView(any(), any()) + verify(mStateController).setOverlayActive(false) + verify(mStateController).setLowLightActive(false) + verify(mStateController).setEntryAnimationsFinished(false) + verify(mStateController, Mockito.never()).setOverlayActive(true) + verify(mUiEventLogger, Mockito.never()) .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START) - Mockito.verify(mDreamOverlayCallbackController, Mockito.never()).onStartDream() + verify(mDreamOverlayCallbackController, Mockito.never()).onStartDream() } @Test @@ -264,7 +288,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mDreamOverlayContainerViewController).init() + verify(mDreamOverlayContainerViewController).init() } @Test @@ -282,7 +306,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView) + verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView) } @Test @@ -297,7 +321,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Truth.assertThat(mService!!.shouldShowComplications()).isTrue() + assertThat(mService.shouldShowComplications()).isTrue() } @Test @@ -312,8 +336,8 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Truth.assertThat(mService!!.dreamComponent).isEqualTo(LOW_LIGHT_COMPONENT) - Mockito.verify(mStateController).setLowLightActive(true) + assertThat(mService.dreamComponent).isEqualTo(LOW_LIGHT_COMPONENT) + verify(mStateController).setLowLightActive(true) } @Test @@ -328,8 +352,8 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Truth.assertThat(mService!!.dreamComponent).isEqualTo(HOME_CONTROL_PANEL_DREAM_COMPONENT) - Mockito.verify(mStateController).setHomeControlPanelActive(true) + assertThat(mService.dreamComponent).isEqualTo(HOME_CONTROL_PANEL_DREAM_COMPONENT) + verify(mStateController).setHomeControlPanelActive(true) } @Test @@ -346,19 +370,19 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() // Verify view added. - Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) + verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) // Service destroyed. - mService!!.onEndDream() + mService.onEndDream() mMainExecutor.runAllReady() // Verify view removed. - Mockito.verify(mWindowManager).removeView(mViewCaptor!!.value) + verify(mWindowManager).removeView(mViewCaptor!!.value) // Verify state correctly set. - Mockito.verify(mStateController).setOverlayActive(false) - Mockito.verify(mStateController).setLowLightActive(false) - Mockito.verify(mStateController).setEntryAnimationsFinished(false) + verify(mStateController).setOverlayActive(false) + verify(mStateController).setLowLightActive(false) + verify(mStateController).setEntryAnimationsFinished(false) } @Test @@ -391,7 +415,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { // Schedule the endDream call in the middle of the startDream implementation, as any // ordering is possible. - Mockito.doAnswer { invocation: InvocationOnMock? -> + Mockito.doAnswer { client.endDream() null } @@ -427,37 +451,37 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() // Verify view added. - Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) + verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) // Service destroyed. - mService!!.onDestroy() + mService.onDestroy() mMainExecutor.runAllReady() // Verify view removed. - Mockito.verify(mWindowManager).removeView(mViewCaptor!!.value) + verify(mWindowManager).removeView(mViewCaptor!!.value) // Verify state correctly set. - Mockito.verify(mKeyguardUpdateMonitor).removeCallback(any()) - Mockito.verify(mLifecycleRegistry).currentState = Lifecycle.State.DESTROYED - Mockito.verify(mStateController).setOverlayActive(false) - Mockito.verify(mStateController).setLowLightActive(false) - Mockito.verify(mStateController).setEntryAnimationsFinished(false) + verify(mKeyguardUpdateMonitor).removeCallback(any()) + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED) + verify(mStateController).setOverlayActive(false) + verify(mStateController).setLowLightActive(false) + verify(mStateController).setEntryAnimationsFinished(false) } @Test fun testDoNotRemoveViewOnDestroyIfOverlayNotStarted() { // Service destroyed without ever starting dream. - mService!!.onDestroy() + mService.onDestroy() mMainExecutor.runAllReady() // Verify no view is removed. - Mockito.verify(mWindowManager, Mockito.never()).removeView(any()) + verify(mWindowManager, Mockito.never()).removeView(any()) // Verify state still correctly set. - Mockito.verify(mKeyguardUpdateMonitor).removeCallback(any()) - Mockito.verify(mLifecycleRegistry).currentState = Lifecycle.State.DESTROYED - Mockito.verify(mStateController).setOverlayActive(false) - Mockito.verify(mStateController).setLowLightActive(false) + verify(mKeyguardUpdateMonitor).removeCallback(any()) + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED) + verify(mStateController).setOverlayActive(false) + verify(mStateController).setLowLightActive(false) } @Test @@ -465,7 +489,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { val client = client // Destroy the service. - mService!!.onDestroy() + mService.onDestroy() mMainExecutor.runAllReady() // Inform the overlay service of dream starting. @@ -476,15 +500,15 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mWindowManager, Mockito.never()).addView(any(), any()) + verify(mWindowManager, Mockito.never()).addView(any(), any()) } @Test fun testNeverRemoveDecorViewIfNotAdded() { // Service destroyed before dream started. - mService!!.onDestroy() + mService.onDestroy() mMainExecutor.runAllReady() - Mockito.verify(mWindowManager, Mockito.never()).removeView(any()) + verify(mWindowManager, Mockito.never()).removeView(any()) } @Test @@ -501,11 +525,11 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() // Verify that a new window is added. - Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) + verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) val windowDecorView = mViewCaptor!!.value // Assert that the overlay is not showing complications. - Truth.assertThat(mService!!.shouldShowComplications()).isFalse() + assertThat(mService.shouldShowComplications()).isFalse() Mockito.clearInvocations(mDreamOverlayComponent) Mockito.clearInvocations(mAmbientTouchComponent) Mockito.clearInvocations(mWindowManager) @@ -522,16 +546,16 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() // Assert that the overlay is showing complications. - Truth.assertThat(mService!!.shouldShowComplications()).isTrue() + assertThat(mService.shouldShowComplications()).isTrue() // Verify that the old overlay window has been removed, and a new one created. - Mockito.verify(mWindowManager).removeView(windowDecorView) - Mockito.verify(mWindowManager).addView(any(), any()) + verify(mWindowManager).removeView(windowDecorView) + verify(mWindowManager).addView(any(), any()) // Verify that new instances of overlay container view controller and overlay touch monitor // are created. - Mockito.verify(mDreamOverlayComponent).getDreamOverlayContainerViewController() - Mockito.verify(mAmbientTouchComponent).getTouchMonitor() + verify(mDreamOverlayComponent).getDreamOverlayContainerViewController() + verify(mAmbientTouchComponent).getTouchMonitor() } @Test @@ -546,15 +570,15 @@ class DreamOverlayServiceTest : SysuiTestCase() { true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - mService!!.onWakeUp() - Mockito.verify(mDreamOverlayContainerViewController).wakeUp() - Mockito.verify(mDreamOverlayCallbackController).onWakeUp() + mService.onWakeUp() + verify(mDreamOverlayContainerViewController).wakeUp() + verify(mDreamOverlayCallbackController).onWakeUp() } @Test fun testWakeUpBeforeStartDoesNothing() { - mService!!.onWakeUp() - Mockito.verify(mDreamOverlayContainerViewController, Mockito.never()).wakeUp() + mService.onWakeUp() + verify(mDreamOverlayContainerViewController, Mockito.never()).wakeUp() } @Test @@ -572,8 +596,8 @@ class DreamOverlayServiceTest : SysuiTestCase() { val paramsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams::class.java) // Verify that a new window is added. - Mockito.verify(mWindowManager).addView(any(), paramsCaptor.capture()) - Truth.assertThat( + verify(mWindowManager).addView(any(), paramsCaptor.capture()) + assertThat( paramsCaptor.value.privateFlags and WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS == WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS @@ -598,7 +622,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { whenever(mDreamOverlayContainerViewController.isBouncerShowing()).thenReturn(true) mService!!.onComeToFront() - Mockito.verify(mScrimController).expand(any()) + verify(mScrimController).expand(any()) } // Tests that glanceable hub is hidden when DreamOverlayService is told that the dream is @@ -617,7 +641,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() mService!!.onComeToFront() - Mockito.verify(mCommunalInteractor).changeScene(eq(CommunalScenes.Blank), nullable()) + assertThat(communalRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) } // Tests that system dialogs (e.g. notification shade) closes when DreamOverlayService is told @@ -636,7 +660,197 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() mService!!.onComeToFront() - Mockito.verify(mSystemDialogsCloser).closeSystemDialogs() + verify(mSystemDialogsCloser).closeSystemDialogs() + } + + @Test + fun testLifecycle_createdAfterConstruction() { + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun testLifecycle_resumedAfterDreamStarts() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.mLifecycles) + .containsExactly( + Lifecycle.State.CREATED, + Lifecycle.State.STARTED, + Lifecycle.State.RESUMED + ) + } + + @Test + fun testLifecycle_destroyedAfterOnDestroy() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + mService.onDestroy() + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.mLifecycles) + .containsExactly( + Lifecycle.State.CREATED, + Lifecycle.State.STARTED, + Lifecycle.State.RESUMED, + Lifecycle.State.DESTROYED + ) + } + + @Test + fun testNotificationShadeShown_setsLifecycleState() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + val callbackCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) + verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture()) + + // Notification shade opens. + callbackCaptor.value.onShadeExpandedChanged(true) + mMainExecutor.runAllReady() + + // Lifecycle state goes from resumed back to started when the notification shade shows. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Notification shade closes. + callbackCaptor.value.onShadeExpandedChanged(false) + mMainExecutor.runAllReady() + + // Lifecycle state goes back to RESUMED. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + @Test + fun testBouncerShown_setsLifecycleState() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + + // Bouncer shows. + bouncerRepository.setPrimaryShow(true) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes from resumed back to started when the notification shade shows. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Bouncer closes. + bouncerRepository.setPrimaryShow(false) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes back to RESUMED. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + @Test + fun testCommunalVisible_setsLifecycleState() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + val transitionState: MutableStateFlow<ObservableTransitionState> = + MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank)) + communalRepository.setTransitionState(transitionState) + + // Communal becomes visible. + transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Communal) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes from resumed back to started when the notification shade shows. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Communal closes. + transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Blank) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes back to RESUMED. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + // Verifies the dream's lifecycle + @Test + fun testLifecycleStarted_whenAnyOcclusion() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + val transitionState: MutableStateFlow<ObservableTransitionState> = + MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank)) + communalRepository.setTransitionState(transitionState) + + // Communal becomes visible. + transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Communal) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes from resumed back to started when the notification shade shows. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Communal closes. + transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Blank) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes back to RESUMED. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + internal class FakeLifecycleRegistry(provider: LifecycleOwner) : LifecycleRegistry(provider) { + val mLifecycles: MutableList<State> = ArrayList() + + override var currentState: State + get() = mLifecycles[mLifecycles.size - 1] + set(state) { + mLifecycles.add(state) + } } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java index 29fbee01a18b..e3c6deed1527 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -108,7 +108,7 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { mTouchHandler.onSessionStart(mTouchSession); verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture()); inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent); - verify(mCentralSurfaces).handleDreamTouch(motionEvent); + verify(mCentralSurfaces).handleExternalShadeWindowTouch(motionEvent); } @Test diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java index d0f08f53fb32..85aeb27261aa 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java @@ -27,6 +27,7 @@ import android.view.InputEvent; import android.view.MotionEvent; import android.view.VelocityTracker; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.logging.UiEvent; @@ -94,13 +95,11 @@ public class BouncerSwipeTouchHandler implements TouchHandler { private Boolean mCapture; private Boolean mExpanded; - private boolean mBouncerInitiallyShowing; - private TouchSession mTouchSession; - private ValueAnimatorCreator mValueAnimatorCreator; + private final ValueAnimatorCreator mValueAnimatorCreator; - private VelocityTrackerFactory mVelocityTrackerFactory; + private final VelocityTrackerFactory mVelocityTrackerFactory; private final UiEventLogger mUiEventLogger; @@ -118,17 +117,12 @@ public class BouncerSwipeTouchHandler implements TouchHandler { private final GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() { @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) { if (mCapture == null) { - mBouncerInitiallyShowing = mCentralSurfaces - .map(CentralSurfaces::isBouncerShowing) - .orElse(false); - if (Flags.dreamOverlayBouncerSwipeDirectionFiltering()) { mCapture = Math.abs(distanceY) > Math.abs(distanceX) - && ((distanceY < 0 && mBouncerInitiallyShowing) - || (distanceY > 0 && !mBouncerInitiallyShowing)); + && distanceY > 0; } else { // If the user scrolling favors a vertical direction, begin capturing // scrolls. @@ -146,13 +140,8 @@ public class BouncerSwipeTouchHandler implements TouchHandler { return false; } - // Don't set expansion for downward scroll when the bouncer is hidden. - if (!mBouncerInitiallyShowing && (e1.getY() < e2.getY())) { - return true; - } - - // Don't set expansion for upward scroll when the bouncer is shown. - if (mBouncerInitiallyShowing && (e1.getY() > e2.getY())) { + // Don't set expansion for downward scroll. + if (e1.getY() < e2.getY()) { return true; } @@ -176,8 +165,7 @@ public class BouncerSwipeTouchHandler implements TouchHandler { final float dragDownAmount = e2.getY() - e1.getY(); final float screenTravelPercentage = Math.abs(e1.getY() - e2.getY()) / mTouchSession.getBounds().height(); - setPanelExpansion(mBouncerInitiallyShowing - ? screenTravelPercentage : 1 - screenTravelPercentage); + setPanelExpansion(1 - screenTravelPercentage); return true; } }; @@ -223,9 +211,9 @@ public class BouncerSwipeTouchHandler implements TouchHandler { LockPatternUtils lockPatternUtils, UserTracker userTracker, @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING) - FlingAnimationUtils flingAnimationUtils, + FlingAnimationUtils flingAnimationUtils, @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING) - FlingAnimationUtils flingAnimationUtilsClosing, + FlingAnimationUtils flingAnimationUtilsClosing, @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage, @Named(BouncerSwipeModule.MIN_BOUNCER_ZONE_SCREEN_PERCENTAGE) float minRegionPercentage, UiEventLogger uiEventLogger) { @@ -247,17 +235,13 @@ public class BouncerSwipeTouchHandler implements TouchHandler { public void getTouchInitiationRegion(Rect bounds, Region region, Rect exclusionRect) { final int width = bounds.width(); final int height = bounds.height(); - final float minBouncerHeight = height * mMinBouncerZoneScreenPercentage; final int minAllowableBottom = Math.round(height * (1 - mMinBouncerZoneScreenPercentage)); - final boolean isBouncerShowing = - mCentralSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false); - final Rect normalRegion = isBouncerShowing - ? new Rect(0, 0, width, Math.round(height * mBouncerZoneScreenPercentage)) - : new Rect(0, Math.round(height * (1 - mBouncerZoneScreenPercentage)), - width, height); + final Rect normalRegion = new Rect(0, + Math.round(height * (1 - mBouncerZoneScreenPercentage)), + width, height); - if (!isBouncerShowing && exclusionRect != null) { + if (exclusionRect != null) { int lowestBottom = Math.min(Math.max(0, exclusionRect.bottom), minAllowableBottom); normalRegion.top = Math.max(normalRegion.top, lowestBottom); } @@ -322,8 +306,7 @@ public class BouncerSwipeTouchHandler implements TouchHandler { : KeyguardBouncerConstants.EXPANSION_HIDDEN; // Log the swiping up to show Bouncer event. - if (!mBouncerInitiallyShowing - && expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { + if (expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { mUiEventLogger.log(DreamEvent.DREAM_SWIPED); } @@ -335,17 +318,15 @@ public class BouncerSwipeTouchHandler implements TouchHandler { } } - private ValueAnimator createExpansionAnimator(float targetExpansion, float expansionHeight) { + private ValueAnimator createExpansionAnimator(float targetExpansion) { final ValueAnimator animator = mValueAnimatorCreator.create(mCurrentExpansion, targetExpansion); animator.addUpdateListener( animation -> { float expansionFraction = (float) animation.getAnimatedValue(); - float dragDownAmount = expansionFraction * expansionHeight; setPanelExpansion(expansionFraction); }); - if (!mBouncerInitiallyShowing - && targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { + if (targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { animator.addListener( new AnimatorListenerAdapter() { @Override @@ -381,8 +362,7 @@ public class BouncerSwipeTouchHandler implements TouchHandler { final float viewHeight = mTouchSession.getBounds().height(); final float currentHeight = viewHeight * mCurrentExpansion; final float targetHeight = viewHeight * expansion; - final float expansionHeight = targetHeight - currentHeight; - final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight); + final ValueAnimator animator = createExpansionAnimator(expansion); if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) { // Hides the bouncer, i.e., fully expands the space above the bouncer. mFlingAnimationUtilsClosing.apply(animator, currentHeight, targetHeight, velocity, diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java index 9ef9938ab8ad..9c7fc9dd307f 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java @@ -23,7 +23,8 @@ import android.graphics.Region; import android.view.GestureDetector; import android.view.MotionEvent; -import com.android.systemui.shade.ShadeViewController; +import androidx.annotation.NonNull; + import com.android.systemui.statusbar.phone.CentralSurfaces; import java.util.Optional; @@ -37,29 +38,34 @@ import javax.inject.Named; */ public class ShadeTouchHandler implements TouchHandler { private final Optional<CentralSurfaces> mSurfaces; - private final ShadeViewController mShadeViewController; private final int mInitiationHeight; + /** + * Tracks whether or not we are capturing a given touch. Will be null before and after a touch. + */ + private Boolean mCapture; + @Inject ShadeTouchHandler(Optional<CentralSurfaces> centralSurfaces, - ShadeViewController shadeViewController, @Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) { mSurfaces = centralSurfaces; - mShadeViewController = shadeViewController; mInitiationHeight = initiationHeight; } @Override public void onSessionStart(TouchSession session) { - if (mSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) { + if (mSurfaces.isEmpty()) { session.pop(); return; } - session.registerInputListener(ev -> { - mShadeViewController.handleExternalTouch((MotionEvent) ev); + session.registerCallback(() -> mCapture = null); + session.registerInputListener(ev -> { if (ev instanceof MotionEvent) { + if (mCapture != null && mCapture) { + mSurfaces.get().handleExternalShadeWindowTouch((MotionEvent) ev); + } if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { session.pop(); } @@ -68,15 +74,25 @@ public class ShadeTouchHandler implements TouchHandler { session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() { @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) { - return true; + if (mCapture == null) { + // Only capture swipes that are going downwards. + mCapture = Math.abs(distanceY) > Math.abs(distanceX) && distanceY < 0; + if (mCapture) { + // Send the initial touches over, as the input listener has already + // processed these touches. + mSurfaces.get().handleExternalShadeWindowTouch(e1); + mSurfaces.get().handleExternalShadeWindowTouch(e2); + } + } + return mCapture; } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { - return true; + return mCapture; } }); } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 3d52bcd02ada..a9ef53104c31 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -19,9 +19,11 @@ package com.android.systemui.dreams; import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE; import static com.android.systemui.dreams.dagger.DreamModule.DREAM_TOUCH_INSET_MANAGER; import static com.android.systemui.dreams.dagger.DreamModule.HOME_CONTROL_PANEL_DREAM_COMPONENT; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.graphics.drawable.ColorDrawable; import android.util.Log; import android.view.View; @@ -34,7 +36,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; +import androidx.lifecycle.LifecycleService; +import androidx.lifecycle.ServiceLifecycleDispatcher; import androidx.lifecycle.ViewModelStore; import com.android.dream.lowlight.dagger.LowLightDreamModule; @@ -52,12 +57,14 @@ import com.android.systemui.complication.Complication; import com.android.systemui.complication.dagger.ComplicationComponent; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.dagger.DreamOverlayComponent; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.Arrays; import java.util.HashSet; +import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Named; @@ -67,7 +74,8 @@ import javax.inject.Named; * dream reaches directly out to the service with a Window reference (via LayoutParams), which the * service uses to insert its own child Window into the dream's parent Window. */ -public class DreamOverlayService extends android.service.dreams.DreamOverlayService { +public class DreamOverlayService extends android.service.dreams.DreamOverlayService implements + LifecycleOwner { private static final String TAG = "DreamOverlayService"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -98,6 +106,21 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // True if the service has been destroyed. private boolean mDestroyed = false; + /** + * True if the notification shade is open. + */ + private boolean mShadeExpanded = false; + + /** + * True if any part of the glanceable hub is visible. + */ + private boolean mCommunalVisible = false; + + /** + * True if the primary bouncer is visible. + */ + private boolean mBouncerShowing = false; + private final ComplicationComponent mComplicationComponent; private final AmbientTouchComponent mAmbientTouchComponent; @@ -107,9 +130,21 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private final DreamOverlayComponent mDreamOverlayComponent; - private final DreamOverlayLifecycleOwner mLifecycleOwner; + /** + * This {@link LifecycleRegistry} controls when dream overlay functionality, like touch + * handling, should be active. It will automatically be paused when the dream overlay is hidden + * while dreaming, such as when the notification shade, bouncer, or glanceable hub are visible. + */ private final LifecycleRegistry mLifecycleRegistry; + /** + * Drives the lifecycle exposed by this service's {@link #getLifecycle()}. + * <p> + * Used to mimic a {@link LifecycleService}, though we do not update the lifecycle in + * {@link #onBind(Intent)} since it's final in the base class. + */ + private final ServiceLifecycleDispatcher mDispatcher = new ServiceLifecycleDispatcher(this); + private TouchMonitor mTouchMonitor; private final CommunalInteractor mCommunalInteractor; @@ -121,17 +156,46 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ @Override public void onShadeExpandedChanged(boolean expanded) { mExecutor.execute(() -> { - if (getCurrentStateLocked() != Lifecycle.State.RESUMED - && getCurrentStateLocked() != Lifecycle.State.STARTED) { + if (mShadeExpanded == expanded) { return; } + mShadeExpanded = expanded; - setCurrentStateLocked( - expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED); + updateLifecycleStateLocked(); }); } }; + private final Consumer<Boolean> mCommunalVisibleConsumer = new Consumer<>() { + @Override + public void accept(Boolean communalVisible) { + mExecutor.execute(() -> { + if (mCommunalVisible == communalVisible) { + return; + } + + mCommunalVisible = communalVisible; + + updateLifecycleStateLocked(); + }); + } + }; + + private final Consumer<Boolean> mBouncerShowingConsumer = new Consumer<>() { + @Override + public void accept(Boolean bouncerShowing) { + mExecutor.execute(() -> { + if (mBouncerShowing == bouncerShowing) { + return; + } + + mBouncerShowing = bouncerShowing; + + updateLifecycleStateLocked(); + }); + } + }; + private final DreamOverlayStateController.Callback mExitAnimationFinishedCallback = new DreamOverlayStateController.Callback() { @Override @@ -183,10 +247,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ UiEventLogger uiEventLogger, @Named(DREAM_TOUCH_INSET_MANAGER) TouchInsetManager touchInsetManager, @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) - ComponentName lowLightDreamComponent, + ComponentName lowLightDreamComponent, @Nullable @Named(HOME_CONTROL_PANEL_DREAM_COMPONENT) - ComponentName homeControlPanelDreamComponent, + ComponentName homeControlPanelDreamComponent, DreamOverlayCallbackController dreamOverlayCallbackController, + KeyguardInteractor keyguardInteractor, @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) { super(executor); mContext = context; @@ -218,10 +283,32 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ new HashSet<>(Arrays.asList( mDreamComplicationComponent.getHideComplicationTouchHandler(), mDreamOverlayComponent.getCommunalTouchHandler()))); - mLifecycleOwner = lifecycleOwner; - mLifecycleRegistry = mLifecycleOwner.getRegistry(); + mLifecycleRegistry = lifecycleOwner.getRegistry(); + + mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED)); - mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED)); + collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(), + mCommunalVisibleConsumer); + collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing, + mBouncerShowingConsumer); + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return mDispatcher.getLifecycle(); + } + + @Override + public void onCreate() { + mDispatcher.onServicePreSuperOnCreate(); + super.onCreate(); + } + + @Override + public void onStart(Intent intent, int startId) { + mDispatcher.onServicePreSuperOnStart(); + super.onStart(intent, startId); } @Override @@ -229,19 +316,20 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback); mExecutor.execute(() -> { - setCurrentStateLocked(Lifecycle.State.DESTROYED); + setLifecycleStateLocked(Lifecycle.State.DESTROYED); resetCurrentDreamOverlayLocked(); mDestroyed = true; }); + mDispatcher.onServicePreSuperOnDestroy(); super.onDestroy(); } @Override public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { - setCurrentStateLocked(Lifecycle.State.STARTED); + setLifecycleStateLocked(Lifecycle.State.STARTED); mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START); @@ -271,7 +359,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ return; } - setCurrentStateLocked(Lifecycle.State.RESUMED); + setLifecycleStateLocked(Lifecycle.State.RESUMED); mStateController.setOverlayActive(true); final ComponentName dreamComponent = getDreamComponent(); mStateController.setLowLightActive( @@ -291,14 +379,27 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ resetCurrentDreamOverlayLocked(); } - private Lifecycle.State getCurrentStateLocked() { + private Lifecycle.State getLifecycleStateLocked() { return mLifecycleRegistry.getCurrentState(); } - private void setCurrentStateLocked(Lifecycle.State state) { + private void setLifecycleStateLocked(Lifecycle.State state) { mLifecycleRegistry.setCurrentState(state); } + private void updateLifecycleStateLocked() { + if (getLifecycleStateLocked() != Lifecycle.State.RESUMED + && getLifecycleStateLocked() != Lifecycle.State.STARTED) { + return; + } + + // If anything is on top of the dream, we should stop touch handling. + boolean shouldPause = mShadeExpanded || mCommunalVisible || mBouncerShowing; + + setLifecycleStateLocked( + shouldPause ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED); + } + @Override public void onWakeUp() { if (mDreamOverlayContainerViewController != null) { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java index 1c047ddcd3d8..fff0c58eecb8 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -98,7 +98,7 @@ public class CommunalTouchHandler implements TouchHandler { // Notification shade window has its own logic to be visible if the hub is open, no need to // do anything here other than send touch events over. session.registerInputListener(ev -> { - surfaces.handleDreamTouch((MotionEvent) ev); + surfaces.handleExternalShadeWindowTouch((MotionEvent) ev); if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { var unused = session.pop(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 7224536cfe70..d19176853387 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -226,7 +226,7 @@ constructor( val ambientIndicationVisible: Flow<Boolean> = repository.ambientIndicationVisible.asStateFlow() /** Whether the primary bouncer is showing or not. */ - val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow + @JvmField val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow /** Whether the alternate bouncer is showing or not. */ val alternateBouncerShowing: Flow<Boolean> = bouncerRepository.alternateBouncerVisible diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index a8481cde8ff0..a5a547403af9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -28,10 +28,14 @@ import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.ambient.touch.TouchMonitor +import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.communal.dagger.Communal import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.ui.compose.CommunalContainer @@ -45,6 +49,8 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.BooleanFlowOperators.or import com.android.systemui.util.kotlin.collectFlow import javax.inject.Inject @@ -67,31 +73,32 @@ constructor( private val shadeInteractor: ShadeInteractor, private val powerManager: PowerManager, private val communalColors: CommunalColors, - @Communal private val dataSourceDelegator: SceneDataSourceDelegator, -) { + private val ambientTouchComponentFactory: AmbientTouchComponent.Factory, + @Communal private val dataSourceDelegator: SceneDataSourceDelegator +) : LifecycleOwner { /** The container view for the hub. This will not be initialized until [initView] is called. */ private var communalContainerView: View? = null /** - * The width of the area in which a right edge swipe can open the hub, in pixels. Read from - * resources when [initView] is called. + * This lifecycle is used to control when the [touchMonitor] listens to touches. The lifecycle + * should only be [Lifecycle.State.RESUMED] when the hub is showing and not covered by anything, + * such as the notification shade or bouncer. */ - // TODO(b/320786721): support RTL layouts - private var rightEdgeSwipeRegionWidth: Int = 0 + private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) /** - * The height of the area in which a top edge swipe while the hub is open will not intercept - * touches, in pixels. This allows the top edge swipe to instead open the notification shade. - * Read from resources when [initView] is called. + * This [TouchMonitor] listens for top and bottom swipe gestures globally when the hub is open. + * When a top or bottom swipe is detected, they will be intercepted and used to open the + * notification shade/bouncer. */ - private var topEdgeSwipeRegionWidth: Int = 0 + private var touchMonitor: TouchMonitor? = null /** - * The height of the area in which a bottom edge swipe while the hub is open will not intercept - * touches, in pixels. This allows the bottom edge swipe to instead open the bouncer. Read from + * The width of the area in which a right edge swipe can open the hub, in pixels. Read from * resources when [initView] is called. */ - private var bottomEdgeSwipeRegionWidth: Int = 0 + // TODO(b/320786721): support RTL layouts + private var rightEdgeSwipeRegionWidth: Int = 0 /** * True if we are currently tracking a gesture for opening the hub that started in the edge @@ -102,9 +109,6 @@ constructor( /** True if we are currently tracking a touch on the hub while it's open. */ private var isTrackingHubTouch = false - /** True if we are tracking a top or bottom swipe gesture while the hub is open. */ - private var isTrackingHubGesture = false - /** * True if the hub UI is fully open, meaning it should receive touch input. * @@ -121,9 +125,15 @@ constructor( private var anyBouncerShowing = false /** - * True if the shade is fully expanded, meaning the hub should not receive any touch input. + * True if the shade is fully expanded and the user is not interacting with it anymore, meaning + * the hub should not receive any touch input. * - * Tracks [ShadeInteractor.isAnyFullyExpanded]. + * We need to not pause the touch handling lifecycle as soon as the shade opens because if the + * user swipes down, then back up without lifting their finger, the lifecycle will be paused + * then resumed, and resuming force-stops all active touch sessions. This means the shade will + * not receive the end of the gesture and will be stuck open. + * + * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting]. */ private var shadeShowing = false @@ -132,8 +142,6 @@ constructor( * and just let the dream overlay's touch handling deal with them. * * Tracks [KeyguardInteractor.isDreaming]. - * - * TODO(b/328838259): figure out a proper solution for touch handling above the lock screen too */ private var isDreaming = false @@ -192,28 +200,45 @@ constructor( throw RuntimeException("Communal view has already been initialized") } + if (touchMonitor == null) { + touchMonitor = + ambientTouchComponentFactory.create(this, HashSet()).getTouchMonitor().apply { + init() + } + } + lifecycleRegistry.currentState = Lifecycle.State.CREATED + communalContainerView = containerView rightEdgeSwipeRegionWidth = containerView.resources.getDimensionPixelSize( R.dimen.communal_right_edge_swipe_region_width ) - topEdgeSwipeRegionWidth = - containerView.resources.getDimensionPixelSize( - R.dimen.communal_top_edge_swipe_region_height - ) - bottomEdgeSwipeRegionWidth = - containerView.resources.getDimensionPixelSize( - R.dimen.communal_bottom_edge_swipe_region_height - ) collectFlow( containerView, keyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState), - { anyBouncerShowing = it } + { + anyBouncerShowing = it + updateLifecycleState() + } + ) + collectFlow( + containerView, + communalInteractor.isCommunalShowing, + { + hubShowing = it + updateLifecycleState() + } + ) + collectFlow( + containerView, + and(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)), + { + shadeShowing = it + updateLifecycleState() + } ) - collectFlow(containerView, communalInteractor.isCommunalShowing, { hubShowing = it }) - collectFlow(containerView, shadeInteractor.isAnyFullyExpanded, { shadeShowing = it }) collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it }) communalContainerView = containerView @@ -221,10 +246,24 @@ constructor( return containerView } + /** + * Updates the lifecycle stored by the [lifecycleRegistry] to control when the [touchMonitor] + * should listen for and intercept top and bottom swipes. + */ + private fun updateLifecycleState() { + val shouldInterceptGestures = hubShowing && !(shadeShowing || anyBouncerShowing) + if (shouldInterceptGestures) { + lifecycleRegistry.currentState = Lifecycle.State.RESUMED + } else { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + } + } + /** Removes the container view from its parent. */ fun disposeView() { communalContainerView?.let { (it.parent as ViewGroup).removeView(it) + lifecycleRegistry.currentState = Lifecycle.State.CREATED communalContainerView = null } } @@ -262,15 +301,7 @@ constructor( if (isDown && !hubOccluded) { // Only intercept down events if the hub isn't occluded by the bouncer or // notification shade. - val y = ev.rawY - val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth - val bottomSwipe = y >= view.height - bottomEdgeSwipeRegionWidth - - if (topSwipe || bottomSwipe) { - isTrackingHubGesture = true - } else { - isTrackingHubTouch = true - } + isTrackingHubTouch = true } if (isTrackingHubTouch) { @@ -283,19 +314,6 @@ constructor( // gesture // may return false from dispatchTouchEvent. return true - } else if (isTrackingHubGesture) { - // Tracking a top or bottom swipe on the hub UI. - if (isUp || isCancel) { - isTrackingHubGesture = false - } - - // If we're dreaming, intercept touches so the hub UI doesn't receive them, but - // don't do anything so that the dream's touch handling takes care of opening - // the bouncer or shade. - // - // If we're not dreaming, we don't intercept touches at the top/bottom edge so that - // swipes can open the notification shade and bouncer. - return isDreaming } return false @@ -347,4 +365,7 @@ constructor( 0 ) } + + override val lifecycle: Lifecycle + get() = lifecycleRegistry } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 907cf5eb6886..44f86da7431e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -25,7 +25,6 @@ import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.app.StatusBarManager; import android.util.Log; import android.view.GestureDetector; -import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; @@ -74,14 +73,14 @@ import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; import com.android.systemui.util.time.SystemClock; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + import java.io.PrintWriter; import java.util.Optional; import java.util.function.Consumer; import javax.inject.Inject; -import kotlinx.coroutines.ExperimentalCoroutinesApi; - /** * Controller for {@link NotificationShadeWindowView}. */ @@ -137,6 +136,11 @@ public class NotificationShadeWindowViewController implements Dumpable { private final PanelExpansionInteractor mPanelExpansionInteractor; private final ShadeExpansionStateManager mShadeExpansionStateManager; + /** + * If {@code true}, an external touch sent in {@link #handleExternalTouch(MotionEvent)} has been + * intercepted and all future touch events for the gesture should be processed by this view. + */ + private boolean mExternalTouchIntercepted = false; private boolean mIsTrackingBarGesture = false; private boolean mIsOcclusionTransitionRunning = false; private DisableSubpixelTextTransitionListener mDisableSubpixelTextTransitionListener; @@ -253,11 +257,28 @@ public class NotificationShadeWindowViewController implements Dumpable { } /** - * Handle a touch event while dreaming by forwarding the event to the content view. + * Handle a touch event while dreaming or on the hub by forwarding the event to the content + * view. + * <p> + * Since important logic for handling touches lives in the dispatch/intercept phases, we + * simulate going through all of these stages before sending onTouchEvent if intercepted. + * * @param event The event to forward. */ - public void handleDreamTouch(MotionEvent event) { - mView.dispatchTouchEvent(event); + public void handleExternalTouch(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + mExternalTouchIntercepted = false; + } + + if (!mView.dispatchTouchEvent(event)) { + return; + } + if (!mExternalTouchIntercepted) { + mExternalTouchIntercepted = mView.onInterceptTouchEvent(event); + } + if (mExternalTouchIntercepted) { + mView.onTouchEvent(event); + } } /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 8fb552f167bc..7d9742849a15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -283,11 +283,12 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner, CoreStartable void awakenDreams(); /** - * Handle a touch event while dreaming when the touch was initiated within a prescribed - * swipeable area. This method is provided for cases where swiping in certain areas of a dream - * should be handled by CentralSurfaces instead (e.g. swiping communal hub open). + * Handle a touch event while dreaming or on the glanceable hub when the touch was initiated + * within a prescribed swipeable area. This method is provided for cases where swiping in + * certain areas should be handled by CentralSurfaces instead (e.g. swiping hub open, opening + * the notification shade over dream or hub). */ - void handleDreamTouch(MotionEvent event); + void handleExternalShadeWindowTouch(MotionEvent event); boolean isBouncerShowing(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt index 8af7ee8389e5..d5e66ff660c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt @@ -79,7 +79,7 @@ abstract class CentralSurfacesEmptyImpl : CentralSurfaces { override fun updateScrimController() {} override fun shouldIgnoreTouch() = false override fun isDeviceInteractive() = false - override fun handleDreamTouch(event: MotionEvent?) {} + override fun handleExternalShadeWindowTouch(event: MotionEvent?) {} override fun awakenDreams() {} override fun isBouncerShowing() = false override fun isBouncerShowingScrimmed() = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index e9aa7aa57b1c..b2b2ceaa9017 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2928,8 +2928,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }; @Override - public void handleDreamTouch(MotionEvent event) { - getNotificationShadeWindowViewController().handleDreamTouch(event); + public void handleExternalShadeWindowTouch(MotionEvent event) { + getNotificationShadeWindowViewController().handleExternalTouch(event); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index fd9daf862190..03f5ecfa92d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -25,23 +25,24 @@ import android.view.MotionEvent import android.view.View import android.view.WindowManager import android.widget.FrameLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import androidx.test.filters.SmallTest import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.ambient.touch.TouchHandler +import com.android.systemui.ambient.touch.TouchMonitor +import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.fakeCommunalRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.setCommunalAvailable import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState @@ -51,7 +52,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.data.repository.fakeShadeRepository -import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.testKosmos @@ -60,7 +60,6 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertThrows @@ -87,16 +86,14 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { @Mock private lateinit var communalViewModel: CommunalViewModel @Mock private lateinit var powerManager: PowerManager @Mock private lateinit var dialogFactory: SystemUIDialogFactory + @Mock private lateinit var touchMonitor: TouchMonitor @Mock private lateinit var communalColors: CommunalColors - private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor - private lateinit var shadeInteractor: ShadeInteractor - private lateinit var keyguardInteractor: KeyguardInteractor + private lateinit var ambientTouchComponentFactory: AmbientTouchComponent.Factory private lateinit var parentView: FrameLayout private lateinit var containerView: View private lateinit var testableLooper: TestableLooper - private lateinit var communalInteractor: CommunalInteractor private lateinit var communalRepository: FakeCommunalRepository private lateinit var underTest: GlanceableHubContainerController @@ -104,32 +101,37 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - communalInteractor = kosmos.communalInteractor communalRepository = kosmos.fakeCommunalRepository - keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor - keyguardInteractor = kosmos.keyguardInteractor - shadeInteractor = kosmos.shadeInteractor - - underTest = - GlanceableHubContainerController( - communalInteractor, - communalViewModel, - dialogFactory, - keyguardTransitionInteractor, - keyguardInteractor, - shadeInteractor, - powerManager, - communalColors, - kosmos.sceneDataSourceDelegator, - ) + + ambientTouchComponentFactory = + object : AmbientTouchComponent.Factory { + override fun create( + lifecycleOwner: LifecycleOwner, + touchHandlers: Set<TouchHandler> + ): AmbientTouchComponent = + object : AmbientTouchComponent { + override fun getTouchMonitor(): TouchMonitor = touchMonitor + } + } + + with(kosmos) { + underTest = + GlanceableHubContainerController( + communalInteractor, + communalViewModel, + dialogFactory, + keyguardTransitionInteractor, + keyguardInteractor, + shadeInteractor, + powerManager, + communalColors, + ambientTouchComponentFactory, + kosmos.sceneDataSourceDelegator, + ) + } testableLooper = TestableLooper.get(this) overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH) - overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH) - overrideResource( - R.dimen.communal_bottom_edge_swipe_region_height, - BOTTOM_SWIPE_REGION_WIDTH - ) // Make communal available so that communalInteractor.desiredScene accurately reflects // scene changes instead of just returning Blank. @@ -161,6 +163,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { shadeInteractor, powerManager, communalColors, + ambientTouchComponentFactory, kosmos.sceneDataSourceDelegator, ) @@ -215,63 +218,137 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test - fun onTouchEvent_topSwipeWhenCommunalOpen_doesNotIntercept() = + fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Touch event in the top swipe region is not intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse() + // Bouncer is visible. + fakeKeyguardTransitionRepository.sendTransitionSteps( + KeyguardState.GLANCEABLE_HUB, + KeyguardState.PRIMARY_BOUNCER, + testScope + ) + testableLooper.processAllMessages() + + // Touch events are not intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + // User activity is not sent to PowerManager. + verify(powerManager, times(0)).userActivity(any(), any(), any()) } } @Test - fun onTouchEvent_bottomSwipeWhenCommunalOpen_doesNotIntercept() = + fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Touch event in the bottom swipe region is not intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse() + // Shade shows up. + fakeShadeRepository.setQsExpansion(1.0f) + testableLooper.processAllMessages() + + // Touch events are not intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() } } @Test - fun onTouchEvent_topSwipeWhenDreaming_doesNotIntercept() = + fun onTouchEvent_containerViewDisposed_doesNotIntercept() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Device is dreaming. - fakeKeyguardRepository.setDreaming(true) - runCurrent() + // Touch events are intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + + // Container view disposed. + underTest.disposeView() - // Touch event in the top swipe region is not intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse() + // Touch events are not intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() } } @Test - fun onTouchEvent_bottomSwipeWhenDreaming_doesNotIntercept() = + fun lifecycle_initializedAfterConstruction() = + with(kosmos) { + val underTest = + GlanceableHubContainerController( + communalInteractor, + communalViewModel, + dialogFactory, + keyguardTransitionInteractor, + keyguardInteractor, + shadeInteractor, + powerManager, + communalColors, + ambientTouchComponentFactory, + kosmos.sceneDataSourceDelegator, + ) + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) + } + + @Test + fun lifecycle_createdAfterViewCreated() = + with(kosmos) { + val underTest = + GlanceableHubContainerController( + communalInteractor, + communalViewModel, + dialogFactory, + keyguardTransitionInteractor, + keyguardInteractor, + shadeInteractor, + powerManager, + communalColors, + ambientTouchComponentFactory, + kosmos.sceneDataSourceDelegator, + ) + + // Only initView without attaching a view as we don't want the flows to start collecting + // yet. + underTest.initView(View(context)) + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun lifecycle_startedAfterFlowsUpdate() { + // Flows start collecting due to test setup, causing the state to advance to STARTED. + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) + } + + @Test + fun lifecycle_resumedAfterCommunalShows() { + // Communal is open. + goToScene(CommunalScenes.Communal) + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + @Test + fun lifecycle_startedAfterCommunalCloses() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Device is dreaming. - fakeKeyguardRepository.setDreaming(true) - runCurrent() + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) + + // Communal closes. + goToScene(CommunalScenes.Blank) - // Touch event in the bottom swipe region is not intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse() + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) } } @Test - fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() = + fun lifecycle_startedAfterPrimaryBouncerShows() = with(kosmos) { testScope.runTest { // Communal is open. @@ -285,44 +362,49 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ) testableLooper.processAllMessages() - // Touch events are not intercepted. - assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() - // User activity is not sent to PowerManager. - verify(powerManager, times(0)).userActivity(any(), any(), any()) + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) } } @Test - fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() = + fun lifecycle_startedAfterAlternateBouncerShows() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Shade shows up. - fakeShadeRepository.setQsExpansion(1.0f) + // Bouncer is visible. + fakeKeyguardTransitionRepository.sendTransitionSteps( + KeyguardState.GLANCEABLE_HUB, + KeyguardState.ALTERNATE_BOUNCER, + testScope + ) testableLooper.processAllMessages() - // Touch events are not intercepted. - assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) } } @Test - fun onTouchEvent_containerViewDisposed_doesNotIntercept() = + fun lifecycle_createdAfterDisposeView() { + // Container view disposed. + underTest.disposeView() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun lifecycle_startedAfterShadeShows() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Touch events are intercepted. - assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() - - // Container view disposed. - underTest.disposeView() + // Shade shows up. + fakeShadeRepository.setQsExpansion(1.0f) + testableLooper.processAllMessages() - // Touch events are not intercepted. - assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) } } @@ -371,8 +453,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { private const val CONTAINER_WIDTH = 100 private const val CONTAINER_HEIGHT = 100 private const val RIGHT_SWIPE_REGION_WIDTH = 20 - private const val TOP_SWIPE_REGION_WIDTH = 20 - private const val BOTTOM_SWIPE_REGION_WIDTH = 20 /** * A touch down event right in the middle of the screen, to avoid being in any of the swipe @@ -389,17 +469,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ) private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0) - private val DOWN_IN_TOP_SWIPE_REGION_EVENT = - MotionEvent.obtain( - 0L, - 0L, - MotionEvent.ACTION_DOWN, - 0f, - TOP_SWIPE_REGION_WIDTH.toFloat(), - 0 - ) - private val DOWN_IN_BOTTOM_SWIPE_REGION_EVENT = - MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, CONTAINER_HEIGHT.toFloat(), 0) private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index da09579e1bde..d95cc2efc868 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -500,6 +500,46 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { } @Test + fun handleExternalTouch_intercepted_sendsOnTouch() { + // Accept dispatch and also intercept. + whenever(view.dispatchTouchEvent(any())).thenReturn(true) + whenever(view.onInterceptTouchEvent(any())).thenReturn(true) + + underTest.handleExternalTouch(DOWN_EVENT) + underTest.handleExternalTouch(MOVE_EVENT) + + // Once intercepted, both events are sent to the view. + verify(view).onTouchEvent(DOWN_EVENT) + verify(view).onTouchEvent(MOVE_EVENT) + } + + @Test + fun handleExternalTouch_notDispatched_interceptNotCalled() { + // Don't accept dispatch + whenever(view.dispatchTouchEvent(any())).thenReturn(false) + + underTest.handleExternalTouch(DOWN_EVENT) + + // Interception is not offered. + verify(view, never()).onInterceptTouchEvent(any()) + } + + @Test + fun handleExternalTouch_notIntercepted_onTouchNotSent() { + // Accept dispatch, but don't dispatch + whenever(view.dispatchTouchEvent(any())).thenReturn(true) + whenever(view.onInterceptTouchEvent(any())).thenReturn(false) + + underTest.handleExternalTouch(DOWN_EVENT) + underTest.handleExternalTouch(MOVE_EVENT) + + // Interception offered for both events, but onTouchEvent is never called. + verify(view).onInterceptTouchEvent(DOWN_EVENT) + verify(view).onInterceptTouchEvent(MOVE_EVENT) + verify(view, never()).onTouchEvent(any()) + } + + @Test fun testGetKeyguardMessageArea() = testScope.runTest { underTest.keyguardMessageArea diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 8dc4756569f1..d4b793720328 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -22,6 +22,7 @@ import android.content.applicationContext import android.os.fakeExecutorHandler import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.bouncerRepository +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.falsingCollector import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository @@ -41,6 +42,7 @@ import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInterac import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.model.sceneContainerPlugin import com.android.systemui.plugins.statusbar.statusBarStateController @@ -78,6 +80,8 @@ class KosmosJavaAdapter( val bouncerRepository by lazy { kosmos.bouncerRepository } val communalRepository by lazy { kosmos.fakeCommunalRepository } val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + val keyguardBouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository } + val keyguardInteractor by lazy { kosmos.keyguardInteractor } val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor } val powerRepository by lazy { kosmos.fakePowerRepository } |