diff options
12 files changed, 268 insertions, 21 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index c073b79ba5a3..a22fecf3688d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -64,12 +64,11 @@ fun CommunalContainer( transitions = sceneTransitions, ) - // Don't show hub mode UI if keyguard is not present. This is important since we're in the - // shade, which can be opened from many locations. - val isKeyguardShowing by viewModel.isKeyguardVisible.collectAsState(initial = false) + // Don't show hub mode UI if communal is not available. Communal is only available if it has + // been enabled via settings and either keyguard is showing, or, the device is currently + // dreaming. val isCommunalAvailable by viewModel.isCommunalAvailable.collectAsState() - - if (!isKeyguardShowing || !isCommunalAvailable) { + if (!isCommunalAvailable) { return } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 86279ef24ca7..1b7117f41bbb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -124,6 +124,7 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) + keyguardRepository.setKeyguardShowing(true) runCurrent() assertThat(isAvailable).isTrue() @@ -150,12 +151,27 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(secondaryUser) + keyguardRepository.setKeyguardShowing(true) runCurrent() assertThat(isAvailable).isFalse() } @Test + fun isCommunalAvailable_whenDreaming_true() = + testScope.runTest { + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setSelectedUserInfo(mainUser) + keyguardRepository.setDreaming(true) + runCurrent() + + assertThat(isAvailable).isTrue() + } + + @Test fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() = testScope.runTest { collectLastValue(underTest.isCommunalAvailable) @@ -163,6 +179,7 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) + keyguardRepository.setKeyguardShowing(true) runCurrent() assertThat(widgetRepository.isHostActive()).isTrue() 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 new file mode 100644 index 000000000000..74c197075461 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.touch; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.verify; + +import android.view.GestureDetector; +import android.view.MotionEvent; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.shared.system.InputChannelCompat; +import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.phone.CentralSurfaces; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class CommunalTouchHandlerTest extends SysuiTestCase { + @Mock + CentralSurfaces mCentralSurfaces; + @Mock + NotificationShadeWindowController mNotificationShadeWindowController; + @Mock + DreamTouchHandler.TouchSession mTouchSession; + CommunalTouchHandler mTouchHandler; + + private static final int INITIATION_WIDTH = 20; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mTouchHandler = new CommunalTouchHandler( + Optional.of(mCentralSurfaces), + mNotificationShadeWindowController, + INITIATION_WIDTH); + } + + @Test + public void testSessionStartForcesShadeOpen() { + mTouchHandler.onSessionStart(mTouchSession); + verify(mNotificationShadeWindowController).setForcePluginOpen(true, mTouchHandler); + } + + @Test + public void testEventPropagation() { + final MotionEvent motionEvent = Mockito.mock(MotionEvent.class); + + final ArgumentCaptor<InputChannelCompat.InputEventListener> + inputEventListenerArgumentCaptor = + ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); + + mTouchHandler.onSessionStart(mTouchSession); + verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture()); + inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent); + verify(mCentralSurfaces).handleDreamTouch(motionEvent); + } + + @Test + public void testTouchPilferingOnScroll() { + final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class); + final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class); + + final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor = + ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); + + mTouchHandler.onSessionStart(mTouchSession); + verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture()); + + assertThat(gestureListenerArgumentCaptor.getValue() + .onScroll(motionEvent1, motionEvent2, 1, 1)) + .isTrue(); + } +} diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 09b02f851c3c..4209c1f6a732 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1805,6 +1805,9 @@ <dimen name="dream_overlay_complication_smartspace_padding">24dp</dimen> <dimen name="dream_overlay_complication_smartspace_max_width">408dp</dimen> + <!-- The width of the swipe target to initiate opening communal hub over dreams. --> + <dimen name="communal_gesture_initiation_width">48dp</dimen> + <!-- The position of the end guide, which dream overlay complications can align their start with if their end is aligned with the parent end. Represented as the percentage over from the start of the parent container. --> diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 44b0383e12c6..4c5871d796b1 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -68,21 +68,23 @@ constructor( private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter ) { - /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean get() = communalRepository.isCommunalEnabled - /** Whether communal features are enabled and available. */ - val isCommunalAvailable: StateFlow<Boolean> = + val isCommunalAvailable = flowOf(isCommunalEnabled) .flatMapLatest { enabled -> if (enabled) combine( keyguardInteractor.isEncryptedOrLockdown, userRepository.selectedUserInfo, - ) { isEncryptedOrLockdown, selectedUserInfo -> - !isEncryptedOrLockdown && selectedUserInfo.isMain + keyguardInteractor.isKeyguardVisible, + keyguardInteractor.isDreaming, + ) { isEncryptedOrLockdown, selectedUserInfo, isKeyguardVisible, isDreaming -> + !isEncryptedOrLockdown && + selectedUserInfo.isMain && + (isKeyguardVisible || isDreaming) } else flowOf(false) } @@ -154,8 +156,6 @@ constructor( it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Communal } - val isKeyguardVisible: Flow<Boolean> = keyguardInteractor.isKeyguardVisible - /** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */ fun onSceneChanged(newScene: CommunalSceneKey) { communalRepository.setDesiredScene(newScene) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index acc7981dd460..1e64d3f9cedc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -35,8 +35,6 @@ abstract class BaseCommunalViewModel( ) { val isCommunalAvailable: StateFlow<Boolean> = communalInteractor.isCommunalAvailable - val isKeyguardVisible: Flow<Boolean> = communalInteractor.isKeyguardVisible - val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene /** Whether widgets are currently being re-ordered. */ diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java new file mode 100644 index 000000000000..c9b56a2ebd9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.touch; + +import static com.android.systemui.dreams.touch.dagger.ShadeModule.COMMUNAL_GESTURE_INITIATION_WIDTH; + +import android.graphics.Rect; +import android.graphics.Region; +import android.view.GestureDetector; +import android.view.MotionEvent; + +import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.phone.CentralSurfaces; + +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Named; + +/** {@link DreamTouchHandler} responsible for handling touches to open communal hub. **/ +public class CommunalTouchHandler implements DreamTouchHandler { + private final int mInitiationWidth; + private final NotificationShadeWindowController mNotificationShadeWindowController; + private final Optional<CentralSurfaces> mCentralSurfaces; + + @Inject + public CommunalTouchHandler( + Optional<CentralSurfaces> centralSurfaces, + NotificationShadeWindowController notificationShadeWindowController, + @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth) { + mInitiationWidth = initiationWidth; + mCentralSurfaces = centralSurfaces; + mNotificationShadeWindowController = notificationShadeWindowController; + } + + @Override + public void onSessionStart(TouchSession session) { + mCentralSurfaces.ifPresent(surfaces -> handleSessionStart(surfaces, session)); + } + + @Override + public void getTouchInitiationRegion(Rect bounds, Region region) { + final Rect outBounds = new Rect(bounds); + outBounds.inset(outBounds.width() - mInitiationWidth, 0, 0, 0); + region.op(outBounds, Region.Op.UNION); + } + + private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) { + // Force the notification shade window open (otherwise the hub won't show while swiping). + mNotificationShadeWindowController.setForcePluginOpen(true, this); + + session.registerInputListener(ev -> { + surfaces.handleDreamTouch((MotionEvent) ev); + if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { + var unused = session.pop(); + } + }); + + session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + float distanceY) { + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return true; + } + }); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java index 94fe4bd04dab..0f08d376f37c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java @@ -18,11 +18,13 @@ package com.android.systemui.dreams.touch.dagger; import android.content.res.Resources; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dreams.touch.CommunalTouchHandler; import com.android.systemui.dreams.touch.DreamTouchHandler; import com.android.systemui.dreams.touch.ShadeTouchHandler; +import com.android.systemui.res.R; +import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoSet; @@ -33,7 +35,7 @@ import javax.inject.Named; * Dependencies for swipe down to notification over dream. */ @Module -public class ShadeModule { +public abstract class ShadeModule { /** * The height, defined in pixels, of the gesture initiation region at the top of the screen for * swiping down notifications. @@ -41,15 +43,22 @@ public class ShadeModule { public static final String NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT = "notification_shade_gesture_initiation_height"; + /** Width of swipe gesture edge to show communal hub. */ + public static final String COMMUNAL_GESTURE_INITIATION_WIDTH = + "communal_gesture_initiation_width"; + /** * Provides {@link ShadeTouchHandler} to handle notification swipe down over dream. */ - @Provides + @Binds @IntoSet - public static DreamTouchHandler providesNotificationShadeTouchHandler( - ShadeTouchHandler touchHandler) { - return touchHandler; - } + public abstract DreamTouchHandler providesNotificationShadeTouchHandler( + ShadeTouchHandler touchHandler); + + /** Provides {@link CommunalTouchHandler}. */ + @Binds + @IntoSet + public abstract DreamTouchHandler bindCommunalTouchHandler(CommunalTouchHandler touchHandler); /** * Provides the height of the gesture area for notification swipe down. @@ -59,4 +68,13 @@ public class ShadeModule { public static int providesNotificationShadeGestureRegionHeight(@Main Resources resources) { return resources.getDimensionPixelSize(R.dimen.dream_overlay_status_bar_height); } + + /** + * Provides the width of the gesture area for swiping open communal hub. + */ + @Provides + @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) + public static int providesCommunalGestureInitiationWidth(@Main Resources resources) { + return resources.getDimensionPixelSize(R.dimen.communal_gesture_initiation_width); + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 8c852cd04738..863bb36e4ed0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -272,6 +272,14 @@ public class NotificationShadeWindowViewController implements Dumpable { return result; } + /** + * Handle a touch event while dreaming by forwarding the event to the content view. + * @param event The event to forward. + */ + public void handleDreamTouch(MotionEvent event) { + mView.dispatchTouchEvent(event); + } + /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */ public void setupExpandedStatusBar() { mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller); 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 90cba409a787..40194361e12b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.UserHandle; +import android.view.MotionEvent; import android.view.RemoteAnimationAdapter; import android.view.View; import android.window.RemoteTransition; @@ -277,6 +278,13 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { 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). + */ + void handleDreamTouch(MotionEvent event); + boolean isBouncerShowing(); boolean isBouncerShowingScrimmed(); 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 7dc4b96ea154..60dfaa790ec9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone import android.content.Intent +import android.view.MotionEvent import androidx.lifecycle.LifecycleRegistry import com.android.keyguard.AuthKeyguardMessageArea import com.android.systemui.animation.ActivityLaunchAnimator @@ -78,6 +79,7 @@ abstract class CentralSurfacesEmptyImpl : CentralSurfaces { override fun updateScrimController() {} override fun shouldIgnoreTouch() = false override fun isDeviceInteractive() = false + override fun handleDreamTouch(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 6e3aabf7c754..266c19c09941 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -80,6 +80,7 @@ import android.util.Log; import android.view.Display; import android.view.IRemoteAnimationRunner; import android.view.IWindowManager; +import android.view.MotionEvent; import android.view.ThreadedRenderer; import android.view.View; import android.view.WindowInsets; @@ -2903,6 +2904,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }; @Override + public void handleDreamTouch(MotionEvent event) { + getNotificationShadeWindowViewController().handleDreamTouch(event); + } + + @Override public void awakenDreams() { mUiBgExecutor.execute(() -> { try { |