a11y for controls management

Use state description to indicate if a control is a Favorite and its
position for when it can be rearranged.

Also, remove a hardcoded string.

Test: manual
Fixes: 155995940
Change-Id: Ia908558a9108e895457e9847f52744f27fe177c3
This commit is contained in:
Fabian Kozynski
2020-05-08 14:29:30 -04:00
parent e6e7cdf1af
commit b3c393f723
3 changed files with 97 additions and 5 deletions

View File

@@ -2698,6 +2698,20 @@
<item quantity="other"><xliff:g id="number" example="3">%s</xliff:g> controls added.</item>
</plurals>
<!-- Removed control in management screen [CHAR LIMIT=20] -->
<string name="controls_removed">Removed</string>
<!-- a11y state description for a control that is currently favorited [CHAR LIMIT=NONE] -->
<string name="accessibility_control_favorite">Favorited</string>
<!-- a11y state description for a control that is currently favorited with its position [CHAR LIMIT=NONE] -->
<string name="accessibility_control_favorite_position">Favorited, position <xliff:g id="number" example="1">%d</xliff:g></string>
<!-- a11y state description for a control that is currently not favorited [CHAR LIMIT=NONE] -->
<string name="accessibility_control_not_favorite">Unfavorited</string>
<!-- a11y action to favorite a control. It will read as "Double-tap to favorite" in screen readers [CHAR LIMIT=NONE] -->
<string name="accessibility_control_change_favorite">favorite</string>
<!-- a11y action to unfavorite a control. It will read as "Double-tap to unfavorite" in screen readers [CHAR LIMIT=NONE] -->
<string name="accessibility_control_change_unfavorite">unfavorite</string>
<!-- Controls management controls screen default title [CHAR LIMIT=30] -->
<string name="controls_favorite_default_title">Controls</string>
<!-- Controls management controls screen subtitle [CHAR LIMIT=NONE] -->

View File

@@ -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

View File

@@ -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)