Merge "Add accessibility for new tiles." into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt
new file mode 100644
index 0000000..b144f06
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt
@@ -0,0 +1,270 @@
+/*
+ * 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
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index c2f1c3d..af167d4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -158,7 +158,7 @@
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
-        savedInstanceState: Bundle?
+        savedInstanceState: Bundle?,
     ): View {
         val context = inflater.context
         return ComposeView(context).apply {
@@ -181,7 +181,7 @@
                                     notificationScrimClippingParams.bottom,
                                     notificationScrimClippingParams.radius,
                                 )
-                            }
+                            },
                     ) {
                         AnimatedContent(targetState = qsState) {
                             when (it) {
@@ -272,7 +272,7 @@
         qsExpansionFraction: Float,
         panelExpansionFraction: Float,
         headerTranslation: Float,
-        squishinessFraction: Float
+        squishinessFraction: Float,
     ) {
         viewModel.qsExpansionValue = qsExpansionFraction
         viewModel.panelExpansionFractionValue = panelExpansionFraction
@@ -318,12 +318,12 @@
     override fun setTransitionToFullShadeProgress(
         isTransitioningToFullShade: Boolean,
         qsTransitionFraction: Float,
-        qsSquishinessFraction: Float
+        qsSquishinessFraction: Float,
     ) {
         super.setTransitionToFullShadeProgress(
             isTransitioningToFullShade,
             qsTransitionFraction,
-            qsSquishinessFraction
+            qsSquishinessFraction,
         )
     }
 
@@ -334,7 +334,7 @@
         bottom: Int,
         cornerRadius: Int,
         visible: Boolean,
-        fullWidth: Boolean
+        fullWidth: Boolean,
     ) {
         notificationScrimClippingParams.isEnabled = visible
         notificationScrimClippingParams.top = top
@@ -402,7 +402,7 @@
                 launch {
                     setListenerJob(
                         heightListener,
-                        viewModel.containerViewModel.editModeViewModel.isEditing
+                        viewModel.containerViewModel.editModeViewModel.isEditing,
                     ) {
                         onQsHeightChanged()
                     }
@@ -410,7 +410,7 @@
                 launch {
                     setListenerJob(
                         qsContainerController,
-                        viewModel.containerViewModel.editModeViewModel.isEditing
+                        viewModel.containerViewModel.editModeViewModel.isEditing,
                     ) {
                         setCustomizerShowing(it)
                     }
@@ -422,6 +422,7 @@
     @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 @@
                             )
                         }
                         .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 @@
                         modifier =
                             Modifier.collapseExpandSemanticAction(
                                 stringResource(id = R.string.accessibility_quick_settings_expand)
-                            )
+                            ),
                     )
                 }
             }
@@ -482,7 +483,7 @@
                     FooterActions(
                         viewModel = viewModel.footerActionsViewModel,
                         qsVisibilityLifecycleOwner = this@QSFragmentCompose,
-                        modifier = Modifier.sysuiResTag("qs_footer_actions")
+                        modifier = Modifier.sysuiResTag("qs_footer_actions"),
                     )
                 }
             }
@@ -562,7 +563,7 @@
 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 {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index eeb55ca..fde40da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -22,18 +22,16 @@
 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 @@
 
     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,
+            )
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index 93037d1..afd47a7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.qs.panels.ui.compose
 
+import android.content.res.Resources
 import android.graphics.drawable.Animatable
 import android.service.quicksettings.Tile.STATE_ACTIVE
 import android.service.quicksettings.Tile.STATE_INACTIVE
@@ -71,6 +72,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -85,13 +87,19 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.toggleableState
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
@@ -106,12 +114,15 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.common.ui.compose.load
+import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.TileDefaults.longPressLabel
 import com.android.systemui.qs.panels.ui.model.GridCell
 import com.android.systemui.qs.panels.ui.model.SpacerGridCell
 import com.android.systemui.qs.panels.ui.model.TileGridCell
+import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
@@ -126,15 +137,15 @@
 
 object TileType
 
+private const val TEST_TAG_SMALL = "qs_tile_small"
+private const val TEST_TAG_LARGE = "qs_tile_large"
+private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target"
+
 @Composable
-fun Tile(
-    tile: TileViewModel,
-    iconOnly: Boolean,
-    showLabels: Boolean = false,
-    modifier: Modifier,
-) {
+fun Tile(tile: TileViewModel, iconOnly: Boolean, showLabels: Boolean = false, modifier: Modifier) {
     val state by tile.state.collectAsStateWithLifecycle(tile.currentState)
-    val uiState = remember(state) { state.toUiState() }
+    val resources = resources()
+    val uiState = remember(state, resources) { state.toUiState(resources) }
     val colors = TileDefaults.getColorForState(uiState)
 
     // TODO(b/361789146): Draw the shapes instead of clipping
@@ -150,6 +161,7 @@
         onClick = tile::onClick,
         onLongClick = tile::onLongClick,
         modifier = modifier.height(tileHeight()),
+        uiState = uiState,
     ) {
         val icon = getTileIcon(icon = uiState.icon)
         if (iconOnly) {
@@ -169,6 +181,7 @@
                     }
                 },
                 onLongClick = { tile.onLongClick(it) },
+                accessibilityUiState = uiState.accessibilityUiState,
             )
         }
     }
@@ -185,6 +198,7 @@
     onClick: (Expandable) -> Unit = {},
     onLongClick: (Expandable) -> Unit = {},
     modifier: Modifier = Modifier,
+    uiState: TileUiState? = null,
     content: @Composable BoxScope.(Expandable) -> Unit,
 ) {
     Column(
@@ -194,7 +208,7 @@
         modifier = modifier,
     ) {
         val backgroundColor =
-            if (iconOnly) {
+            if (iconOnly || uiState?.handlesSecondaryClick != true) {
                 colors.iconBackground
             } else {
                 colors.background
@@ -202,18 +216,43 @@
         Expandable(
             color = backgroundColor,
             shape = shape,
-            modifier = Modifier.height(tileHeight()).clip(shape)
+            modifier = Modifier.height(tileHeight()).clip(shape),
         ) {
+            val longPressLabel = longPressLabel()
             Box(
                 modifier =
                     Modifier.fillMaxSize()
                         .thenIf(clickEnabled) {
                             Modifier.combinedClickable(
                                 onClick = { onClick(it) },
-                                onLongClick = { onLongClick(it) }
+                                onLongClick = { onLongClick(it) },
+                                onClickLabel = uiState?.accessibilityUiState?.clickLabel,
+                                onLongClickLabel = longPressLabel,
                             )
                         }
-                        .tilePadding(),
+                        .thenIf(uiState != null) {
+                            uiState as TileUiState
+                            Modifier.semantics {
+                                    role = uiState.accessibilityUiState.accessibilityRole
+                                    if (
+                                        uiState.accessibilityUiState.accessibilityRole ==
+                                            Role.Switch
+                                    ) {
+                                        uiState.accessibilityUiState.toggleableState?.let {
+                                            toggleableState = it
+                                        }
+                                    }
+                                    stateDescription = uiState.accessibilityUiState.stateDescription
+                                }
+                                .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE)
+                                .thenIf(iconOnly) {
+                                    Modifier.semantics {
+                                        contentDescription =
+                                            uiState.accessibilityUiState.contentDescription
+                                    }
+                                }
+                        }
+                        .tilePadding()
             ) {
                 content(it)
             }
@@ -238,21 +277,39 @@
     icon: Icon,
     colors: TileColors,
     iconShape: Shape,
+    accessibilityUiState: AccessibilityUiState? = null,
     toggleClickSupported: Boolean = false,
     onClick: () -> Unit = {},
     onLongClick: () -> Unit = {},
 ) {
     Row(
         verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = tileHorizontalArrangement()
+        horizontalArrangement = tileHorizontalArrangement(),
     ) {
         // Icon
+        val longPressLabel = longPressLabel()
         Box(
             modifier =
                 Modifier.size(TileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) {
                     Modifier.clip(iconShape)
                         .background(colors.iconBackground, { 1f })
-                        .combinedClickable(onClick = onClick, onLongClick = onLongClick)
+                        .combinedClickable(
+                            onClick = onClick,
+                            onLongClick = onLongClick,
+                            onLongClickLabel = longPressLabel,
+                        )
+                        .thenIf(accessibilityUiState != null) {
+                            accessibilityUiState as AccessibilityUiState
+                            Modifier.semantics {
+                                    contentDescription = accessibilityUiState.contentDescription
+                                    stateDescription = accessibilityUiState.stateDescription
+                                    accessibilityUiState.toggleableState?.let {
+                                        toggleableState = it
+                                    }
+                                    role = Role.Switch
+                                }
+                                .sysuiResTag(TEST_TAG_TOGGLE)
+                        }
                 }
         ) {
             TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center))
@@ -260,16 +317,19 @@
 
         // Labels
         Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
-            Text(
-                label,
-                color = colors.label,
-                modifier = Modifier.tileMarquee(),
-            )
+            Text(label, color = colors.label, modifier = Modifier.tileMarquee())
             if (!TextUtils.isEmpty(secondaryLabel)) {
                 Text(
                     secondaryLabel ?: "",
                     color = colors.secondaryLabel,
-                    modifier = Modifier.tileMarquee(),
+                    modifier =
+                        Modifier.tileMarquee().thenIf(
+                            accessibilityUiState
+                                ?.stateDescription
+                                ?.contains(secondaryLabel ?: "") == true
+                        ) {
+                            Modifier.clearAndSetSemantics {}
+                        },
                 )
             }
         }
@@ -277,10 +337,7 @@
 }
 
 private fun Modifier.tileMarquee(): Modifier {
-    return basicMarquee(
-        iterations = 1,
-        initialDelayMillis = 200,
-    )
+    return basicMarquee(iterations = 1, initialDelayMillis = 200)
 }
 
 @Composable
@@ -320,11 +377,11 @@
         Column(
             verticalArrangement =
                 spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
-            modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState())
+            modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()),
         ) {
             AnimatedContent(
                 targetState = currentListState.dragInProgress,
-                modifier = Modifier.wrapContentSize()
+                modifier = Modifier.wrapContentSize(),
             ) { dragIsInProgress ->
                 EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) {
                     if (dragIsInProgress) {
@@ -348,12 +405,12 @@
             AnimatedVisibility(
                 visible = !currentListState.dragInProgress,
                 enter = fadeIn(),
-                exit = fadeOut()
+                exit = fadeOut(),
             ) {
                 Column(
                     verticalArrangement =
                         spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
-                    modifier = modifier.fillMaxSize()
+                    modifier = modifier.fillMaxSize(),
                 ) {
                     EditGridHeader { Text(text = "Hold and drag to add tiles.") }
 
@@ -381,14 +438,14 @@
 @Composable
 private fun EditGridHeader(
     modifier: Modifier = Modifier,
-    content: @Composable BoxScope.() -> Unit
+    content: @Composable BoxScope.() -> Unit,
 ) {
     CompositionLocalProvider(
         LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f)
     ) {
         Box(
             contentAlignment = Alignment.Center,
-            modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight)
+            modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight),
         ) {
             content()
         }
@@ -403,7 +460,7 @@
         modifier =
             Modifier.fillMaxHeight()
                 .border(1.dp, LocalContentColor.current, shape = CircleShape)
-                .padding(10.dp)
+                .padding(10.dp),
     ) {
         Icon(imageVector = Icons.Default.Clear, contentDescription = null)
         Text(text = "Remove")
@@ -454,7 +511,7 @@
                         gridContentOffset = coordinates.positionInRoot()
                     }
                     .testTag(CURRENT_TILES_GRID_TEST_TAG),
-            columns = GridCells.Fixed(columns)
+            columns = GridCells.Fixed(columns),
         ) {
             editTiles(
                 listState.tiles,
@@ -488,7 +545,7 @@
     // Available tiles
     TileLazyGrid(
         modifier = Modifier.height(availableGridHeight).testTag(AVAILABLE_TILES_GRID_TEST_TAG),
-        columns = GridCells.Fixed(columns)
+        columns = GridCells.Fixed(columns),
     ) {
         groupedTiles.forEach { category, tiles ->
             stickyHeader {
@@ -498,7 +555,7 @@
                     color = labelColors.label,
                     modifier =
                         Modifier.background(Color.Black)
-                            .padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
+                            .padding(start = 16.dp, bottom = 8.dp, top = 8.dp),
                 )
             }
             editTiles(
@@ -542,7 +599,7 @@
         count = cells.size,
         key = { cells[it].key(it, dragAndDropState) },
         span = { cells[it].span },
-        contentType = { TileType }
+        contentType = { TileType },
     ) { index ->
         when (val cell = cells[index]) {
             is TileGridCell ->
@@ -552,7 +609,7 @@
                         Modifier.background(
                                 color = MaterialTheme.colorScheme.secondary,
                                 alpha = { EditModeTileDefaults.PLACEHOLDER_ALPHA },
-                                shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius)
+                                shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
                             )
                             .animateItem()
                     )
@@ -565,7 +622,7 @@
                         onClick = onClick,
                         onResize = onResize,
                         showLabels = showLabels,
-                        indicatePosition = indicatePosition
+                        indicatePosition = indicatePosition,
                     )
                 }
             is SpacerGridCell -> SpacerGridCell()
@@ -604,7 +661,7 @@
         modifier =
             Modifier.height(tileHeight)
                 .animateItem()
-                .semantics {
+                .semantics(mergeDescendants = true) {
                     onClick(onClickActionName) { false }
                     this.stateDescription = stateDescription
                 }
@@ -613,7 +670,7 @@
                     onClick,
                     onResize,
                     dragAndDropState,
-                )
+                ),
     )
 }
 
@@ -645,7 +702,7 @@
             TileIcon(
                 icon = tileViewModel.icon,
                 color = colors.icon,
-                modifier = Modifier.align(Alignment.Center)
+                modifier = Modifier.align(Alignment.Center),
             )
         } else {
             LargeTileContent(
@@ -694,11 +751,7 @@
             }
         }
     if (loadedDrawable !is Animatable) {
-        Icon(
-            icon = icon,
-            tint = color,
-            modifier = iconModifier,
-        )
+        Icon(icon = icon, tint = color, modifier = iconModifier)
     } else if (icon is Icon.Resource) {
         val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
         val painter =
@@ -716,7 +769,7 @@
             painter = painter,
             contentDescription = icon.contentDescription?.load(),
             colorFilter = ColorFilter.tint(color = color),
-            modifier = iconModifier
+            modifier = iconModifier,
         )
     }
 }
@@ -765,6 +818,8 @@
     val TileHeight = 72.dp
     val IconTileWithLabelHeight = 140.dp
 
+    @Composable fun longPressLabel() = stringResource(id = R.string.accessibility_long_click_tile)
+
     /** An active tile without dual target uses the active color as background */
     @Composable
     fun activeTileColors(): TileColors =
