blob: 79dd9edef0f006545581e6dc53add93211000352 [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
26import android.widget.CheckBox
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -050027import android.widget.ImageView
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050028import android.widget.TextView
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050029import androidx.recyclerview.widget.GridLayoutManager
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050030import androidx.recyclerview.widget.RecyclerView
31import com.android.systemui.R
Fabian Kozynski8765d352020-04-06 21:16:02 -040032import com.android.systemui.controls.ControlInterface
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -050033import com.android.systemui.controls.ui.RenderInfo
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050034
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050035private typealias ModelFavoriteChanger = (String, Boolean) -> Unit
36
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050037/**
38 * Adapter for binding [Control] information to views.
39 *
Fabian Kozynski8765d352020-04-06 21:16:02 -040040 * The model for this adapter is provided by a [ControlModel] that is set using
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050041 * [changeFavoritesModel]. This allows for updating the model if there's a reload.
42 *
Fabian Kozynski8765d352020-04-06 21:16:02 -040043 * @property elevation elevation of each control view
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050044 */
45class ControlAdapter(
Fabian Kozynski04e7bde2020-02-13 13:02:33 -050046 private val elevation: Float
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050047) : RecyclerView.Adapter<Holder>() {
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050048
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050049 companion object {
Fabian Kozynski6936cd12020-04-30 12:14:03 -040050 const val TYPE_ZONE = 0
51 const val TYPE_CONTROL = 1
52 const val TYPE_DIVIDER = 2
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050053 }
54
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050055 val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
56 override fun getSpanSize(position: Int): Int {
Fabian Kozynski8765d352020-04-06 21:16:02 -040057 return if (getItemViewType(position) != TYPE_CONTROL) 2 else 1
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050058 }
59 }
60
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050061 private var model: ControlsModel? = null
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050062
63 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
Fabian Kozynski713b7272020-03-03 18:35:52 -050064 val layoutInflater = LayoutInflater.from(parent.context)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050065 return when (viewType) {
66 TYPE_CONTROL -> {
67 ControlHolder(
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050068 layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply {
69 layoutParams.apply {
70 width = ViewGroup.LayoutParams.MATCH_PARENT
71 }
Fabian Kozynski04e7bde2020-02-13 13:02:33 -050072 elevation = this@ControlAdapter.elevation
Fabian Kozynski9b972e82020-03-26 14:33:11 -040073 background = parent.context.getDrawable(
74 R.drawable.control_background_ripple)
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050075 }
76 ) { id, favorite ->
77 model?.changeFavoriteStatus(id, favorite)
78 }
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050079 }
80 TYPE_ZONE -> {
81 ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false))
82 }
Fabian Kozynski8765d352020-04-06 21:16:02 -040083 TYPE_DIVIDER -> {
84 DividerHolder(layoutInflater.inflate(
Joshua Tsuji04631802020-04-13 12:30:34 -040085 R.layout.controls_horizontal_divider_with_empty, parent, false))
Fabian Kozynski8765d352020-04-06 21:16:02 -040086 }
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050087 else -> throw IllegalStateException("Wrong viewType: $viewType")
88 }
89 }
90
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050091 fun changeModel(model: ControlsModel) {
92 this.model = model
Fabian Kozynski9aa23af2020-02-05 17:47:47 -050093 notifyDataSetChanged()
94 }
95
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050096 override fun getItemCount() = model?.elements?.size ?: 0
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -050097
98 override fun onBindViewHolder(holder: Holder, index: Int) {
Fabian Kozynski1e3178d2020-02-19 09:32:27 -050099 model?.let {
100 holder.bindData(it.elements[index])
101 }
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500102 }
103
Fabian Kozynski8765d352020-04-06 21:16:02 -0400104 override fun onBindViewHolder(holder: Holder, position: Int, payloads: MutableList<Any>) {
105 if (payloads.isEmpty()) {
106 super.onBindViewHolder(holder, position, payloads)
107 } else {
108 model?.let {
109 val el = it.elements[position]
110 if (el is ControlInterface) {
111 holder.updateFavorite(el.favorite)
112 }
113 }
114 }
115 }
116
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500117 override fun getItemViewType(position: Int): Int {
Fabian Kozynski1e3178d2020-02-19 09:32:27 -0500118 model?.let {
119 return when (it.elements.get(position)) {
120 is ZoneNameWrapper -> TYPE_ZONE
Fabian Kozynski8765d352020-04-06 21:16:02 -0400121 is ControlStatusWrapper -> TYPE_CONTROL
122 is ControlInfoWrapper -> TYPE_CONTROL
123 is DividerWrapper -> TYPE_DIVIDER
Fabian Kozynski1e3178d2020-02-19 09:32:27 -0500124 }
125 } ?: throw IllegalStateException("Getting item type for null model")
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500126 }
127}
128
129/**
130 * Holder for binding views in the [RecyclerView]-
131 * @param view the [View] for this [Holder]
132 */
133sealed class Holder(view: View) : RecyclerView.ViewHolder(view) {
134
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500135 /**
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500136 * Bind the data from the model into the view
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500137 */
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500138 abstract fun bindData(wrapper: ElementWrapper)
Fabian Kozynski8765d352020-04-06 21:16:02 -0400139
140 open fun updateFavorite(favorite: Boolean) {}
141}
142
143/**
144 * Holder for using with [DividerWrapper] to display a divider between zones.
145 *
Fabian Kozynski6936cd12020-04-30 12:14:03 -0400146 * 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 -0400147 * be toggled visible or gone.
148 */
149private class DividerHolder(view: View) : Holder(view) {
150 private val frame: View = itemView.requireViewById(R.id.frame)
151 private val divider: View = itemView.requireViewById(R.id.divider)
152 override fun bindData(wrapper: ElementWrapper) {
153 wrapper as DividerWrapper
154 frame.visibility = if (wrapper.showNone) View.VISIBLE else View.GONE
155 divider.visibility = if (wrapper.showDivider) View.VISIBLE else View.GONE
156 }
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500157}
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500158
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500159/**
160 * Holder for using with [ZoneNameWrapper] to display names of zones.
161 */
162private class ZoneHolder(view: View) : Holder(view) {
163 private val zone: TextView = itemView as TextView
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500164
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500165 override fun bindData(wrapper: ElementWrapper) {
166 wrapper as ZoneNameWrapper
167 zone.text = wrapper.zoneName
168 }
169}
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500170
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500171/**
Fabian Kozynski8765d352020-04-06 21:16:02 -0400172 * Holder for using with [ControlStatusWrapper] to display names of zones.
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500173 * @param favoriteCallback this callback will be called whenever the favorite state of the
174 * [Control] this view represents changes.
175 */
Fabian Kozynski8765d352020-04-06 21:16:02 -0400176internal class ControlHolder(
177 view: View,
178 val favoriteCallback: ModelFavoriteChanger
179) : Holder(view) {
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500180 private val icon: ImageView = itemView.requireViewById(R.id.icon)
181 private val title: TextView = itemView.requireViewById(R.id.title)
182 private val subtitle: TextView = itemView.requireViewById(R.id.subtitle)
183 private val removed: TextView = itemView.requireViewById(R.id.status)
Fabian Kozynski9b972e82020-03-26 14:33:11 -0400184 private val favorite: CheckBox = itemView.requireViewById<CheckBox>(R.id.favorite).apply {
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500185 visibility = View.VISIBLE
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500186 }
187
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500188 override fun bindData(wrapper: ElementWrapper) {
Fabian Kozynski8765d352020-04-06 21:16:02 -0400189 wrapper as ControlInterface
190 val renderInfo = getRenderInfo(wrapper.component, wrapper.deviceType)
191 title.text = wrapper.title
192 subtitle.text = wrapper.subtitle
193 favorite.isChecked = wrapper.favorite
194 removed.text = if (wrapper.removed) "Removed" else ""
Fabian Kozynskiaa1e5482020-04-02 11:22:01 -0400195 itemView.setOnClickListener {
196 favorite.isChecked = !favorite.isChecked
Fabian Kozynski8765d352020-04-06 21:16:02 -0400197 favoriteCallback(wrapper.controlId, favorite.isChecked)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500198 }
199 applyRenderInfo(renderInfo)
200 }
201
Fabian Kozynski8765d352020-04-06 21:16:02 -0400202 override fun updateFavorite(favorite: Boolean) {
203 this.favorite.isChecked = favorite
204 }
205
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500206 private fun getRenderInfo(
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500207 component: ComponentName,
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500208 @DeviceTypes.DeviceType deviceType: Int
209 ): RenderInfo {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500210 return RenderInfo.lookup(itemView.context, component, deviceType, true)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500211 }
212
213 private fun applyRenderInfo(ri: RenderInfo) {
214 val context = itemView.context
215 val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
216
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500217 icon.setImageDrawable(ri.icon)
Fabian Kozynski9aa23af2020-02-05 17:47:47 -0500218 icon.setImageTintList(fg)
Fabian Kozynskif10b6ab2019-12-27 09:31:04 -0500219 }
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500220}
221
222class MarginItemDecorator(
223 private val topMargin: Int,
224 private val sideMargins: Int
225) : RecyclerView.ItemDecoration() {
226
227 override fun getItemOffsets(
228 outRect: Rect,
229 view: View,
230 parent: RecyclerView,
231 state: RecyclerView.State
232 ) {
Fabian Kozynski6936cd12020-04-30 12:14:03 -0400233 val position = parent.getChildAdapterPosition(view)
234 if (position == RecyclerView.NO_POSITION) return
235 val type = parent.adapter?.getItemViewType(position)
236 if (type == ControlAdapter.TYPE_CONTROL) {
237 outRect.apply {
238 top = topMargin
239 left = sideMargins
240 right = sideMargins
241 bottom = 0
242 }
243 } else if (type == ControlAdapter.TYPE_ZONE && position == 0) {
244 // add negative padding to the first zone to counteract the margin
245 val margin = (view.layoutParams as ViewGroup.MarginLayoutParams).topMargin
246 outRect.apply {
247 top = -margin
248 left = 0
249 right = 0
250 bottom = 0
251 }
Fabian Kozynski5fc5f6b2020-02-03 15:21:14 -0500252 }
253 }
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500254}