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)