diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ec29622c9ba28..8c10f61db7a06 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2698,6 +2698,20 @@
- %s controls added.
+
+ Removed
+
+
+ Favorited
+
+ Favorited, position %d
+
+ Unfavorited
+
+ favorite
+
+ unfavorite
+
Controls
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
index 79dd9edef0f00..4b283d607bb89 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -23,9 +23,14 @@ import android.service.controls.DeviceTypes
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.accessibility.AccessibilityNodeInfo
import android.widget.CheckBox
import android.widget.ImageView
+import android.widget.Switch
import android.widget.TextView
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
@@ -72,7 +77,8 @@ class ControlAdapter(
elevation = this@ControlAdapter.elevation
background = parent.context.getDrawable(
R.drawable.control_background_ripple)
- }
+ },
+ model is FavoritesModel // Indicates that position information is needed
) { id, favorite ->
model?.changeFavoriteStatus(id, favorite)
}
@@ -175,8 +181,14 @@ private class ZoneHolder(view: View) : Holder(view) {
*/
internal class ControlHolder(
view: View,
+ val withPosition: Boolean,
val favoriteCallback: ModelFavoriteChanger
) : Holder(view) {
+ private val favoriteStateDescription =
+ itemView.context.getString(R.string.accessibility_control_favorite)
+ private val notFavoriteStateDescription =
+ itemView.context.getString(R.string.accessibility_control_not_favorite)
+
private val icon: ImageView = itemView.requireViewById(R.id.icon)
private val title: TextView = itemView.requireViewById(R.id.title)
private val subtitle: TextView = itemView.requireViewById(R.id.subtitle)
@@ -185,15 +197,38 @@ internal class ControlHolder(
visibility = View.VISIBLE
}
+ private val accessibilityDelegate = ControlHolderAccessibilityDelegate(this::stateDescription)
+
+ init {
+ ViewCompat.setAccessibilityDelegate(itemView, accessibilityDelegate)
+ }
+
+ // Determine the stateDescription based on favorite state and maybe position
+ private fun stateDescription(favorite: Boolean): CharSequence? {
+ if (!favorite) {
+ return notFavoriteStateDescription
+ } else if (!withPosition) {
+ return favoriteStateDescription
+ } else {
+ val position = layoutPosition + 1
+ return itemView.context.getString(
+ R.string.accessibility_control_favorite_position, position)
+ }
+ }
+
override fun bindData(wrapper: ElementWrapper) {
wrapper as ControlInterface
val renderInfo = getRenderInfo(wrapper.component, wrapper.deviceType)
title.text = wrapper.title
subtitle.text = wrapper.subtitle
- favorite.isChecked = wrapper.favorite
- removed.text = if (wrapper.removed) "Removed" else ""
+ updateFavorite(wrapper.favorite)
+ removed.text = if (wrapper.removed) {
+ itemView.context.getText(R.string.controls_removed)
+ } else {
+ ""
+ }
itemView.setOnClickListener {
- favorite.isChecked = !favorite.isChecked
+ updateFavorite(!favorite.isChecked)
favoriteCallback(wrapper.controlId, favorite.isChecked)
}
applyRenderInfo(renderInfo)
@@ -201,6 +236,8 @@ internal class ControlHolder(
override fun updateFavorite(favorite: Boolean) {
this.favorite.isChecked = favorite
+ accessibilityDelegate.isFavorite = favorite
+ itemView.stateDescription = stateDescription(favorite)
}
private fun getRenderInfo(
@@ -219,6 +256,36 @@ internal class ControlHolder(
}
}
+private class ControlHolderAccessibilityDelegate(
+ val stateRetriever: (Boolean) -> CharSequence?
+) : AccessibilityDelegateCompat() {
+
+ var isFavorite = false
+
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+
+ // Change the text for the double-tap action
+ val clickActionString = if (isFavorite) {
+ host.context.getString(R.string.accessibility_control_change_unfavorite)
+ } else {
+ host.context.getString(R.string.accessibility_control_change_favorite)
+ }
+ val click = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfo.ACTION_CLICK,
+ // “favorite/unfavorite”
+ clickActionString)
+ info.addAction(click)
+
+ // Determine the stateDescription based on the holder information
+ info.stateDescription = stateRetriever(isFavorite)
+ // Remove the information at the end indicating row and column.
+ info.setCollectionItemInfo(null)
+
+ info.className = Switch::class.java.name
+ }
+}
+
class MarginItemDecorator(
private val topMargin: Int,
private val sideMargins: Int
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
index 4e9c550297c53..3a4e82c3793f4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
@@ -191,7 +191,18 @@ class ControlsEditingActivity @Inject constructor(
recyclerView.apply {
this.adapter = adapter
- layoutManager = GridLayoutManager(recyclerView.context, 2).apply {
+ layoutManager = object : GridLayoutManager(recyclerView.context, 2) {
+
+ // This will remove from the announcement the row corresponding to the divider,
+ // as it's not something that should be announced.
+ override fun getRowCountForAccessibility(
+ recycler: RecyclerView.Recycler,
+ state: RecyclerView.State
+ ): Int {
+ val initial = super.getRowCountForAccessibility(recycler, state)
+ return if (initial > 0) initial - 1 else initial
+ }
+ }.apply {
spanSizeLookup = adapter.spanSizeLookup
}
addItemDecoration(itemDecorator)