From b6c1e106cf8037f48ea2ffcc41b7900a67bbcd5d Mon Sep 17 00:00:00 2001 From: Prince Date: Mon, 21 Oct 2024 18:45:50 +0000 Subject: Allow accessibility talk back resize widgets Fixes: 368056266 Test: atest ResizeableItemFrameViewModelTest Flag: com.android.systemui.communal_widget_resizing Change-Id: Ic230db93c9e862aa6b0e6cbafc009ae14e6242d3 --- .../systemui/communal/ui/compose/CommunalHub.kt | 46 ++++++++++ .../communal/ui/compose/ResizeableItemFrame.kt | 7 +- .../viewmodel/ResizeableItemFrameViewModelTest.kt | 100 +++++++++++++++++++++ packages/SystemUI/res/values/strings.xml | 4 + .../ui/viewmodel/ResizeableItemFrameViewModel.kt | 67 ++++++++++++++ 5 files changed, 218 insertions(+), 6 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 476cced6a03d..e329aaee9a06 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 @@ -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) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt index 521330f60fa8..8e85432f4f36 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt @@ -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) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt index 22b114c632cd..0269577af789 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt @@ -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 { diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 0aa5ccf7a2b4..c838180f9541 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1311,6 +1311,10 @@ Anyone can view widgets on your lock screen, even if your tablet\'s locked. unselect widget + + Decrease height + + Increase height Lock screen widgets diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt index db4bee781a58..bde5d0f87a66 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt @@ -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, 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..