@@ -850,7 +905,7 @@
                     } else {
                         InactiveCornerRadius
                     },
-                label = label
+                label = label,
             )
         return RoundedCornerShape(animatedCornerRadius)
     }
@@ -858,3 +913,14 @@
 
 private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
 private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
+
+/**
+ * A composable function that returns the [Resources]. It will be recomposed when [Configuration]
+ * gets updated.
+ */
+@Composable
+@ReadOnlyComposable
+private fun resources(): Resources {
+    LocalConfiguration.current
+    return LocalContext.current.resources
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
index 45051fe..aa42080 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
@@ -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 @@
     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]
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 7ceb786..7bff827 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -32,7 +32,7 @@
 import android.service.quicksettings.Tile;
 import android.text.TextUtils;
 import android.util.Log;
-import android.widget.Switch;
+import android.widget.Button;
 
 import androidx.annotation.VisibleForTesting;
 
@@ -59,13 +59,13 @@
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.policy.BluetoothController;
 
-import kotlinx.coroutines.Job;
-
 import java.util.List;
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
+import kotlinx.coroutines.Job;
+
 /** Quick settings tile: Bluetooth **/
 public class BluetoothTile extends QSTileImpl<BooleanState> {
 
@@ -147,6 +147,8 @@
         }
     }
 
