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

Commit a05067b3 authored by Fabian Kozynski's avatar Fabian Kozynski Committed by Android (Google) Code Review
Browse files

Merge "Add accessibility for new tiles." into main

parents feaa25f8 33866840
Loading
Loading
Loading
Loading
+270 −0
Original line number Diff line number Diff line
/*
 * 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.qs.panels.ui.viewmodel

import android.content.res.Resources
import android.content.res.mainResources
import android.service.quicksettings.Tile
import android.widget.Button
import android.widget.Switch
import androidx.compose.ui.semantics.Role
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class TileUiStateTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val resources: Resources
        get() = kosmos.mainResources

    @Test
    fun stateUnavailable_secondaryLabelNotmodified() {
        val testString = "TEST STRING"
        val state =
            QSTile.State().apply {
                state = Tile.STATE_UNAVAILABLE
                secondaryLabel = testString
            }

        val uiState = state.toUiState()

        assertThat(uiState.state).isEqualTo(Tile.STATE_UNAVAILABLE)
    }

    @Test
    fun accessibilityRole_switch() {
        val stateSwitch =
            QSTile.State().apply { expandedAccessibilityClassName = Switch::class.java.name }
        val uiState = stateSwitch.toUiState()
        assertThat(uiState.accessibilityRole).isEqualTo(Role.Switch)
    }

    @Test
    fun accessibilityRole_button() {
        val stateButton =
            QSTile.State().apply { expandedAccessibilityClassName = Button::class.java.name }
        val uiState = stateButton.toUiState()
        assertThat(uiState.accessibilityRole).isEqualTo(Role.Button)
    }

    @Test
    fun accessibilityRole_switchWithSecondaryClick() {
        val stateSwitchWithSecondaryClick =
            QSTile.State().apply {
                expandedAccessibilityClassName = Switch::class.java.name
                handlesSecondaryClick = true
            }
        val uiState = stateSwitchWithSecondaryClick.toUiState()
        assertThat(uiState.accessibilityRole).isEqualTo(Role.Button)
    }

    @Test
    fun switchInactive_secondaryLabelNotModified() {
        val testString = "TEST STRING"
        val state =
            QSTile.State().apply {
                expandedAccessibilityClassName = Switch::class.java.name
                state = Tile.STATE_INACTIVE
                secondaryLabel = testString
            }

        val uiState = state.toUiState()

        assertThat(uiState.secondaryLabel).isEqualTo(testString)
    }

    @Test
    fun switchActive_secondaryLabelNotModified() {
        val testString = "TEST STRING"
        val state =
            QSTile.State().apply {
                expandedAccessibilityClassName = Switch::class.java.name
                state = Tile.STATE_ACTIVE
                secondaryLabel = testString
            }

        val uiState = state.toUiState()

        assertThat(uiState.secondaryLabel).isEqualTo(testString)
    }

    @Test
    fun buttonInactive_secondaryLabelNotModifiedWhenEmpty() {
        val state =
            QSTile.State().apply {
                expandedAccessibilityClassName = Button::class.java.name
                state = Tile.STATE_INACTIVE
                secondaryLabel = ""
            }

        val uiState = state.toUiState()

        assertThat(uiState.secondaryLabel).isEmpty()
    }

    @Test
    fun buttonActive_secondaryLabelNotModifiedWhenEmpty() {
        val state =
            QSTile.State().apply {
                expandedAccessibilityClassName = Button::class.java.name
                state = Tile.STATE_ACTIVE
                secondaryLabel = ""
            }

        val uiState = state.toUiState()

        assertThat(uiState.secondaryLabel).isEmpty()
    }

    @Test
    fun buttonUnavailable_emptySecondaryLabel_default() {
        val state =
            QSTile.State().apply {
                expandedAccessibilityClassName = Button::class.java.name
                state = Tile.STATE_UNAVAILABLE
                secondaryLabel = ""
            }

        val uiState = state.toUiState()

        assertThat(uiState.secondaryLabel).isEqualTo(resources.getString(R.string.tile_unavailable))
    }

    @Test
    fun switchUnavailable_emptySecondaryLabel_defaultUnavailable() {
        val state =
            QSTile.State().apply {
                expandedAccessibilityClassName = Switch::class.java.name
                state = Tile.STATE_UNAVAILABLE
                secondaryLabel = ""
            }

        val uiState = state.toUiState()

        assertThat(uiState.secondaryLabel).isEqualTo(resources.getString(R.string.tile_unavailable))
    }

    @Test
    fun switchInactive_emptySecondaryLabel_defaultOff() {
        val state =
            QSTile.State().apply {
                expandedAccessibilityClassName = Switch::class.java.name
                state = Tile.STATE_INACTIVE
                secondaryLabel = ""
            }

        val uiState = state.toUiState()

        assertThat(uiState.secondaryLabel).isEqualTo(resources.getString(R.string.switch_bar_off))
    }

    @Test
    fun switchActive_emptySecondaryLabel_defaultOn() {
        val state =
            QSTile.State().apply {
                expandedAccessibilityClassName = Switch::class.java.name
                state = Tile.STATE_ACTIVE
                secondaryLabel = ""
            }

        val uiState = state.toUiState()

        assertThat(uiState.secondaryLabel).isEqualTo(resources.getString(R.string.switch_bar_on))
    }

    @Test
    fun disabledByPolicy_inactive_appearsAsUnavailable() {
        val stateDisabledByPolicy =
            QSTile.State().apply {
                state = Tile.STATE_INACTIVE
                disabledByPolicy = true
            }

        val uiState = stateDisabledByPolicy.toUiState()

        assertThat(uiState.state).isEqualTo(Tile.STATE_UNAVAILABLE)
    }

    @Test
    fun disabledByPolicy_active_appearsAsUnavailable() {
        val stateDisabledByPolicy =
            QSTile.State().apply {
                state = Tile.STATE_ACTIVE
                disabledByPolicy = true
            }

        val uiState = stateDisabledByPolicy.toUiState()

        assertThat(uiState.state).isEqualTo(Tile.STATE_UNAVAILABLE)
    }

    @Test
    fun disabledByPolicy_clickLabel() {
        val stateDisabledByPolicy =
            QSTile.State().apply {
                state = Tile.STATE_INACTIVE
                disabledByPolicy = true
            }

        val uiState = stateDisabledByPolicy.toUiState()
        assertThat(uiState.accessibilityUiState.clickLabel)
            .isEqualTo(
                resources.getString(
                    R.string.accessibility_tile_disabled_by_policy_action_description
                )
            )
    }

    @Test
    fun notDisabledByPolicy_clickLabel_null() {
        val stateDisabledByPolicy =
            QSTile.State().apply {
                state = Tile.STATE_INACTIVE
                disabledByPolicy = false
            }

        val uiState = stateDisabledByPolicy.toUiState()
        assertThat(uiState.accessibilityUiState.clickLabel).isNull()
    }

    @Test
    fun disabledByPolicy_unavailableInStateDescription() {
        val state =
            QSTile.State().apply {
                disabledByPolicy = true
                state = Tile.STATE_INACTIVE
            }

        val uiState = state.toUiState()
        assertThat(uiState.accessibilityUiState.stateDescription)
            .contains(resources.getString(R.string.tile_unavailable))
    }

    private fun QSTile.State.toUiState() = toUiState(resources)
}

private val TileUiState.accessibilityRole: Role
    get() = accessibilityUiState.accessibilityRole
+13 −12
Original line number Diff line number Diff line
@@ -158,7 +158,7 @@ constructor(
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
        savedInstanceState: Bundle?,
    ): View {
        val context = inflater.context
        return ComposeView(context).apply {
@@ -181,7 +181,7 @@ constructor(
                                    notificationScrimClippingParams.bottom,
                                    notificationScrimClippingParams.radius,
                                )
                            }
                            },
                    ) {
                        AnimatedContent(targetState = qsState) {
                            when (it) {
@@ -272,7 +272,7 @@ constructor(
        qsExpansionFraction: Float,
        panelExpansionFraction: Float,
        headerTranslation: Float,
        squishinessFraction: Float
        squishinessFraction: Float,
    ) {
        viewModel.qsExpansionValue = qsExpansionFraction
        viewModel.panelExpansionFractionValue = panelExpansionFraction
@@ -318,12 +318,12 @@ constructor(
    override fun setTransitionToFullShadeProgress(
        isTransitioningToFullShade: Boolean,
        qsTransitionFraction: Float,
        qsSquishinessFraction: Float
        qsSquishinessFraction: Float,
    ) {
        super.setTransitionToFullShadeProgress(
            isTransitioningToFullShade,
            qsTransitionFraction,
            qsSquishinessFraction
            qsSquishinessFraction,
        )
    }

@@ -334,7 +334,7 @@ constructor(
        bottom: Int,
        cornerRadius: Int,
        visible: Boolean,
        fullWidth: Boolean
        fullWidth: Boolean,
    ) {
        notificationScrimClippingParams.isEnabled = visible
        notificationScrimClippingParams.top = top
@@ -402,7 +402,7 @@ constructor(
                launch {
                    setListenerJob(
                        heightListener,
                        viewModel.containerViewModel.editModeViewModel.isEditing
                        viewModel.containerViewModel.editModeViewModel.isEditing,
                    ) {
                        onQsHeightChanged()
                    }
@@ -410,7 +410,7 @@ constructor(
                launch {
                    setListenerJob(
                        qsContainerController,
                        viewModel.containerViewModel.editModeViewModel.isEditing
                        viewModel.containerViewModel.editModeViewModel.isEditing,
                    ) {
                        setCustomizerShowing(it)
                    }
@@ -422,6 +422,7 @@ constructor(
    @Composable
    private fun QuickQuickSettingsElement() {
        val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
        val bottomPadding = dimensionResource(id = R.dimen.qqs_layout_padding_bottom)
        DisposableEffect(Unit) {
            qqsVisible.value = true

@@ -441,7 +442,7 @@ constructor(
                            )
                        }
                        .onSizeChanged { size -> qqsHeight.value = size.height }
                        .padding(top = { qqsPadding })
                        .padding(top = { qqsPadding }, bottom = { bottomPadding.roundToPx() })
            ) {
                val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle()
                if (qsEnabled) {
@@ -450,7 +451,7 @@ constructor(
                        modifier =
                            Modifier.collapseExpandSemanticAction(
                                stringResource(id = R.string.accessibility_quick_settings_expand)
                            )
                            ),
                    )
                }
            }
@@ -482,7 +483,7 @@ constructor(
                    FooterActions(
                        viewModel = viewModel.footerActionsViewModel,
                        qsVisibilityLifecycleOwner = this@QSFragmentCompose,
                        modifier = Modifier.sysuiResTag("qs_footer_actions")
                        modifier = Modifier.sysuiResTag("qs_footer_actions"),
                    )
                }
            }
@@ -562,7 +563,7 @@ private fun View.setBackPressedDispatcher() {
private suspend inline fun <Listener : Any, Data> setListenerJob(
    listenerFlow: MutableStateFlow<Listener?>,
    dataFlow: Flow<Data>,
    crossinline onCollect: suspend Listener.(Data) -> Unit
    crossinline onCollect: suspend Listener.(Data) -> Unit,
) {
    coroutineScope {
        try {
+11 −9
Original line number Diff line number Diff line
@@ -22,18 +22,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.util.fastMap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel

@Composable
fun QuickQuickSettings(
    viewModel: QuickQuickSettingsViewModel,
    modifier: Modifier = Modifier,
) {
fun QuickQuickSettings(viewModel: QuickQuickSettingsViewModel, modifier: Modifier = Modifier) {
    val sizedTiles by
        viewModel.tileViewModels.collectAsStateWithLifecycle(initialValue = emptyList())
    val tiles = sizedTiles.map { it.tile }
    val tiles = sizedTiles.fastMap { it.tile }

    DisposableEffect(tiles) {
        val token = Any()
@@ -44,14 +42,18 @@ fun QuickQuickSettings(

    TileLazyGrid(
        modifier = modifier.sysuiResTag("qqs_tile_layout"),
        columns = GridCells.Fixed(columns)
        columns = GridCells.Fixed(columns),
    ) {
        items(
            tiles.size,
            sizedTiles.size,
            key = { index -> sizedTiles[index].tile.spec.spec },
            span = { index -> GridItemSpan(sizedTiles[index].width) }
            span = { index -> GridItemSpan(sizedTiles[index].width) },
        ) { index ->
            Tile(tile = tiles[index], iconOnly = sizedTiles[index].isIcon, modifier = Modifier)
            Tile(
                tile = sizedTiles[index].tile,
                iconOnly = sizedTiles[index].isIcon,
                modifier = Modifier,
            )
        }
    }
}
+112 −46

File changed.

Preview size limit exceeded, changes collapsed.

+78 −6
Original line number Diff line number Diff line
@@ -16,8 +16,16 @@

package com.android.systemui.qs.panels.ui.viewmodel

import android.content.res.Resources
import android.service.quicksettings.Tile
import android.text.TextUtils
import android.widget.Switch
import androidx.compose.runtime.Immutable
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.state.ToggleableState
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.tileimpl.SubtitleArrayMapping
import com.android.systemui.res.R
import java.util.function.Supplier

@Immutable
@@ -27,14 +35,78 @@ data class TileUiState(
    val state: Int,
    val handlesSecondaryClick: Boolean,
    val icon: Supplier<QSTile.Icon?>,
    val accessibilityUiState: AccessibilityUiState,
)

fun QSTile.State.toUiState(): TileUiState {
data class AccessibilityUiState(
    val contentDescription: String,
    val stateDescription: String,
    val accessibilityRole: Role,
    val toggleableState: ToggleableState? = null,
    val clickLabel: String? = null,
)

fun QSTile.State.toUiState(resources: Resources): TileUiState {
    val accessibilityRole =
        if (expandedAccessibilityClassName == Switch::class.java.name && !handlesSecondaryClick) {
            Role.Switch
        } else {
            Role.Button
        }
    // State handling and description
    val stateDescription = StringBuilder()
    val stateText =
        if (accessibilityRole == Role.Switch || state == Tile.STATE_UNAVAILABLE) {
            getStateText(resources)
        } else {
            ""
        }
    val secondaryLabel = getSecondaryLabel(stateText)
    if (!TextUtils.isEmpty(stateText)) {
        stateDescription.append(stateText)
    }
    if (disabledByPolicy && state != Tile.STATE_UNAVAILABLE) {
        stateDescription.append(", ")
        stateDescription.append(getUnavailableText(spec, resources))
    }
    if (
        !TextUtils.isEmpty(this.stateDescription) &&
            !stateDescription.contains(this.stateDescription!!)
    ) {
        stateDescription.append(", ")
        stateDescription.append(this.stateDescription)
    }
    val toggleableState =
        if (accessibilityRole == Role.Switch || handlesSecondaryClick) {
            ToggleableState(state == Tile.STATE_ACTIVE)
        } else {
            null
        }
    return TileUiState(
        label?.toString() ?: "",
        secondaryLabel?.toString() ?: "",
        state,
        handlesSecondaryClick,
        icon?.let { Supplier { icon } } ?: iconSupplier ?: Supplier { null },
        label = label?.toString() ?: "",
        secondaryLabel = secondaryLabel?.toString() ?: "",
        state = if (disabledByPolicy) Tile.STATE_UNAVAILABLE else state,
        handlesSecondaryClick = handlesSecondaryClick,
        icon = icon?.let { Supplier { icon } } ?: iconSupplier ?: Supplier { null },
        AccessibilityUiState(
            contentDescription?.toString() ?: "",
            stateDescription.toString(),
            accessibilityRole,
            toggleableState,
            resources
                .getString(R.string.accessibility_tile_disabled_by_policy_action_description)
                .takeIf { disabledByPolicy },
        ),
    )
}

private fun QSTile.State.getStateText(resources: Resources): CharSequence {
    val arrayResId = SubtitleArrayMapping.getSubtitleId(spec)
    val array = resources.getStringArray(arrayResId)
    return array[state]
}

private fun getUnavailableText(spec: String?, resources: Resources): String {
    val arrayResId = SubtitleArrayMapping.getSubtitleId(spec)
    return resources.getStringArray(arrayResId)[Tile.STATE_UNAVAILABLE]
}
Loading