blob: 2653ce0423b803b7638332c2f3373ee6db95fcf0 [file] [log] [blame]
Matt Pietal22231792020-01-23 09:51:09 -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.ui
18
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070019import android.animation.Animator
20import android.animation.AnimatorListenerAdapter
21import android.animation.ValueAnimator
Matt Pietal94316e92020-04-22 10:58:32 -040022import android.app.Dialog
Matt Pietal22231792020-01-23 09:51:09 -050023import android.content.Context
24import android.graphics.drawable.ClipDrawable
Matt Pietala103a942020-03-13 12:10:35 -040025import android.graphics.drawable.GradientDrawable
Matt Pietal22231792020-01-23 09:51:09 -050026import android.graphics.drawable.LayerDrawable
27import android.service.controls.Control
Matt Pietalf8cc0fa2020-03-26 08:48:50 -040028import android.service.controls.DeviceTypes
Matt Pietal22231792020-01-23 09:51:09 -050029import android.service.controls.actions.ControlAction
Matt Pietal22231792020-01-23 09:51:09 -050030import android.service.controls.templates.ControlTemplate
Matt Pietal53a8bbd2020-03-05 16:10:34 -050031import android.service.controls.templates.StatelessTemplate
Matt Pietala635bd12020-02-03 13:36:18 -050032import android.service.controls.templates.TemperatureControlTemplate
Matt Pietal22231792020-01-23 09:51:09 -050033import android.service.controls.templates.ToggleRangeTemplate
34import android.service.controls.templates.ToggleTemplate
Lucas Dupind60b3322020-04-15 18:06:47 -070035import android.util.MathUtils
Matt Pietal22231792020-01-23 09:51:09 -050036import android.view.View
37import android.view.ViewGroup
38import android.widget.ImageView
39import android.widget.TextView
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070040import com.android.internal.graphics.ColorUtils
41import com.android.systemui.Interpolators
42import com.android.systemui.R
Matt Pietal22231792020-01-23 09:51:09 -050043import com.android.systemui.controls.controller.ControlsController
Matt Pietal1aac43b2020-02-04 15:43:31 -050044import com.android.systemui.util.concurrency.DelayableExecutor
Matt Pietalb582b692020-02-14 19:37:57 -050045import kotlin.reflect.KClass
46
Matt Pietaldc78c842020-03-30 08:09:18 -040047/**
48 * Wraps the widgets that make up the UI representation of a {@link Control}. Updates to the view
49 * are signaled via calls to {@link #bindData}. Similar to the ViewHolder concept used in
50 * RecyclerViews.
51 */
Matt Pietal22231792020-01-23 09:51:09 -050052class ControlViewHolder(
53 val layout: ViewGroup,
Matt Pietal1aac43b2020-02-04 15:43:31 -050054 val controlsController: ControlsController,
Matt Pietal5bdfba42020-02-14 09:14:43 -050055 val uiExecutor: DelayableExecutor,
Matt Pietal677981d2020-04-24 14:38:32 -040056 val bgExecutor: DelayableExecutor,
57 val controlActionCoordinator: ControlActionCoordinator
Matt Pietal22231792020-01-23 09:51:09 -050058) {
Matt Pietaldc78c842020-03-30 08:09:18 -040059
60 companion object {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070061 const val STATE_ANIMATION_DURATION = 700L
Matt Pietaldc78c842020-03-30 08:09:18 -040062 private const val UPDATE_DELAY_IN_MILLIS = 3000L
Matt Pietald8ca2c02020-04-24 19:20:48 -040063 private const val ALPHA_ENABLED = 255
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070064 private const val ALPHA_DISABLED = 0
Matt Pietaldc78c842020-03-30 08:09:18 -040065 private val FORCE_PANEL_DEVICES = setOf(
66 DeviceTypes.TYPE_THERMOSTAT,
67 DeviceTypes.TYPE_CAMERA
68 )
Matt Pietal677981d2020-04-24 14:38:32 -040069
70 const val MIN_LEVEL = 0
71 const val MAX_LEVEL = 10000
Matt Pietaldc78c842020-03-30 08:09:18 -040072 }
73
Lucas Dupin92f6cca2020-04-14 22:30:54 -070074 private val toggleBackgroundIntensity: Float = layout.context.resources
75 .getFraction(R.fraction.controls_toggle_bg_intensity, 1, 1)
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070076 private var stateAnimator: ValueAnimator? = null
Lucas Dupin92f6cca2020-04-14 22:30:54 -070077 private val baseLayer: GradientDrawable
Matt Pietal22231792020-01-23 09:51:09 -050078 val icon: ImageView = layout.requireViewById(R.id.icon)
79 val status: TextView = layout.requireViewById(R.id.status)
Matt Pietal22231792020-01-23 09:51:09 -050080 val title: TextView = layout.requireViewById(R.id.title)
81 val subtitle: TextView = layout.requireViewById(R.id.subtitle)
82 val context: Context = layout.getContext()
83 val clipLayer: ClipDrawable
Matt Pietal22231792020-01-23 09:51:09 -050084 lateinit var cws: ControlWithState
Matt Pietal1aac43b2020-02-04 15:43:31 -050085 var cancelUpdate: Runnable? = null
Matt Pietalb582b692020-02-14 19:37:57 -050086 var behavior: Behavior? = null
Matt Pietalb7da66c2020-03-06 10:49:31 -050087 var lastAction: ControlAction? = null
Matt Pietal94316e92020-04-22 10:58:32 -040088 private var lastChallengeDialog: Dialog? = null
89
Matt Pietaldc78c842020-03-30 08:09:18 -040090 val deviceType: Int
91 get() = cws.control?.let { it.getDeviceType() } ?: cws.ci.deviceType
Matt Pietal22231792020-01-23 09:51:09 -050092
93 init {
94 val ld = layout.getBackground() as LayerDrawable
95 ld.mutate()
96 clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) as ClipDrawable
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070097 clipLayer.alpha = ALPHA_DISABLED
Lucas Dupin92f6cca2020-04-14 22:30:54 -070098 baseLayer = ld.findDrawableByLayerId(R.id.background) as GradientDrawable
Matt Pietal5f478c72020-04-01 15:53:54 -040099 // needed for marquee to start
100 status.setSelected(true)
Matt Pietal22231792020-01-23 09:51:09 -0500101 }
102
103 fun bindData(cws: ControlWithState) {
104 this.cws = cws
105
Matt Pietal1aac43b2020-02-04 15:43:31 -0500106 cancelUpdate?.run()
107
Matt Pietal31ec9f22020-03-12 09:02:17 -0400108 val (controlStatus, template) = cws.control?.let {
Matt Pietal22231792020-01-23 09:51:09 -0500109 title.setText(it.getTitle())
110 subtitle.setText(it.getSubtitle())
Matt Pietaldc78c842020-03-30 08:09:18 -0400111 Pair(it.status, it.controlTemplate)
Matt Pietal22231792020-01-23 09:51:09 -0500112 } ?: run {
113 title.setText(cws.ci.controlTitle)
Matt Pietal85878262020-03-18 15:34:46 -0400114 subtitle.setText(cws.ci.controlSubtitle)
Matt Pietal22231792020-01-23 09:51:09 -0500115 Pair(Control.STATUS_UNKNOWN, ControlTemplate.NO_TEMPLATE)
116 }
117
Matt Pietal307b1ef2020-02-10 07:27:01 -0500118 cws.control?.let {
Matt Pietal31ec9f22020-03-12 09:02:17 -0400119 layout.setClickable(true)
Matt Pietala635bd12020-02-03 13:36:18 -0500120 layout.setOnLongClickListener(View.OnLongClickListener() {
Matt Pietal677981d2020-04-24 14:38:32 -0400121 controlActionCoordinator.longPress(this@ControlViewHolder)
Matt Pietala635bd12020-02-03 13:36:18 -0500122 true
123 })
124 }
125
Matt Pietaldc78c842020-03-30 08:09:18 -0400126 val clazz = findBehavior(controlStatus, template, deviceType)
Matt Pietalb582b692020-02-14 19:37:57 -0500127 if (behavior == null || behavior!!::class != clazz) {
128 // Behavior changes can signal a change in template from the app or
129 // first time setup
130 behavior = clazz.java.newInstance()
131 behavior?.initialize(this)
Matt Pietal31ec9f22020-03-12 09:02:17 -0400132
133 // let behaviors define their own, if necessary, and clear any existing ones
134 layout.setAccessibilityDelegate(null)
Matt Pietalb582b692020-02-14 19:37:57 -0500135 }
Matt Pietal31ec9f22020-03-12 09:02:17 -0400136
Matt Pietalb582b692020-02-14 19:37:57 -0500137 behavior?.bind(cws)
Matt Pietal31ec9f22020-03-12 09:02:17 -0400138
Matt Pietal5f478c72020-04-01 15:53:54 -0400139 layout.setContentDescription("${title.text} ${subtitle.text} ${status.text}")
Matt Pietal22231792020-01-23 09:51:09 -0500140 }
141
Matt Pietal1aac43b2020-02-04 15:43:31 -0500142 fun actionResponse(@ControlAction.ResponseResult response: Int) {
Matt Pietal94316e92020-04-22 10:58:32 -0400143 // OK responses signal normal behavior, and the app will provide control updates
144 val failedAttempt = lastChallengeDialog != null
145 when (response) {
146 ControlAction.RESPONSE_OK ->
147 lastChallengeDialog = null
148 ControlAction.RESPONSE_UNKNOWN -> {
149 lastChallengeDialog = null
150 setTransientStatus(context.resources.getString(R.string.controls_error_failed))
151 }
152 ControlAction.RESPONSE_FAIL -> {
153 lastChallengeDialog = null
154 setTransientStatus(context.resources.getString(R.string.controls_error_failed))
155 }
156 ControlAction.RESPONSE_CHALLENGE_PIN -> {
157 lastChallengeDialog = ChallengeDialogs.createPinDialog(this, false, failedAttempt)
158 lastChallengeDialog?.show()
159 }
160 ControlAction.RESPONSE_CHALLENGE_PASSPHRASE -> {
161 lastChallengeDialog = ChallengeDialogs.createPinDialog(this, true, failedAttempt)
162 lastChallengeDialog?.show()
163 }
164 ControlAction.RESPONSE_CHALLENGE_ACK -> {
165 lastChallengeDialog = ChallengeDialogs.createConfirmationDialog(this)
166 lastChallengeDialog?.show()
167 }
168 }
169 }
170
171 fun dismiss() {
172 lastChallengeDialog?.dismiss()
173 lastChallengeDialog = null
Matt Pietal1aac43b2020-02-04 15:43:31 -0500174 }
175
Matt Pietal307b1ef2020-02-10 07:27:01 -0500176 fun setTransientStatus(tempStatus: String) {
177 val previousText = status.getText()
Matt Pietal307b1ef2020-02-10 07:27:01 -0500178
179 cancelUpdate = uiExecutor.executeDelayed({
180 status.setText(previousText)
Matt Pietal307b1ef2020-02-10 07:27:01 -0500181 }, UPDATE_DELAY_IN_MILLIS)
182
183 status.setText(tempStatus)
Matt Pietal307b1ef2020-02-10 07:27:01 -0500184 }
185
Matt Pietal22231792020-01-23 09:51:09 -0500186 fun action(action: ControlAction) {
Matt Pietalb7da66c2020-03-06 10:49:31 -0500187 lastAction = action
Matt Pietal313f37d2020-02-24 11:27:22 -0500188 controlsController.action(cws.componentName, cws.ci, action)
Matt Pietal22231792020-01-23 09:51:09 -0500189 }
190
Matt Pietal6b4b65c2020-04-22 14:16:53 -0400191 fun usePanel(): Boolean = deviceType in ControlViewHolder.FORCE_PANEL_DEVICES
Matt Pietaldc78c842020-03-30 08:09:18 -0400192
193 private fun findBehavior(
194 status: Int,
195 template: ControlTemplate,
196 deviceType: Int
197 ): KClass<out Behavior> {
Matt Pietal22231792020-01-23 09:51:09 -0500198 return when {
Matt Pietal94316e92020-04-22 10:58:32 -0400199 status == Control.STATUS_UNKNOWN -> StatusBehavior::class
200 status == Control.STATUS_ERROR -> StatusBehavior::class
201 status == Control.STATUS_NOT_FOUND -> StatusBehavior::class
Matt Pietaldc78c842020-03-30 08:09:18 -0400202 deviceType == DeviceTypes.TYPE_CAMERA -> TouchBehavior::class
Matt Pietalb582b692020-02-14 19:37:57 -0500203 template is ToggleTemplate -> ToggleBehavior::class
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500204 template is StatelessTemplate -> TouchBehavior::class
Matt Pietalb582b692020-02-14 19:37:57 -0500205 template is ToggleRangeTemplate -> ToggleRangeBehavior::class
206 template is TemperatureControlTemplate -> TemperatureControlBehavior::class
Matt Pietalb582b692020-02-14 19:37:57 -0500207 else -> DefaultBehavior::class
Matt Pietal22231792020-01-23 09:51:09 -0500208 }
209 }
210
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700211 internal fun applyRenderInfo(enabled: Boolean, offset: Int = 0, animated: Boolean = true) {
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500212 setEnabled(enabled)
213
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500214 val ri = RenderInfo.lookup(context, cws.componentName, deviceType, enabled, offset)
215
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700216 val fg = context.resources.getColorStateList(ri.foreground, context.theme)
217 val bg = context.resources.getColor(R.color.control_default_background, context.theme)
Matt Pietal4f5cd672020-04-22 13:51:11 -0400218 var (newClipColor, newAlpha) = if (enabled) {
219 // allow color overrides for the enabled state only
220 val color = cws.control?.getCustomColor()?.let {
221 val state = intArrayOf(android.R.attr.state_enabled)
222 it.getColorForState(state, it.getDefaultColor())
223 } ?: context.resources.getColor(ri.enabledBackground, context.theme)
224 listOf(color, ALPHA_ENABLED)
Matt Pietala103a942020-03-13 12:10:35 -0400225 } else {
Matt Pietal4f5cd672020-04-22 13:51:11 -0400226 listOf(
227 context.resources.getColor(R.color.control_default_background, context.theme),
228 ALPHA_DISABLED
229 )
Matt Pietala103a942020-03-13 12:10:35 -0400230 }
231
Matt Pietal22231792020-01-23 09:51:09 -0500232 status.setTextColor(fg)
Matt Pietalf8cc0fa2020-03-26 08:48:50 -0400233
Matt Pietal4f5cd672020-04-22 13:51:11 -0400234 cws.control?.getCustomIcon()?.let {
235 // do not tint custom icons, assume the intended icon color is correct
236 icon.imageTintList = null
237 icon.setImageIcon(it)
238 } ?: run {
239 icon.setImageDrawable(ri.icon)
240
241 // do not color app icons
242 if (deviceType != DeviceTypes.TYPE_ROUTINE) {
243 icon.imageTintList = fg
244 }
Matt Pietalf8cc0fa2020-03-26 08:48:50 -0400245 }
Matt Pietal22231792020-01-23 09:51:09 -0500246
Matt Pietala103a942020-03-13 12:10:35 -0400247 (clipLayer.getDrawable() as GradientDrawable).apply {
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700248 val newBaseColor = if (behavior is ToggleRangeBehavior) {
249 ColorUtils.blendARGB(bg, newClipColor, toggleBackgroundIntensity)
250 } else {
251 bg
252 }
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700253 stateAnimator?.cancel()
254 if (animated) {
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700255 val oldColor = color?.defaultColor ?: newClipColor
256 val oldBaseColor = baseLayer.color?.defaultColor ?: newBaseColor
Lucas Dupind60b3322020-04-15 18:06:47 -0700257 val oldAlpha = layout.alpha
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700258 stateAnimator = ValueAnimator.ofInt(clipLayer.alpha, newAlpha).apply {
259 addUpdateListener {
260 alpha = it.animatedValue as Int
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700261 setColor(ColorUtils.blendARGB(oldColor, newClipColor, it.animatedFraction))
262 baseLayer.setColor(ColorUtils.blendARGB(oldBaseColor,
263 newBaseColor, it.animatedFraction))
Matt Pietale27ca7c2020-05-01 08:29:10 -0400264 layout.alpha = MathUtils.lerp(oldAlpha, 1f, it.animatedFraction)
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700265 }
266 addListener(object : AnimatorListenerAdapter() {
267 override fun onAnimationEnd(animation: Animator?) {
268 stateAnimator = null
269 }
270 })
271 duration = STATE_ANIMATION_DURATION
272 interpolator = Interpolators.CONTROL_STATE
273 start()
274 }
275 } else {
276 alpha = newAlpha
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700277 setColor(newClipColor)
278 baseLayer.setColor(newBaseColor)
Matt Pietale27ca7c2020-05-01 08:29:10 -0400279 layout.alpha = 1f
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700280 }
Matt Pietalb582b692020-02-14 19:37:57 -0500281 }
Matt Pietal22231792020-01-23 09:51:09 -0500282 }
Matt Pietalb582b692020-02-14 19:37:57 -0500283
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500284 private fun setEnabled(enabled: Boolean) {
Matt Pietal22231792020-01-23 09:51:09 -0500285 status.setEnabled(enabled)
286 icon.setEnabled(enabled)
287 }
288}