Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 664054d9 authored by cocod's avatar cocod
Browse files

Add custom actions to improve Voice Access swipe on UMO in hub

When using "number" command, Voice Access speech recognition may
misinterpret non-fluent user input and outputs an unintended swipe
start position. This leads to scrolling of the entire hub instead
of the UMO, as the hub is horizontally scrollable.

This fix adds custom actions to enable reliable control of
switching players in UMO on hub, via the "show commands" voice
command.

Fixes: 378805659
Flag: EXEMPT bugfix
Test: atest MediaCarouselScrollHandlerTest
Test: manually with VoiceAccess
Change-Id: Id433d14652b54311c1bb63305574c6d961020d75
parent e9c2327c
Loading
Loading
Loading
Loading
+32 −9
Original line number Diff line number Diff line
@@ -1704,6 +1704,28 @@ private fun Umo(
    viewModel: BaseCommunalViewModel,
    contentScope: ContentScope?,
    modifier: Modifier = 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(
@@ -1716,6 +1738,7 @@ private fun Umo(
            UmoLegacy(viewModel, modifier)
        }
    }
}

@Composable
private fun UmoLegacy(viewModel: BaseCommunalViewModel, modifier: Modifier = Modifier) {
+18 −0
Original line number Diff line number Diff line
@@ -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
@@ -122,6 +123,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()
@@ -163,6 +165,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()

@@ -904,6 +908,20 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
            assertThat(selectedKey2).isEqualTo(key)
        }

    @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() =
+117 −4
Original line number Diff line number Diff line
@@ -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,19 @@ 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 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,6 +48,7 @@ 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)

    @Mock lateinit var mediaCarousel: MediaScrollView
@@ -53,6 +60,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 +73,7 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
    fun setup() {
        MockitoAnnotations.initMocks(this)
        executor = FakeExecutor(clock)
        whenever(mediaCarousel.contentContainer).thenReturn(contentContainer)
        mediaCarouselScrollHandler =
            MediaCarouselScrollHandler(
                mediaCarousel,
@@ -74,10 +85,9 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
                closeGuts,
                falsingManager,
                logSmartspaceImpression,
                logger
                logger,
            )
        mediaCarouselScrollHandler.playerWidthPlusPadding = carouselWidth

        whenever(mediaCarousel.touchListener).thenReturn(mediaCarouselScrollHandler.touchListener)
    }

@@ -128,4 +138,107 @@ 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)
        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)
        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
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -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] -->
+6 −0
Original line number Diff line number Diff line
@@ -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) {}

Loading