diff options
| author | 2024-04-24 01:15:35 +0000 | |
|---|---|---|
| committer | 2024-04-24 01:15:35 +0000 | |
| commit | 48556f26f527380ab033bac07a46894d883fc1eb (patch) | |
| tree | 35375abc1ea5970f19a80a8e4156b213aac3fe03 | |
| parent | 2e086e365bde1ab16d2580fe69e09e553e197862 (diff) | |
| parent | 86fa77ab95757a714031c425ea8bdc08c0324c9a (diff) | |
Merge changes I92b9c220,I31a55ca1,I536a2e1e into main
* changes:
Fix ShadeTouchHandler over the lock screen
Use TouchMonitor for touch handling over hub
Stop overlay touch handling when the bouncer or glanceable hub are visible over the dream
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 } |