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

Commit b6c1e106 authored by Prince's avatar Prince Committed by Prince Donkor
Browse files

Allow accessibility talk back resize widgets

Fixes: 368056266
Test: atest ResizeableItemFrameViewModelTest
Flag: com.android.systemui.communal_widget_resizing
Change-Id: Ic230db93c9e862aa6b0e6cbafc009ae14e6242d3
parent a8ef53cc
Loading
Loading
Loading
Loading
+46 −0
Original line number Diff line number Diff line
@@ -181,9 +181,11 @@ import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.ui.viewmodel.ResizeInfo
import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
import com.android.systemui.communal.util.DensityUtils.Companion.adjustedDp
import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import kotlin.math.max
@@ -665,6 +667,7 @@ private fun ResizableItemFrameWrapper(
    maxHeightPx: Int,
    modifier: Modifier = Modifier,
    alpha: () -> Float = { 1f },
    viewModel: ResizeableItemFrameViewModel,
    onResize: (info: ResizeInfo) -> Unit = {},
    content: @Composable (modifier: Modifier) -> Unit,
) {
@@ -680,6 +683,7 @@ private fun ResizableItemFrameWrapper(
            enabled = enabled,
            alpha = alpha,
            modifier = modifier,
            viewModel = viewModel,
            onResize = onResize,
            minHeightPx = minHeightPx,
            maxHeightPx = maxHeightPx,
@@ -796,6 +800,14 @@ private fun BoxScope.CommunalHubLazyGrid(
                    false
                }

            val resizeableItemFrameViewModel =
                rememberViewModel(
                    key = item.size.span,
                    traceName = "ResizeableItemFrame.viewModel.$index",
                ) {
                    ResizeableItemFrameViewModel()
                }

            if (viewModel.isEditMode && dragDropState != null) {
                val isItemDragging = dragDropState.draggingItemKey == item.key
                val outlineAlpha by
@@ -821,6 +833,7 @@ private fun BoxScope.CommunalHubLazyGrid(
                                )
                            }
                            .thenIf(isItemDragging) { Modifier.zIndex(1f) },
                    viewModel = resizeableItemFrameViewModel,
                    onResize = { resizeInfo -> contentListState.resize(index, resizeInfo) },
                    minHeightPx = widgetSizeInfo.minHeightPx,
                    maxHeightPx = widgetSizeInfo.maxHeightPx,
@@ -843,6 +856,7 @@ private fun BoxScope.CommunalHubLazyGrid(
                            contentListState = contentListState,
                            interactionHandler = interactionHandler,
                            widgetSection = widgetSection,
                            resizeableItemFrameViewModel = resizeableItemFrameViewModel,
                        )
                    }
                }
@@ -857,6 +871,7 @@ private fun BoxScope.CommunalHubLazyGrid(
                    contentListState = contentListState,
                    interactionHandler = interactionHandler,
                    widgetSection = widgetSection,
                    resizeableItemFrameViewModel = resizeableItemFrameViewModel,
                )
            }
        }
