diff options
author | 2025-02-10 11:32:51 -0800 | |
---|---|---|
committer | 2025-02-10 11:32:51 -0800 | |
commit | 52cb45a7e98b28cba214d43b15d460123d4119ed (patch) | |
tree | 7bfcd3bad0486fea8f656f884f2b5a8beac52109 | |
parent | 502c8a76fa49d3cc821cb9eada96acd6327ae1b9 (diff) | |
parent | 7b89ab31a4ac56a5d65eaf6d37746861c3009044 (diff) |
Merge "Reapply "Add custom actions to improve Voice Access swipe on UMO in hub"" into main
7 files changed, 240 insertions, 27 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 9c57efc24a22..418a7a52a97e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1705,15 +1705,38 @@ private fun Umo( contentScope: ContentScope?, modifier: Modifier = Modifier, ) { - if (SceneContainerFlag.isEnabled && contentScope != null) { - contentScope.MediaCarousel( - modifier = modifier.fillMaxSize(), - isVisible = true, - mediaHost = viewModel.mediaHost, - carouselController = viewModel.mediaCarouselController, - ) - } else { - UmoLegacy(viewModel, modifier) + val showNextActionLabel = stringResource(R.string.accessibility_action_label_umo_show_next) + val showPreviousActionLabel = + stringResource(R.string.accessibility_action_label_umo_show_previous) + + Box( + modifier = + modifier.thenIf(!viewModel.isEditMode) { + Modifier.semantics { + customActions = + listOf( + CustomAccessibilityAction(showNextActionLabel) { + viewModel.onShowNextMedia() + true + }, + CustomAccessibilityAction(showPreviousActionLabel) { + viewModel.onShowPreviousMedia() + true + }, + ) + } + } + ) { + if (SceneContainerFlag.isEnabled && contentScope != null) { + contentScope.MediaCarousel( + modifier = modifier.fillMaxSize(), + isVisible = true, + mediaHost = viewModel.mediaHost, + carouselController = viewModel.mediaCarouselController, + ) + } else { + UmoLegacy(viewModel, modifier) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 85155157eda2..433894b58350 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -78,6 +78,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.controller.mediaCarouselController +import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor @@ -120,6 +121,7 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost + @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler @Mock private lateinit var metricsLogger: CommunalMetricsLogger private val kosmos = testKosmos() @@ -161,6 +163,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) whenever(mediaHost.visible).thenReturn(true) + whenever(kosmos.mediaCarouselController.mediaCarouselScrollHandler) + .thenReturn(mediaCarouselScrollHandler) kosmos.powerInteractor.setAwakeForTest() @@ -903,6 +907,20 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + fun onShowPreviousMedia_scrollHandler_isCalled() = + testScope.runTest { + underTest.onShowPreviousMedia() + verify(mediaCarouselScrollHandler).scrollByStep(-1) + } + + @Test + fun onShowNextMedia_scrollHandler_isCalled() = + testScope.runTest { + underTest.onShowNextMedia() + verify(mediaCarouselScrollHandler).scrollByStep(1) + } + + @Test @EnableFlags(FLAG_BOUNCER_UI_REVAMP) fun uiIsBlurred_whenPrimaryBouncerIsShowing() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt index d073cf1ac9db..c2f0ab92b32b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt @@ -16,8 +16,11 @@ package com.android.systemui.media.controls.ui.view +import android.content.res.Resources import android.testing.TestableLooper import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -25,16 +28,21 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PageIndicator import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.Mock import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -42,7 +50,9 @@ import org.mockito.MockitoAnnotations class MediaCarouselScrollHandlerTest : SysuiTestCase() { private val carouselWidth = 1038 + private val settingsButtonWidth = 200 private val motionEventUp = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0) + private lateinit var testableLooper: TestableLooper @Mock lateinit var mediaCarousel: MediaScrollView @Mock lateinit var pageIndicator: PageIndicator @@ -53,6 +63,9 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { @Mock lateinit var falsingManager: FalsingManager @Mock lateinit var logSmartspaceImpression: (Boolean) -> Unit @Mock lateinit var logger: MediaUiEventLogger + @Mock lateinit var contentContainer: ViewGroup + @Mock lateinit var settingsButton: View + @Mock lateinit var resources: Resources lateinit var executor: FakeExecutor private val clock = FakeSystemClock() @@ -63,6 +76,11 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(clock) + testableLooper = TestableLooper.get(this) + PhysicsAnimatorTestUtils.prepareForTest() + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + + whenever(mediaCarousel.contentContainer).thenReturn(contentContainer) mediaCarouselScrollHandler = MediaCarouselScrollHandler( mediaCarousel, @@ -74,13 +92,17 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { closeGuts, falsingManager, logSmartspaceImpression, - logger + logger, ) mediaCarouselScrollHandler.playerWidthPlusPadding = carouselWidth - whenever(mediaCarousel.touchListener).thenReturn(mediaCarouselScrollHandler.touchListener) } + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() + } + @Test fun testCarouselScroll_shortScroll() { whenever(mediaCarousel.isLayoutRtl).thenReturn(false) @@ -128,4 +150,109 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() { verify(mediaCarousel).smoothScrollTo(eq(0), anyInt()) } + + @Test + fun testCarouselScrollByStep_scrollRight() { + setupMediaContainer(visibleIndex = 0) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel).smoothScrollTo(eq(carouselWidth), anyInt()) + } + + @Test + fun testCarouselScrollByStep_scrollLeft() { + setupMediaContainer(visibleIndex = 1) + + mediaCarouselScrollHandler.scrollByStep(-1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel).smoothScrollTo(eq(0), anyInt()) + } + + @Test + fun testCarouselScrollByStep_scrollRight_alreadyAtEnd() { + setupMediaContainer(visibleIndex = 1) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat()) + } + + @Test + fun testCarouselScrollByStep_scrollLeft_alreadyAtStart() { + setupMediaContainer(visibleIndex = 0) + + mediaCarouselScrollHandler.scrollByStep(-1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat()) + } + + @Test + fun testCarouselScrollByStep_scrollLeft_alreadyAtStart_isRTL() { + setupMediaContainer(visibleIndex = 0) + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + whenever(mediaCarousel.isLayoutRtl).thenReturn(true) + + mediaCarouselScrollHandler.scrollByStep(-1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat()) + } + + @Test + fun testCarouselScrollByStep_scrollRight_alreadyAtEnd_isRTL() { + setupMediaContainer(visibleIndex = 1) + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + whenever(mediaCarousel.isLayoutRtl).thenReturn(true) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat()) + } + + @Test + fun testScrollByStep_noScroll_notDismissible() { + setupMediaContainer(visibleIndex = 1, showsSettingsButton = false) + + mediaCarouselScrollHandler.scrollByStep(1) + clock.advanceTime(DISMISS_DELAY) + executor.runAllReady() + + verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt()) + verify(mediaCarousel, never()).animationTargetX = anyFloat() + } + + private fun setupMediaContainer(visibleIndex: Int, showsSettingsButton: Boolean = true) { + whenever(contentContainer.childCount).thenReturn(2) + val child1: View = mock() + val child2: View = mock() + whenever(child1.left).thenReturn(0) + whenever(child2.left).thenReturn(carouselWidth) + whenever(contentContainer.getChildAt(0)).thenReturn(child1) + whenever(contentContainer.getChildAt(1)).thenReturn(child2) + + whenever(settingsButton.width).thenReturn(settingsButtonWidth) + whenever(settingsButton.context).thenReturn(context) + whenever(settingsButton.resources).thenReturn(resources) + whenever(settingsButton.resources.getDimensionPixelSize(anyInt())).thenReturn(20) + mediaCarouselScrollHandler.onSettingsButtonUpdated(settingsButton) + + mediaCarouselScrollHandler.visibleMediaIndex = visibleIndex + mediaCarouselScrollHandler.showsSettingsButton = showsSettingsButton + } } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d18a90a17abe..86292039d93d 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1351,6 +1351,10 @@ <string name="accessibility_action_label_shrink_widget">Decrease height</string> <!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] --> <string name="accessibility_action_label_expand_widget">Increase height</string> + <!-- Label for accessibility action to show the next media player. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_umo_show_next">Show next</string> + <!-- Label for accessibility action to show the previous media player. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_umo_show_previous">Show previous</string> <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] --> <string name="communal_widgets_disclaimer_title">Lock screen widgets</string> <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] --> 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 49003a735fbd..a4860dfc47ce 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 @@ -202,6 +202,12 @@ abstract class BaseCommunalViewModel( /** Called as the user request to show the customize widget button. */ open fun onLongClick() {} + /** Called as the user requests to switch to the previous player in UMO. */ + open fun onShowPreviousMedia() {} + + /** Called as the user requests to switch to the next player in UMO. */ + open fun onShowNextMedia() {} + /** Called as the UI determines that a new widget has been added to the grid. */ open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 4bc44005d2fc..dd4018a9d7b9 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -254,6 +254,14 @@ constructor( } } + override fun onShowPreviousMedia() { + mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(-1) + } + + override fun onShowNextMedia() { + mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(1) + } + override fun onTapWidget(componentName: ComponentName, rank: Int) { metricsLogger.logTapWidget(componentName.flattenToString(), rank) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt index d63c2e07b94f..0107a5278e3e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt @@ -23,11 +23,11 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider +import androidx.annotation.VisibleForTesting import androidx.core.view.GestureDetectorCompat import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringForce import com.android.app.tracing.TraceStateLogger -import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.Utils import com.android.systemui.Gefingerpoken import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS @@ -38,9 +38,10 @@ import com.android.systemui.res.R import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.DelayableExecutor import com.android.wm.shell.shared.animation.PhysicsAnimator +import kotlin.math.sign private const val FLING_SLOP = 1000000 -private const val DISMISS_DELAY = 100L +@VisibleForTesting const val DISMISS_DELAY = 100L private const val SCROLL_DELAY = 100L private const val RUBBERBAND_FACTOR = 0.2f private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f @@ -64,7 +65,7 @@ class MediaCarouselScrollHandler( private val closeGuts: (immediate: Boolean) -> Unit, private val falsingManager: FalsingManager, private val logSmartspaceImpression: (Boolean) -> Unit, - private val logger: MediaUiEventLogger + private val logger: MediaUiEventLogger, ) { /** Trace state logger for media carousel visibility */ private val visibleStateLogger = TraceStateLogger("$TAG#visibleToUser") @@ -96,7 +97,7 @@ class MediaCarouselScrollHandler( /** What's the currently visible player index? */ var visibleMediaIndex: Int = 0 - private set + @VisibleForTesting set /** How much are we scrolled into the current media? */ private var scrollIntoCurrentMedia: Int = 0 @@ -137,14 +138,14 @@ class MediaCarouselScrollHandler( eStart: MotionEvent?, eCurrent: MotionEvent, vX: Float, - vY: Float + vY: Float, ) = onFling(vX, vY) override fun onScroll( down: MotionEvent?, lastMotion: MotionEvent, distanceX: Float, - distanceY: Float + distanceY: Float, ) = onScroll(down!!, lastMotion, distanceX) override fun onDown(e: MotionEvent): Boolean { @@ -157,6 +158,7 @@ class MediaCarouselScrollHandler( val touchListener = object : Gefingerpoken { override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) + override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) } @@ -168,7 +170,7 @@ class MediaCarouselScrollHandler( scrollX: Int, scrollY: Int, oldScrollX: Int, - oldScrollY: Int + oldScrollY: Int, ) { if (playerWidthPlusPadding == 0) { return @@ -177,7 +179,7 @@ class MediaCarouselScrollHandler( val relativeScrollX = scrollView.relativeScrollX onMediaScrollingChanged( relativeScrollX / playerWidthPlusPadding, - relativeScrollX % playerWidthPlusPadding + relativeScrollX % playerWidthPlusPadding, ) } } @@ -209,7 +211,7 @@ class MediaCarouselScrollHandler( 0, carouselWidth, carouselHeight, - cornerRadius.toFloat() + cornerRadius.toFloat(), ) } } @@ -235,7 +237,7 @@ class MediaCarouselScrollHandler( getMaxTranslation().toFloat(), 0.0f, 1.0f, - Math.abs(contentTranslation) + Math.abs(contentTranslation), ) val settingsTranslation = (1.0f - settingsOffset) * @@ -323,7 +325,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = 0.0f, - config = translationConfig + config = translationConfig, ) .start() scrollView.animationTargetX = newTranslation @@ -391,7 +393,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = 0.0f, - config = translationConfig + config = translationConfig, ) .start() } else { @@ -430,7 +432,7 @@ class MediaCarouselScrollHandler( CONTENT_TRANSLATION, newTranslation, startVelocity = vX, - config = translationConfig + config = translationConfig, ) .start() scrollView.animationTargetX = newTranslation @@ -583,10 +585,35 @@ class MediaCarouselScrollHandler( // We need to post this to wait for the active player becomes visible. mainExecutor.executeDelayed( { scrollView.smoothScrollTo(view.left, scrollView.scrollY) }, - SCROLL_DELAY + SCROLL_DELAY, ) } + /** + * Scrolls the media carousel by the number of players specified by [step]. If scrolling beyond + * the carousel's bounds: + * - If the carousel is not dismissible, the settings button is displayed. + * - If the carousel is dismissible, no action taken. + * + * @param step A positive number means next, and negative means previous. + */ + fun scrollByStep(step: Int) { + val destIndex = visibleMediaIndex + step + if (destIndex >= mediaContent.childCount || destIndex < 0) { + if (!showsSettingsButton) return + var translation = getMaxTranslation() * sign(-step.toFloat()) + translation = if (isRtl) -translation else translation + PhysicsAnimator.getInstance(this) + .spring(CONTENT_TRANSLATION, translation, config = translationConfig) + .start() + scrollView.animationTargetX = translation + } else if (scrollView.getContentTranslation() != 0.0f) { + resetTranslation(true) + } else { + scrollToPlayer(destIndex = destIndex) + } + } + companion object { private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") { |