Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package com.android.systemui.controls.management |
| 18 | |
Matt Pietal | 53a8bbd | 2020-03-05 16:10:34 -0500 | [diff] [blame] | 19 | import android.content.ComponentName |
Fabian Kozynski | 5fc5f6b | 2020-02-03 15:21:14 -0500 | [diff] [blame] | 20 | import android.graphics.Rect |
Fabian Kozynski | 6936cd1 | 2020-04-30 12:14:03 -0400 | [diff] [blame] | 21 | import android.service.controls.Control |
Fabian Kozynski | 5fc5f6b | 2020-02-03 15:21:14 -0500 | [diff] [blame] | 22 | import android.service.controls.DeviceTypes |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 23 | import android.view.LayoutInflater |
| 24 | import android.view.View |
| 25 | import android.view.ViewGroup |
| 26 | import android.widget.CheckBox |
Fabian Kozynski | 5fc5f6b | 2020-02-03 15:21:14 -0500 | [diff] [blame] | 27 | import android.widget.ImageView |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 28 | import android.widget.TextView |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 29 | import androidx.recyclerview.widget.GridLayoutManager |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 30 | import androidx.recyclerview.widget.RecyclerView |
| 31 | import com.android.systemui.R |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 32 | import com.android.systemui.controls.ControlInterface |
Fabian Kozynski | 5fc5f6b | 2020-02-03 15:21:14 -0500 | [diff] [blame] | 33 | import com.android.systemui.controls.ui.RenderInfo |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 34 | |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 35 | private typealias ModelFavoriteChanger = (String, Boolean) -> Unit |
| 36 | |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 37 | /** |
| 38 | * Adapter for binding [Control] information to views. |
| 39 | * |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 40 | * The model for this adapter is provided by a [ControlModel] that is set using |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 41 | * [changeFavoritesModel]. This allows for updating the model if there's a reload. |
| 42 | * |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 43 | * @property elevation elevation of each control view |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 44 | */ |
| 45 | class ControlAdapter( |
Fabian Kozynski | 04e7bde | 2020-02-13 13:02:33 -0500 | [diff] [blame] | 46 | private val elevation: Float |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 47 | ) : RecyclerView.Adapter<Holder>() { |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 48 | |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 49 | companion object { |
Fabian Kozynski | 6936cd1 | 2020-04-30 12:14:03 -0400 | [diff] [blame] | 50 | const val TYPE_ZONE = 0 |
| 51 | const val TYPE_CONTROL = 1 |
| 52 | const val TYPE_DIVIDER = 2 |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 53 | } |
| 54 | |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 55 | val spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { |
| 56 | override fun getSpanSize(position: Int): Int { |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 57 | return if (getItemViewType(position) != TYPE_CONTROL) 2 else 1 |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 58 | } |
| 59 | } |
| 60 | |
Fabian Kozynski | 1e3178d | 2020-02-19 09:32:27 -0500 | [diff] [blame] | 61 | private var model: ControlsModel? = null |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 62 | |
| 63 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { |
Fabian Kozynski | 713b727 | 2020-03-03 18:35:52 -0500 | [diff] [blame] | 64 | val layoutInflater = LayoutInflater.from(parent.context) |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 65 | return when (viewType) { |
| 66 | TYPE_CONTROL -> { |
| 67 | ControlHolder( |
Fabian Kozynski | 1e3178d | 2020-02-19 09:32:27 -0500 | [diff] [blame] | 68 | layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply { |
| 69 | layoutParams.apply { |
| 70 | width = ViewGroup.LayoutParams.MATCH_PARENT |
| 71 | } |
Fabian Kozynski | 04e7bde | 2020-02-13 13:02:33 -0500 | [diff] [blame] | 72 | elevation = this@ControlAdapter.elevation |
Fabian Kozynski | 9b972e8 | 2020-03-26 14:33:11 -0400 | [diff] [blame] | 73 | background = parent.context.getDrawable( |
| 74 | R.drawable.control_background_ripple) |
Fabian Kozynski | 1e3178d | 2020-02-19 09:32:27 -0500 | [diff] [blame] | 75 | } |
| 76 | ) { id, favorite -> |
| 77 | model?.changeFavoriteStatus(id, favorite) |
| 78 | } |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 79 | } |
| 80 | TYPE_ZONE -> { |
| 81 | ZoneHolder(layoutInflater.inflate(R.layout.controls_zone_header, parent, false)) |
| 82 | } |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 83 | TYPE_DIVIDER -> { |
| 84 | DividerHolder(layoutInflater.inflate( |
Joshua Tsuji | 0463180 | 2020-04-13 12:30:34 -0400 | [diff] [blame] | 85 | R.layout.controls_horizontal_divider_with_empty, parent, false)) |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 86 | } |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 87 | else -> throw IllegalStateException("Wrong viewType: $viewType") |
| 88 | } |
| 89 | } |
| 90 | |
Fabian Kozynski | 1e3178d | 2020-02-19 09:32:27 -0500 | [diff] [blame] | 91 | fun changeModel(model: ControlsModel) { |
| 92 | this.model = model |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 93 | notifyDataSetChanged() |
| 94 | } |
| 95 | |
Fabian Kozynski | 1e3178d | 2020-02-19 09:32:27 -0500 | [diff] [blame] | 96 | override fun getItemCount() = model?.elements?.size ?: 0 |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 97 | |
| 98 | override fun onBindViewHolder(holder: Holder, index: Int) { |
Fabian Kozynski | 1e3178d | 2020-02-19 09:32:27 -0500 | [diff] [blame] | 99 | model?.let { |
| 100 | holder.bindData(it.elements[index]) |
| 101 | } |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 102 | } |
| 103 | |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 104 | 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 Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 117 | override fun getItemViewType(position: Int): Int { |
Fabian Kozynski | 1e3178d | 2020-02-19 09:32:27 -0500 | [diff] [blame] | 118 | model?.let { |
| 119 | return when (it.elements.get(position)) { |
| 120 | is ZoneNameWrapper -> TYPE_ZONE |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 121 | is ControlStatusWrapper -> TYPE_CONTROL |
| 122 | is ControlInfoWrapper -> TYPE_CONTROL |
| 123 | is DividerWrapper -> TYPE_DIVIDER |
Fabian Kozynski | 1e3178d | 2020-02-19 09:32:27 -0500 | [diff] [blame] | 124 | } |
| 125 | } ?: throw IllegalStateException("Getting item type for null model") |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 126 | } |
| 127 | } |
| 128 | |
| 129 | /** |
| 130 | * Holder for binding views in the [RecyclerView]- |
| 131 | * @param view the [View] for this [Holder] |
| 132 | */ |
| 133 | sealed class Holder(view: View) : RecyclerView.ViewHolder(view) { |
| 134 | |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 135 | /** |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 136 | * Bind the data from the model into the view |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 137 | */ |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 138 | abstract fun bindData(wrapper: ElementWrapper) |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 139 | |
| 140 | open fun updateFavorite(favorite: Boolean) {} |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Holder for using with [DividerWrapper] to display a divider between zones. |
| 145 | * |
Fabian Kozynski | 6936cd1 | 2020-04-30 12:14:03 -0400 | [diff] [blame] | 146 | * The divider can be shown or hidden. It also has a view the height of a control, that can |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 147 | * be toggled visible or gone. |
| 148 | */ |
| 149 | private 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 Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 157 | } |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 158 | |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 159 | /** |
| 160 | * Holder for using with [ZoneNameWrapper] to display names of zones. |
| 161 | */ |
| 162 | private class ZoneHolder(view: View) : Holder(view) { |
| 163 | private val zone: TextView = itemView as TextView |
Fabian Kozynski | 5fc5f6b | 2020-02-03 15:21:14 -0500 | [diff] [blame] | 164 | |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 165 | override fun bindData(wrapper: ElementWrapper) { |
| 166 | wrapper as ZoneNameWrapper |
| 167 | zone.text = wrapper.zoneName |
| 168 | } |
| 169 | } |
Fabian Kozynski | 5fc5f6b | 2020-02-03 15:21:14 -0500 | [diff] [blame] | 170 | |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 171 | /** |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 172 | * Holder for using with [ControlStatusWrapper] to display names of zones. |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 173 | * @param favoriteCallback this callback will be called whenever the favorite state of the |
| 174 | * [Control] this view represents changes. |
| 175 | */ |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 176 | internal class ControlHolder( |
| 177 | view: View, |
| 178 | val favoriteCallback: ModelFavoriteChanger |
| 179 | ) : Holder(view) { |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 180 | 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 Kozynski | 9b972e8 | 2020-03-26 14:33:11 -0400 | [diff] [blame] | 184 | private val favorite: CheckBox = itemView.requireViewById<CheckBox>(R.id.favorite).apply { |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 185 | visibility = View.VISIBLE |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 186 | } |
| 187 | |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 188 | override fun bindData(wrapper: ElementWrapper) { |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 189 | 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 Kozynski | aa1e548 | 2020-04-02 11:22:01 -0400 | [diff] [blame] | 195 | itemView.setOnClickListener { |
| 196 | favorite.isChecked = !favorite.isChecked |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 197 | favoriteCallback(wrapper.controlId, favorite.isChecked) |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 198 | } |
| 199 | applyRenderInfo(renderInfo) |
| 200 | } |
| 201 | |
Fabian Kozynski | 8765d35 | 2020-04-06 21:16:02 -0400 | [diff] [blame] | 202 | override fun updateFavorite(favorite: Boolean) { |
| 203 | this.favorite.isChecked = favorite |
| 204 | } |
| 205 | |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 206 | private fun getRenderInfo( |
Matt Pietal | 53a8bbd | 2020-03-05 16:10:34 -0500 | [diff] [blame] | 207 | component: ComponentName, |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 208 | @DeviceTypes.DeviceType deviceType: Int |
| 209 | ): RenderInfo { |
Matt Pietal | 53a8bbd | 2020-03-05 16:10:34 -0500 | [diff] [blame] | 210 | return RenderInfo.lookup(itemView.context, component, deviceType, true) |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 211 | } |
| 212 | |
| 213 | private fun applyRenderInfo(ri: RenderInfo) { |
| 214 | val context = itemView.context |
| 215 | val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme()) |
| 216 | |
Matt Pietal | 53a8bbd | 2020-03-05 16:10:34 -0500 | [diff] [blame] | 217 | icon.setImageDrawable(ri.icon) |
Fabian Kozynski | 9aa23af | 2020-02-05 17:47:47 -0500 | [diff] [blame] | 218 | icon.setImageTintList(fg) |
Fabian Kozynski | f10b6ab | 2019-12-27 09:31:04 -0500 | [diff] [blame] | 219 | } |
Fabian Kozynski | 5fc5f6b | 2020-02-03 15:21:14 -0500 | [diff] [blame] | 220 | } |
| 221 | |
| 222 | class 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 Kozynski | 6936cd1 | 2020-04-30 12:14:03 -0400 | [diff] [blame] | 233 | 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 Kozynski | 5fc5f6b | 2020-02-03 15:21:14 -0500 | [diff] [blame] | 252 | } |
| 253 | } |
Matt Pietal | 53a8bbd | 2020-03-05 16:10:34 -0500 | [diff] [blame] | 254 | } |