@@ -1080,6 +1095,7 @@ private fun CommunalContent(
    contentListState: ContentListState,
    interactionHandler: RemoteViews.InteractionHandler?,
    widgetSection: CommunalAppWidgetSection,
    resizeableItemFrameViewModel: ResizeableItemFrameViewModel,
) {
    when (model) {
        is CommunalContentModel.WidgetContent.Widget ->
@@ -1093,6 +1109,7 @@ private fun CommunalContent(
                index,
                contentListState,
                widgetSection,
                resizeableItemFrameViewModel,
            )
        is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
        is CommunalContentModel.WidgetContent.DisabledWidget ->
@@ -1223,7 +1240,9 @@ private fun WidgetContent(
    index: Int,
    contentListState: ContentListState,
    widgetSection: CommunalAppWidgetSection,
    resizeableItemFrameViewModel: ResizeableItemFrameViewModel,
) {
    val coroutineScope = rememberCoroutineScope()
    val context = LocalContext.current
    val accessibilityLabel =
        remember(model, context) {
@@ -1234,6 +1253,10 @@ private fun WidgetContent(
    val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget)
    val unselectWidgetActionLabel =
        stringResource(R.string.accessibility_action_label_unselect_widget)

    val shrinkWidgetLabel = stringResource(R.string.accessibility_action_label_shrink_widget)
    val expandWidgetLabel = stringResource(R.string.accessibility_action_label_expand_widget)

    val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle()
    val selectedIndex =
        selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
@@ -1292,6 +1315,29 @@ private fun WidgetContent(
                                true
                            }
                        val actions = mutableListOf(deleteAction)

                        if (communalWidgetResizing() && resizeableItemFrameViewModel.canShrink()) {
                            actions.add(
                                CustomAccessibilityAction(shrinkWidgetLabel) {
                                    coroutineScope.launch {
                                        resizeableItemFrameViewModel.shrinkToNextAnchor()
                                    }
                                    true
                                }
                            )
                        }

                        if (communalWidgetResizing() && resizeableItemFrameViewModel.canExpand()) {
                            actions.add(
                                CustomAccessibilityAction(expandWidgetLabel) {
                                    coroutineScope.launch {
                                        resizeableItemFrameViewModel.expandToNextAnchor()
                                    }
                                    true
                                }
                            )
                        }

                        if (selectedIndex != null && selectedIndex != index) {
                            actions.add(
                                CustomAccessibilityAction(placeWidgetActionLabel) {
+1 −6
Original line number Diff line number Diff line
@@ -56,7 +56,6 @@ import com.android.compose.modifiers.thenIf
import com.android.systemui.communal.ui.viewmodel.DragHandle
import com.android.systemui.communal.ui.viewmodel.ResizeInfo
import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
import com.android.systemui.lifecycle.rememberViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine

@@ -192,16 +191,12 @@ fun ResizableItemFrame(
    maxHeightPx: Int = Int.MAX_VALUE,
    resizeMultiple: Int = 1,
    alpha: () -> Float = { 1f },
    viewModel: ResizeableItemFrameViewModel,
    onResize: (info: ResizeInfo) -> Unit = {},
    content: @Composable () -> Unit,
) {
    val brush = SolidColor(outlineColor)
    val onResizeUpdated by rememberUpdatedState(onResize)
    val viewModel =
        rememberViewModel(key = currentSpan, traceName = "ResizeableItemFrame.viewModel") {
            ResizeableItemFrameViewModel()
        }

    val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2
    val isDragging by
        remember(viewModel) {
+100 −0
Original line number Diff line number Diff line
@@ -366,6 +366,106 @@ class ResizeableItemFrameViewModelTest : SysuiTestCase() {
            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
        }

    @Test
    fun testCanExpand_atTopPosition_withMultipleAnchors_returnsTrue() =
        testScope.runTest {
            val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0)

            updateGridLayout(twoRowGrid)
            assertThat(underTest.canExpand()).isTrue()
            assertThat(underTest.bottomDragState.anchors.toList())
                .containsAtLeast(0 to 0f, 1 to 45f)
        }

    @Test
    fun testCanExpand_atTopPosition_withSingleAnchors_returnsFalse() =
        testScope.runTest {
            val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0)
            updateGridLayout(oneRowGrid)
            assertThat(underTest.canExpand()).isFalse()
        }

    @Test
    fun testCanExpand_atBottomPosition_withMultipleAnchors_returnsTrue() =
        testScope.runTest {
            val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1)
            updateGridLayout(twoRowGrid)
            assertThat(underTest.canExpand()).isTrue()
            assertThat(underTest.topDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f)
        }

    @Test
    fun testCanShrink_atMinimumHeight_returnsFalse() =
        testScope.runTest {
            val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0)
            updateGridLayout(oneRowGrid)
            assertThat(underTest.canShrink()).isFalse()
        }

    @Test
    fun testCanShrink_atFullSize_checksBottomDragState() = runTestWithSnapshots {
        val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0)
        updateGridLayout(twoSpanGrid)

        assertThat(underTest.canShrink()).isTrue()
        assertThat(underTest.bottomDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f)
    }

    @Test
    fun testResizeByAccessibility_expandFromBottom_usesTopDragState() = runTestWithSnapshots {
        val resizeInfo by collectLastValue(underTest.resizeInfo)

        val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1)
        updateGridLayout(twoSpanGrid)

        underTest.expandToNextAnchor()

        assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.TOP))
    }

    @Test
    fun testResizeByAccessibility_expandFromTop_usesBottomDragState() = runTestWithSnapshots {
        val resizeInfo by collectLastValue(underTest.resizeInfo)

        val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0)
        updateGridLayout(twoSpanGrid)

        underTest.expandToNextAnchor()

        assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.BOTTOM))
    }

    @Test
    fun testResizeByAccessibility_shrinkFromFull_usesBottomDragState() = runTestWithSnapshots {
        val resizeInfo by collectLastValue(underTest.resizeInfo)

        val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0)
        updateGridLayout(twoSpanGrid)

        underTest.shrinkToNextAnchor()

        assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM))
    }

    @Test
    fun testResizeByAccessibility_cannotResizeAtMinSize() = runTestWithSnapshots {
        val resizeInfo by collectLastValue(underTest.resizeInfo)

        // Set up grid at minimum size
        val minSizeGrid =
            singleSpanGrid.copy(
                totalSpans = 2,
                currentSpan = 1,
                minHeightPx = singleSpanGrid.minHeightPx,
                currentRow = 0,
            )
        updateGridLayout(minSizeGrid)

        underTest.shrinkToNextAnchor()

        assertThat(resizeInfo).isNull()
    }

    @Test(expected = IllegalArgumentException::class)
    fun testIllegalState_maxHeightLessThanMinHeight() =
        testScope.runTest {
+4 −0
Original line number Diff line number Diff line
@@ -1311,6 +1311,10 @@
    <string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string>
    <!-- Label for accessibility action to unselect a widget in edit mode. [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_label_unselect_widget">unselect widget</string>
    <!-- Label for accessibility action to shrink a widget in edit mode. [CHAR LIMIT=NONE] -->
    <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>
    <!-- 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] -->
+67 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ package com.android.systemui.communal.ui.viewmodel

import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.snapTo
import androidx.compose.runtime.snapshotFlow
import com.android.app.tracing.coroutines.coroutineScopeTraced as coroutineScope
import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -81,6 +82,72 @@ class ResizeableItemFrameViewModel : ExclusiveActivatable() {
            get() = roundDownToMultiple(getSpansForPx(minHeightPx))
    }

    /** Check if widget can expanded based on current drag states */
    fun canExpand(): Boolean {
        return getNextAnchor(bottomDragState, moveUp = false) != null ||
            getNextAnchor(topDragState, moveUp = true) != null
    }

    /** Check if widget can shrink based on current drag states */
    fun canShrink(): Boolean {
        return getNextAnchor(bottomDragState, moveUp = true) != null ||
            getNextAnchor(topDragState, moveUp = false) != null
    }

    /** Get the next anchor value in the specified direction */
    private fun getNextAnchor(state: AnchoredDraggableState<Int>, moveUp: Boolean): Int? {
        var nextAnchor: Int? = null
        var nextAnchorDiff = Int.MAX_VALUE
        val currentValue = state.currentValue

        for (i in 0 until state.anchors.size) {
            val anchor = state.anchors.anchorAt(i) ?: continue
            if (anchor == currentValue) continue

            val diff =
                if (moveUp) {
                    currentValue - anchor
                } else {
                    anchor - currentValue
                }

            if (diff in 1..<nextAnchorDiff) {
                nextAnchor = anchor
                nextAnchorDiff = diff
            }
        }

        return nextAnchor
    }

    /** Handle expansion to the next anchor */
    suspend fun expandToNextAnchor() {
        if (!canExpand()) return
        val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = false)
        if (bottomAnchor != null) {
            bottomDragState.snapTo(bottomAnchor)
            return
        }
        val topAnchor =
            getNextAnchor(
                state = topDragState,
                moveUp = true, // Moving up to expand
            )
        topAnchor?.let { topDragState.snapTo(it) }
    }

    /** Handle shrinking to the next anchor */
    suspend fun shrinkToNextAnchor() {
        if (!canShrink()) return
        val topAnchor = getNextAnchor(state = topDragState, moveUp = false)
        if (topAnchor != null) {
            topDragState.snapTo(topAnchor)
            return
        }
        val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = true)
        bottomAnchor?.let { bottomDragState.snapTo(it) }
    }

    /**
     * The layout information necessary in order to calculate the pixel offsets of the drag anchor
     * points.