+
+
     @Override
     public Intent getLongClickIntent() {
         return new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
@@ -221,7 +223,7 @@
             state.state = Tile.STATE_INACTIVE;
         }
 
-        state.expandedAccessibilityClassName = Switch.class.getName();
+        state.expandedAccessibilityClassName = Button.class.getName();
         state.forceExpandIcon = mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
index 078698c..7606293 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
@@ -55,7 +55,7 @@
     qsLogger: QSLogger,
     private val keyguardStateController: KeyguardStateController,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
-    private val fontScalingDialogDelegateProvider: Provider<FontScalingDialogDelegate>
+    private val fontScalingDialogDelegateProvider: Provider<FontScalingDialogDelegate>,
 ) :
     QSTileImpl<QSTile.State?>(
         host,
@@ -66,7 +66,7 @@
         metricsLogger,
         statusBarStateController,
         activityStarter,
-        qsLogger
+        qsLogger,
     ) {
     private val icon = ResourceIcon.get(R.drawable.ic_qs_font_scaling)
 
@@ -86,7 +86,7 @@
                     expandable?.dialogTransitionController(
                         DialogCuj(
                             InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                            INTERACTION_JANK_TAG
+                            INTERACTION_JANK_TAG,
                         )
                     )
                 controller?.let { dialogTransitionAnimator.show(dialog, controller) }
@@ -102,7 +102,7 @@
                 /* cancelAction= */ null,
                 /* dismissShade= */ true,
                 /* afterKeyguardGone= */ true,
-                /* deferred= */ false
+                /* deferred= */ false,
             )
         }
     }
@@ -110,6 +110,7 @@
     override fun handleUpdateState(state: QSTile.State?, arg: Any?) {
         state?.label = mContext.getString(R.string.quick_settings_font_scaling_label)
         state?.icon = icon
+        state?.contentDescription = state?.label
     }
 
     override fun getLongClickIntent(): Intent? {