blob: 4b283d607bb891f587d7ff965d3bbd460eaa6f8c [file] [log] [blame]
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -05001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.controls.management
18
Matt Pietal53a8bbd2020-03-05 16:10:34 -050019import android.content.ComponentName
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -050020import android.graphics.Rect
Fabian Kozynski6936cd12020-04-30 12:14:03 -040021import android.service.controls.Control
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -050022import android.service.controls.DeviceTypes
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050023import android.view.LayoutInflater
24import android.view.View
25import android.view.ViewGroup
Fabian Kozynskib3c393f2020-05-08 14:29:30 -040026import android.view.accessibility.AccessibilityNodeInfo
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050027import android.widget.CheckBox
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -050028import android.widget.ImageView
Fabian Kozynskib3c393f2020-05-08 14:29:30 -040029import android.widget.Switch
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050030import android.widget.TextView
Fabian Kozynskib3c393f2020-05-08 14:29:30 -040031import androidx.core.view.AccessibilityDelegateCompat
32import androidx.core.view.ViewCompat
33import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050034import androidx.recyclerview.widget.GridLayoutManager
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050035import androidx.recyclerview.widget.RecyclerView
36import com.android.systemui.R
Fabian Kozynski8765d352020-04-06 21:16:02 -040037import com.android.systemui.controls.ControlInterface
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -050038import com.android.systemui.controls.ui.RenderInfo
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050039
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050040private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
41
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050042/**
43 * Adapter for binding [Control] information to views.
44 *
Fabian Kozynski8765d352020-04-06 21:16:02 -040045 * The model for this adapter is provided by a [ControlModel] that is set using
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050046 * [changeFavoritesModel]. This allows for updating the model if there's a reload.
47 *
Fabian Kozynski8765d352020-04-06 21:16:02 -040048 * @property elevation elevation of each control view
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050049 */
50class ControlAdapter(
Fabian Kozynski04e7bde2020-02-13 13:02:33 -050051 private val elevation: Float
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050052) : RecyclerView.Adapter<Holder>() {
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050053
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050054 companion object {
Fabian Kozynski6936cd12020-04-30 12:14:03 -040055 const val TYPE_ZONE = 0
56 const val TYPE_CONTROL = 1
57 const val TYPE_DIVIDER = 2
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050058 }
59
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050060 val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
61 override fun getSpanSize(position: Int): Int {
Fabian Kozynski8765d352020-04-06 21:16:02 -040062 return if (getItemViewType(position) != TYPE_CONTROL) 2 else 1
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050063 }
64 }
65
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050066 private var model: ControlsModel? = null
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050067
68 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
Fabian Kozynski713b7272020-03-03 18:35:52 -050069 val layoutInflater = LayoutInflater.from(parent.context)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050070 return when (viewType) {
71 TYPE_CONTROL -> {
72 ControlHolder(
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050073 layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply {
74 layoutParams.apply {
75 width = ViewGroup.LayoutParams.MATCH_PARENT
76 }
Fabian Kozynski04e7bde2020-02-13 13:02:33 -050077 elevation = this@ControlAdapter.elevation
Fabian Kozynski9b972e82020-03-26 14:33:11 -040078 background = parent.context.getDrawable(
79 R.drawable.control_background_ripple)
Fabian Kozynskib3c393f2020-05-08 14:29:30 -040080 },
81 model is FavoritesModel // Indicates that position information is needed
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050082 ) { id, favorite ->
83 model?.changeFavoriteStatus(id, favorite)
84 }
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050085 }
86 TYPE_ZONE -> {
87 ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false))
88 }
Fabian Kozynski8765d352020-04-06 21:16:02 -040089 TYPE_DIVIDER -> {
90 DividerHolder(layoutInflater.inflate(
Joshua Tsuji04631802020-04-13 12:30:34 -040091 R.layout.controls_horizontal_divider_with_empty, parent, false))
Fabian Kozynski8765d352020-04-06 21:16:02 -040092 }
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050093 else -> throw IllegalStateException("Wrong viewType: $viewType")
94 }
95 }
96
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050097 fun changeModel(model: ControlsModel) {
98 this.model = model
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050099 notifyDataSetChanged()
100 }
101
Fabian Kozynski1e3178d2020-02-19 09:32:27 -0500102 override fun getItemCount() = model?.elements?.size ?: 0
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500103
104 override fun onBindViewHolder(holder: Holder, index: Int) {
Fabian Kozynski1e3178d2020-02-19 09:32:27 -0500105 model?.let {
106 holder.bindData(it.elements[index])
107 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500108 }
109
Fabian Kozynski8765d352020-04-06 21:16:02 -0400110 override fun onBindViewHolder(holder: Holder, position: Int, payloads: MutableList<Any>) {
111 if (payloads.isEmpty()) {
112 super.onBindViewHolder(holder, position, payloads)
113 } else {
114 model?.let {
115 val el = it.elements[position]
116 if (el is ControlInterface) {
117 holder.updateFavorite(el.favorite)
118 }
119 }
120 }
121 }
122
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500123 override fun getItemViewType(position: Int): Int {
Fabian Kozynski1e3178d2020-02-19 09:32:27 -0500124 model?.let {
125 return when (it.elements.get(position)) {
126 is ZoneNameWrapper -> TYPE_ZONE
Fabian Kozynski8765d352020-04-06 21:16:02 -0400127 is ControlStatusWrapper -> TYPE_CONTROL
128 is ControlInfoWrapper -> TYPE_CONTROL
129 is DividerWrapper -> TYPE_DIVIDER
Fabian Kozynski1e3178d2020-02-19 09:32:27 -0500130 }
131 } ?: throw IllegalStateException("Getting item type for null model")
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500132 }
133}
134
135/**
136 * Holder for binding views in the [RecyclerView]-
137 * @param view the [View] for this [Holder]
138 */
139sealed class Holder(view: View) : RecyclerView.ViewHolder(view) {
140
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500141 /**
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500142 * Bind the data from the model into the view
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500143 */
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500144 abstract fun bindData(wrapper: ElementWrapper)
Fabian Kozynski8765d352020-04-06 21:16:02 -0400145
146 open fun updateFavorite(favorite: Boolean) {}
147}
148
149/**
150 * Holder for using with [DividerWrapper] to display a divider between zones.
151 *
Fabian Kozynski6936cd12020-04-30 12:14:03 -0400152 * The divider can be shown or hidden. It also has a view the height of a control, that can
Fabian Kozynski8765d352020-04-06 21:16:02 -0400153 * be toggled visible or gone.
154 */
155private class DividerHolder(view: View) : Holder(view) {
156 private val frame: View = itemView.requireViewById(R.id.frame)
157 private val divider: View = itemView.requireViewById(R.id.divider)
158 override fun bindData(wrapper: ElementWrapper) {
159 wrapper as DividerWrapper
160 frame.visibility = if (wrapper.showNone) View.VISIBLE else View.GONE
161 divider.visibility = if (wrapper.showDivider) View.VISIBLE else View.GONE
162 }
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500163}
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500164
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500165/**
166 * Holder for using with [ZoneNameWrapper] to display names of zones.
167 */
168private class ZoneHolder(view: View) : Holder(view) {
169 private val zone: TextView = itemView as TextView
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500170
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500171 override fun bindData(wrapper: ElementWrapper) {
172 wrapper as ZoneNameWrapper
173 zone.text = wrapper.zoneName
174 }
175}
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500176
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500177/**
Fabian Kozynski8765d352020-04-06 21:16:02 -0400178 * Holder for using with [ControlStatusWrapper] to display names of zones.
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500179 * @param favoriteCallback this callback will be called whenever the favorite state of the
180 * [Control] this view represents changes.
181 */
Fabian Kozynski8765d352020-04-06 21:16:02 -0400182internal class ControlHolder(
183 view: View,
Fabian Kozynskib3c393f2020-05-08 14:29:30 -0400184 val withPosition: Boolean,
Fabian Kozynski8765d352020-04-06 21:16:02 -0400185 val favoriteCallback: ModelFavoriteChanger
186) : Holder(view) {
Fabian Kozynskib3c393f2020-05-08 14:29:30 -0400187 private val favoriteStateDescription =
188 itemView.context.getString(R.string.accessibility_control_favorite)
189 private val notFavoriteStateDescription =
190 itemView.context.getString(R.string.accessibility_control_not_favorite)
191
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500192 private val icon: ImageView = itemView.requireViewById(R.id.icon)
193 private val title: TextView = itemView.requireViewById(R.id.title)
194 private val subtitle: TextView = itemView.requireViewById(R.id.subtitle)
195 private val removed: TextView = itemView.requireViewById(R.id.status)
Fabian Kozynski9b972e82020-03-26 14:33:11 -0400196 private val favorite: CheckBox = itemView.requireViewById<CheckBox>(R.id.favorite).apply {
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500197 visibility = View.VISIBLE
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500198 }
199
Fabian Kozynskib3c393f2020-05-08 14:29:30 -0400200 private val accessibilityDelegate = ControlHolderAccessibilityDelegate(this::stateDescription)
201
202 init {
203 ViewCompat.setAccessibilityDelegate(itemView, accessibilityDelegate)
204 }
205
206 // Determine the stateDescription based on favorite state and maybe position
207 private fun stateDescription(favorite: Boolean): CharSequence? {
208 if (!favorite) {
209 return notFavoriteStateDescription
210 } else if (!withPosition) {
211 return favoriteStateDescription
212 } else {
213 val position = layoutPosition + 1
214 return itemView.context.getString(
215 R.string.accessibility_control_favorite_position, position)
216 }
217 }
218
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500219 override fun bindData(wrapper: ElementWrapper) {
Fabian Kozynski8765d352020-04-06 21:16:02 -0400220 wrapper as ControlInterface
221 val renderInfo = getRenderInfo(wrapper.component, wrapper.deviceType)
222 title.text = wrapper.title
223 subtitle.text = wrapper.subtitle
Fabian Kozynskib3c393f2020-05-08 14:29:30 -0400224 updateFavorite(wrapper.favorite)
225 removed.text = if (wrapper.removed) {
226 itemView.context.getText(R.string.controls_removed)
227 } else {
228 ""
229 }
Fabian Kozynskiaa1e5482020-04-02 11:22:01 -0400230 itemView.setOnClickListener {
Fabian Kozynskib3c393f2020-05-08 14:29:30 -0400231 updateFavorite(!favorite.isChecked)
Fabian Kozynski8765d352020-04-06 21:16:02 -0400232 favoriteCallback(wrapper.controlId, favorite.isChecked)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500233 }
234 applyRenderInfo(renderInfo)
235 }
236
Fabian Kozynski8765d352020-04-06 21:16:02 -0400237 override fun updateFavorite(favorite: Boolean) {
238 this.favorite.isChecked = favorite
Fabian Kozynskib3c393f2020-05-08 14:29:30 -0400239 accessibilityDelegate.isFavorite = favorite
240 itemView.stateDescription = stateDescription(favorite)
Fabian Kozynski8765d352020-04-06 21:16:02 -0400241 }
242
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500243 private fun getRenderInfo(
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500244 component: ComponentName,
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500245 @DeviceTypes.DeviceType deviceType: Int
246 ): RenderInfo {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500247 return RenderInfo.lookup(itemView.context, component, deviceType, true)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500248 }
249
250 private fun applyRenderInfo(ri: RenderInfo) {
251 val context = itemView.context
252 val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
253
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500254 icon.setImageDrawable(ri.icon)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500255 icon.setImageTintList(fg)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500256 }
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500257}
258
Fabian Kozynskib3c393f2020-05-08 14:29:30 -0400259private class ControlHolderAccessibilityDelegate(
260 val stateRetriever: (Boolean) -> CharSequence?
261) : AccessibilityDelegateCompat() {
262
263 var isFavorite = false
264
265 override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
266 super.onInitializeAccessibilityNodeInfo(host, info)
267
268 // Change the text for the double-tap action
269 val clickActionString = if (isFavorite) {
270 host.context.getString(R.string.accessibility_control_change_unfavorite)
271 } else {
272 host.context.getString(R.string.accessibility_control_change_favorite)
273 }
274 val click = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
275 AccessibilityNodeInfo.ACTION_CLICK,
276 // “favorite/unfavorite”
277 clickActionString)
278 info.addAction(click)
279
280 // Determine the stateDescription based on the holder information
281 info.stateDescription = stateRetriever(isFavorite)
282 // Remove the information at the end indicating row and column.
283 info.setCollectionItemInfo(null)
284
285 info.className = Switch::class.java.name
286 }
287}
288
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500289class MarginItemDecorator(
290 private val topMargin: Int,
291 private val sideMargins: Int
292) : RecyclerView.ItemDecoration() {
293
294 override fun getItemOffsets(
295 outRect: Rect,
296 view: View,
297 parent: RecyclerView,
298 state: RecyclerView.State
299 ) {
Fabian Kozynski6936cd12020-04-30 12:14:03 -0400300 val position = parent.getChildAdapterPosition(view)
301 if (position == RecyclerView.NO_POSITION) return
302 val type = parent.adapter?.getItemViewType(position)
303 if (type == ControlAdapter.TYPE_CONTROL) {
304 outRect.apply {
305 top = topMargin
306 left = sideMargins
307 right = sideMargins
308 bottom = 0
309 }
310 } else if (type == ControlAdapter.TYPE_ZONE && position == 0) {
311 // add negative padding to the first zone to counteract the margin
312 val margin = (view.layoutParams as ViewGroup.MarginLayoutParams).topMargin
313 outRect.apply {
314 top = -margin
315 left = 0
316 right = 0
317 bottom = 0
318 }
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500319 }
320 }
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500321}