blob: 865a38a2076c3cba9dced3cedde3009ea7ff4f12 [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
Matt Pietaldc6bb1d2020-05-14 13:33:41 -040021import android.animation.AnimatorSet
22import android.animation.ObjectAnimator
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070023import android.animation.ValueAnimator
Matt Pietaldc6bb1d2020-05-14 13:33:41 -040024import android.annotation.ColorRes
Matt Pietal94316e92020-04-22 10:58:32 -040025import android.app.Dialog
Matt Pietal22231792020-01-23 09:51:09 -050026import android.content.Context
Matt Pietaldc6bb1d2020-05-14 13:33:41 -040027import android.content.res.ColorStateList
Matt Pietal22231792020-01-23 09:51:09 -050028import android.graphics.drawable.ClipDrawable
Matt Pietaldc6bb1d2020-05-14 13:33:41 -040029import android.graphics.drawable.Drawable
Matt Pietala103a942020-03-13 12:10:35 -040030import android.graphics.drawable.GradientDrawable
Matt Pietal22231792020-01-23 09:51:09 -050031import android.graphics.drawable.LayerDrawable
Matt Pietal798d1132020-06-01 09:47:42 -040032import android.graphics.drawable.StateListDrawable
Matt Pietal22231792020-01-23 09:51:09 -050033import android.service.controls.Control
Matt Pietalf8cc0fa2020-03-26 08:48:50 -040034import android.service.controls.DeviceTypes
Matt Pietal22231792020-01-23 09:51:09 -050035import android.service.controls.actions.ControlAction
Matt Pietal22231792020-01-23 09:51:09 -050036import android.service.controls.templates.ControlTemplate
Matt Pietalcf516132020-05-07 16:50:04 -040037import android.service.controls.templates.RangeTemplate
Matt Pietal53a8bbd2020-03-05 16:10:34 -050038import android.service.controls.templates.StatelessTemplate
Matt Pietala635bd12020-02-03 13:36:18 -050039import android.service.controls.templates.TemperatureControlTemplate
Matt Pietal22231792020-01-23 09:51:09 -050040import android.service.controls.templates.ToggleRangeTemplate
41import android.service.controls.templates.ToggleTemplate
Lucas Dupind60b3322020-04-15 18:06:47 -070042import android.util.MathUtils
Matt Pietaldc6bb1d2020-05-14 13:33:41 -040043import android.util.TypedValue
Matt Pietal22231792020-01-23 09:51:09 -050044import android.view.View
45import android.view.ViewGroup
46import android.widget.ImageView
47import android.widget.TextView
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070048import com.android.internal.graphics.ColorUtils
49import com.android.systemui.Interpolators
50import com.android.systemui.R
Matt Pietal22231792020-01-23 09:51:09 -050051import com.android.systemui.controls.controller.ControlsController
Matt Pietal1aac43b2020-02-04 15:43:31 -050052import com.android.systemui.util.concurrency.DelayableExecutor
Matt Pietalb582b692020-02-14 19:37:57 -050053import kotlin.reflect.KClass
54
Matt Pietaldc78c842020-03-30 08:09:18 -040055/**
56 * Wraps the widgets that make up the UI representation of a {@link Control}. Updates to the view
57 * are signaled via calls to {@link #bindData}. Similar to the ViewHolder concept used in
58 * RecyclerViews.
59 */
Matt Pietal22231792020-01-23 09:51:09 -050060class ControlViewHolder(
61 val layout: ViewGroup,
Matt Pietal1aac43b2020-02-04 15:43:31 -050062 val controlsController: ControlsController,
Matt Pietal5bdfba42020-02-14 09:14:43 -050063 val uiExecutor: DelayableExecutor,
Matt Pietal677981d2020-04-24 14:38:32 -040064 val bgExecutor: DelayableExecutor,
65 val controlActionCoordinator: ControlActionCoordinator
Matt Pietal22231792020-01-23 09:51:09 -050066) {
Matt Pietaldc78c842020-03-30 08:09:18 -040067
68 companion object {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070069 const val STATE_ANIMATION_DURATION = 700L
Matt Pietald8ca2c02020-04-24 19:20:48 -040070 private const val ALPHA_ENABLED = 255
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070071 private const val ALPHA_DISABLED = 0
Matt Pietaldc6bb1d2020-05-14 13:33:41 -040072 private const val STATUS_ALPHA_ENABLED = 1f
73 private const val STATUS_ALPHA_DIMMED = 0.45f
Matt Pietaldc78c842020-03-30 08:09:18 -040074 private val FORCE_PANEL_DEVICES = setOf(
75 DeviceTypes.TYPE_THERMOSTAT,
76 DeviceTypes.TYPE_CAMERA
77 )
Matt Pietal798d1132020-06-01 09:47:42 -040078 private val ATTR_ENABLED = intArrayOf(android.R.attr.state_enabled)
79 private val ATTR_DISABLED = intArrayOf(-android.R.attr.state_enabled)
Matt Pietal677981d2020-04-24 14:38:32 -040080 const val MIN_LEVEL = 0
81 const val MAX_LEVEL = 10000
Matt Pietalcf516132020-05-07 16:50:04 -040082
83 fun findBehaviorClass(
84 status: Int,
85 template: ControlTemplate,
86 deviceType: Int
87 ): KClass<out Behavior> {
88 return when {
Matt Pietal96f746b2020-06-08 14:40:00 -040089 status != Control.STATUS_OK -> StatusBehavior::class
Matt Pietalcf516132020-05-07 16:50:04 -040090 deviceType == DeviceTypes.TYPE_CAMERA -> TouchBehavior::class
Matt Pietal733d6372020-06-12 09:36:52 -040091 template == ControlTemplate.NO_TEMPLATE -> TouchBehavior::class
Matt Pietalcf516132020-05-07 16:50:04 -040092 template is ToggleTemplate -> ToggleBehavior::class
93 template is StatelessTemplate -> TouchBehavior::class
94 template is ToggleRangeTemplate -> ToggleRangeBehavior::class
95 template is RangeTemplate -> ToggleRangeBehavior::class
96 template is TemperatureControlTemplate -> TemperatureControlBehavior::class
97 else -> DefaultBehavior::class
98 }
99 }
Matt Pietaldc78c842020-03-30 08:09:18 -0400100 }
101
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700102 private val toggleBackgroundIntensity: Float = layout.context.resources
103 .getFraction(R.fraction.controls_toggle_bg_intensity, 1, 1)
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700104 private var stateAnimator: ValueAnimator? = null
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400105 private var statusAnimator: Animator? = null
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700106 private val baseLayer: GradientDrawable
Matt Pietal22231792020-01-23 09:51:09 -0500107 val icon: ImageView = layout.requireViewById(R.id.icon)
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400108 private val status: TextView = layout.requireViewById(R.id.status)
109 private var nextStatusText: CharSequence = ""
Matt Pietal22231792020-01-23 09:51:09 -0500110 val title: TextView = layout.requireViewById(R.id.title)
111 val subtitle: TextView = layout.requireViewById(R.id.subtitle)
112 val context: Context = layout.getContext()
113 val clipLayer: ClipDrawable
Matt Pietal22231792020-01-23 09:51:09 -0500114 lateinit var cws: ControlWithState
Matt Pietalb582b692020-02-14 19:37:57 -0500115 var behavior: Behavior? = null
Matt Pietalb7da66c2020-03-06 10:49:31 -0500116 var lastAction: ControlAction? = null
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400117 var isLoading = false
Matt Pietal370db872020-06-04 11:13:29 -0400118 var visibleDialog: Dialog? = null
Matt Pietal94316e92020-04-22 10:58:32 -0400119 private var lastChallengeDialog: Dialog? = null
Matt Pietale4dda8b2020-05-08 09:03:57 -0400120 private val onDialogCancel: () -> Unit = { lastChallengeDialog = null }
Matt Pietal94316e92020-04-22 10:58:32 -0400121
Matt Pietaldc78c842020-03-30 08:09:18 -0400122 val deviceType: Int
Matt Pietal96f746b2020-06-08 14:40:00 -0400123 get() = cws.control?.let { it.deviceType } ?: cws.ci.deviceType
124 val controlStatus: Int
125 get() = cws.control?.let { it.status } ?: Control.STATUS_UNKNOWN
126 val controlTemplate: ControlTemplate
127 get() = cws.control?.let { it.controlTemplate } ?: ControlTemplate.NO_TEMPLATE
Matt Pietal22231792020-01-23 09:51:09 -0500128
Matt Pietal3b859c32020-06-18 15:27:13 -0400129 var userInteractionInProgress = false
130
Matt Pietal22231792020-01-23 09:51:09 -0500131 init {
132 val ld = layout.getBackground() as LayerDrawable
133 ld.mutate()
134 clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) as ClipDrawable
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700135 clipLayer.alpha = ALPHA_DISABLED
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700136 baseLayer = ld.findDrawableByLayerId(R.id.background) as GradientDrawable
Matt Pietal5f478c72020-04-01 15:53:54 -0400137 // needed for marquee to start
138 status.setSelected(true)
Matt Pietal22231792020-01-23 09:51:09 -0500139 }
140
141 fun bindData(cws: ControlWithState) {
Matt Pietal3b859c32020-06-18 15:27:13 -0400142 // If an interaction is in progress, the update may visually interfere with the action the
143 // action the user wants to make. Don't apply the update, and instead assume a new update
144 // will coming from when the user interaction is complete.
145 if (userInteractionInProgress) return
146
Matt Pietal22231792020-01-23 09:51:09 -0500147 this.cws = cws
148
Matt Pietal96f746b2020-06-08 14:40:00 -0400149 // For the following statuses only, assume the title/subtitle could not be set properly
150 // by the app and instead use the last known information from favorites
151 if (controlStatus == Control.STATUS_UNKNOWN || controlStatus == Control.STATUS_NOT_FOUND) {
Matt Pietal22231792020-01-23 09:51:09 -0500152 title.setText(cws.ci.controlTitle)
Matt Pietal85878262020-03-18 15:34:46 -0400153 subtitle.setText(cws.ci.controlSubtitle)
Matt Pietal96f746b2020-06-08 14:40:00 -0400154 } else {
155 cws.control?.let {
156 title.setText(it.title)
157 subtitle.setText(it.subtitle)
158 }
Matt Pietal22231792020-01-23 09:51:09 -0500159 }
160
Matt Pietal307b1ef2020-02-10 07:27:01 -0500161 cws.control?.let {
Matt Pietal31ec9f22020-03-12 09:02:17 -0400162 layout.setClickable(true)
Matt Pietala635bd12020-02-03 13:36:18 -0500163 layout.setOnLongClickListener(View.OnLongClickListener() {
Matt Pietal677981d2020-04-24 14:38:32 -0400164 controlActionCoordinator.longPress(this@ControlViewHolder)
Matt Pietala635bd12020-02-03 13:36:18 -0500165 true
166 })
Matt Pietal603f8e72020-06-05 11:02:40 -0400167
168 controlActionCoordinator.runPendingAction(cws.ci.controlId)
Matt Pietala635bd12020-02-03 13:36:18 -0500169 }
170
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400171 isLoading = false
Matt Pietal96f746b2020-06-08 14:40:00 -0400172 behavior = bindBehavior(behavior,
173 findBehaviorClass(controlStatus, controlTemplate, deviceType))
Matt Pietalc8796852020-05-13 15:13:30 -0400174 updateContentDescription()
Matt Pietal22231792020-01-23 09:51:09 -0500175 }
176
Matt Pietal1aac43b2020-02-04 15:43:31 -0500177 fun actionResponse(@ControlAction.ResponseResult response: Int) {
Matt Pietal603f8e72020-06-05 11:02:40 -0400178 controlActionCoordinator.enableActionOnTouch(cws.ci.controlId)
179
Matt Pietal94316e92020-04-22 10:58:32 -0400180 // OK responses signal normal behavior, and the app will provide control updates
181 val failedAttempt = lastChallengeDialog != null
182 when (response) {
183 ControlAction.RESPONSE_OK ->
184 lastChallengeDialog = null
185 ControlAction.RESPONSE_UNKNOWN -> {
186 lastChallengeDialog = null
Matt Pietal2f63cc02020-06-19 15:21:13 -0400187 setErrorStatus()
Matt Pietal94316e92020-04-22 10:58:32 -0400188 }
189 ControlAction.RESPONSE_FAIL -> {
190 lastChallengeDialog = null
Matt Pietal2f63cc02020-06-19 15:21:13 -0400191 setErrorStatus()
Matt Pietal94316e92020-04-22 10:58:32 -0400192 }
193 ControlAction.RESPONSE_CHALLENGE_PIN -> {
Matt Pietale4dda8b2020-05-08 09:03:57 -0400194 lastChallengeDialog = ChallengeDialogs.createPinDialog(
Matt Pietal9f403a12020-06-18 08:48:24 -0400195 this, false /* useAlphanumeric */, failedAttempt, onDialogCancel)
Matt Pietal94316e92020-04-22 10:58:32 -0400196 lastChallengeDialog?.show()
197 }
198 ControlAction.RESPONSE_CHALLENGE_PASSPHRASE -> {
Matt Pietale4dda8b2020-05-08 09:03:57 -0400199 lastChallengeDialog = ChallengeDialogs.createPinDialog(
Matt Pietal9f403a12020-06-18 08:48:24 -0400200 this, true /* useAlphanumeric */, failedAttempt, onDialogCancel)
Matt Pietal94316e92020-04-22 10:58:32 -0400201 lastChallengeDialog?.show()
202 }
203 ControlAction.RESPONSE_CHALLENGE_ACK -> {
Matt Pietale4dda8b2020-05-08 09:03:57 -0400204 lastChallengeDialog = ChallengeDialogs.createConfirmationDialog(
205 this, onDialogCancel)
Matt Pietal94316e92020-04-22 10:58:32 -0400206 lastChallengeDialog?.show()
207 }
208 }
209 }
210
211 fun dismiss() {
212 lastChallengeDialog?.dismiss()
213 lastChallengeDialog = null
Matt Pietal370db872020-06-04 11:13:29 -0400214 visibleDialog?.dismiss()
215 visibleDialog = null
Matt Pietal1aac43b2020-02-04 15:43:31 -0500216 }
217
Matt Pietal2f63cc02020-06-19 15:21:13 -0400218 fun setErrorStatus() {
219 val text = context.resources.getString(R.string.controls_error_failed)
Matt Pietal370db872020-06-04 11:13:29 -0400220 animateStatusChange(/* animated */ true, {
Matt Pietal2f63cc02020-06-19 15:21:13 -0400221 setStatusText(text, /* immediately */ true)
Matt Pietal370db872020-06-04 11:13:29 -0400222 })
Matt Pietal307b1ef2020-02-10 07:27:01 -0500223 }
224
Matt Pietalc8796852020-05-13 15:13:30 -0400225 private fun updateContentDescription() =
226 layout.setContentDescription("${title.text} ${subtitle.text} ${status.text}")
227
Matt Pietal22231792020-01-23 09:51:09 -0500228 fun action(action: ControlAction) {
Matt Pietalb7da66c2020-03-06 10:49:31 -0500229 lastAction = action
Matt Pietal313f37d2020-02-24 11:27:22 -0500230 controlsController.action(cws.componentName, cws.ci, action)
Matt Pietal22231792020-01-23 09:51:09 -0500231 }
232
Matt Pietal733d6372020-06-12 09:36:52 -0400233 fun usePanel(): Boolean {
234 return deviceType in ControlViewHolder.FORCE_PANEL_DEVICES ||
235 controlTemplate == ControlTemplate.NO_TEMPLATE
236 }
Matt Pietaldc78c842020-03-30 08:09:18 -0400237
Matt Pietalcf516132020-05-07 16:50:04 -0400238 fun bindBehavior(
239 existingBehavior: Behavior?,
240 clazz: KClass<out Behavior>,
241 offset: Int = 0
242 ): Behavior {
243 val behavior = if (existingBehavior == null || existingBehavior!!::class != clazz) {
244 // Behavior changes can signal a change in template from the app or
245 // first time setup
246 val newBehavior = clazz.java.newInstance()
247 newBehavior.initialize(this)
248
249 // let behaviors define their own, if necessary, and clear any existing ones
250 layout.setAccessibilityDelegate(null)
251 newBehavior
252 } else {
253 existingBehavior
254 }
255
256 return behavior.also {
257 it.bind(cws, offset)
Matt Pietal22231792020-01-23 09:51:09 -0500258 }
259 }
260
Matt Pietalcf516132020-05-07 16:50:04 -0400261 internal fun applyRenderInfo(enabled: Boolean, offset: Int, animated: Boolean = true) {
Matt Pietal96f746b2020-06-08 14:40:00 -0400262 val deviceTypeOrError = if (controlStatus == Control.STATUS_OK ||
263 controlStatus == Control.STATUS_UNKNOWN) {
264 deviceType
265 } else {
266 RenderInfo.ERROR_ICON
267 }
268 val ri = RenderInfo.lookup(context, cws.componentName, deviceTypeOrError, offset)
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700269 val fg = context.resources.getColorStateList(ri.foreground, context.theme)
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400270 val newText = nextStatusText
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400271 val control = cws.control
272
273 var shouldAnimate = animated
274 if (newText == status.text) {
275 shouldAnimate = false
276 }
277 animateStatusChange(shouldAnimate) {
278 updateStatusRow(enabled, newText, ri.icon, fg, control)
279 }
280
281 animateBackgroundChange(shouldAnimate, enabled, ri.enabledBackground)
282 }
283
284 fun getStatusText() = status.text
285
286 fun setStatusTextSize(textSize: Float) =
287 status.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
288
289 fun setStatusText(text: CharSequence, immediately: Boolean = false) {
290 if (immediately) {
291 status.alpha = STATUS_ALPHA_ENABLED
292 status.text = text
Matt Pietal395723d2020-06-12 13:56:48 -0400293 updateContentDescription()
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400294 }
Matt Pietal5a5de402020-06-15 13:29:57 -0400295 nextStatusText = text
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400296 }
297
298 private fun animateBackgroundChange(
299 animated: Boolean,
300 enabled: Boolean,
301 @ColorRes bgColor: Int
302 ) {
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700303 val bg = context.resources.getColor(R.color.control_default_background, context.theme)
Matt Pietal4f5cd672020-04-22 13:51:11 -0400304 var (newClipColor, newAlpha) = if (enabled) {
305 // allow color overrides for the enabled state only
306 val color = cws.control?.getCustomColor()?.let {
307 val state = intArrayOf(android.R.attr.state_enabled)
308 it.getColorForState(state, it.getDefaultColor())
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400309 } ?: context.resources.getColor(bgColor, context.theme)
Matt Pietal4f5cd672020-04-22 13:51:11 -0400310 listOf(color, ALPHA_ENABLED)
Matt Pietala103a942020-03-13 12:10:35 -0400311 } else {
Matt Pietal4f5cd672020-04-22 13:51:11 -0400312 listOf(
313 context.resources.getColor(R.color.control_default_background, context.theme),
314 ALPHA_DISABLED
315 )
Matt Pietala103a942020-03-13 12:10:35 -0400316 }
317
Matt Pietala103a942020-03-13 12:10:35 -0400318 (clipLayer.getDrawable() as GradientDrawable).apply {
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700319 val newBaseColor = if (behavior is ToggleRangeBehavior) {
320 ColorUtils.blendARGB(bg, newClipColor, toggleBackgroundIntensity)
321 } else {
322 bg
323 }
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700324 stateAnimator?.cancel()
325 if (animated) {
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700326 val oldColor = color?.defaultColor ?: newClipColor
327 val oldBaseColor = baseLayer.color?.defaultColor ?: newBaseColor
Lucas Dupind60b3322020-04-15 18:06:47 -0700328 val oldAlpha = layout.alpha
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700329 stateAnimator = ValueAnimator.ofInt(clipLayer.alpha, newAlpha).apply {
330 addUpdateListener {
331 alpha = it.animatedValue as Int
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700332 setColor(ColorUtils.blendARGB(oldColor, newClipColor, it.animatedFraction))
333 baseLayer.setColor(ColorUtils.blendARGB(oldBaseColor,
334 newBaseColor, it.animatedFraction))
Matt Pietale27ca7c2020-05-01 08:29:10 -0400335 layout.alpha = MathUtils.lerp(oldAlpha, 1f, it.animatedFraction)
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700336 }
337 addListener(object : AnimatorListenerAdapter() {
338 override fun onAnimationEnd(animation: Animator?) {
339 stateAnimator = null
340 }
341 })
342 duration = STATE_ANIMATION_DURATION
343 interpolator = Interpolators.CONTROL_STATE
344 start()
345 }
346 } else {
347 alpha = newAlpha
Lucas Dupin92f6cca2020-04-14 22:30:54 -0700348 setColor(newClipColor)
349 baseLayer.setColor(newBaseColor)
Matt Pietale27ca7c2020-05-01 08:29:10 -0400350 layout.alpha = 1f
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700351 }
Matt Pietalb582b692020-02-14 19:37:57 -0500352 }
Matt Pietal22231792020-01-23 09:51:09 -0500353 }
Matt Pietalb582b692020-02-14 19:37:57 -0500354
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400355 private fun animateStatusChange(animated: Boolean, statusRowUpdater: () -> Unit) {
356 statusAnimator?.cancel()
357
358 if (!animated) {
359 statusRowUpdater.invoke()
360 return
361 }
362
363 if (isLoading) {
364 statusRowUpdater.invoke()
365 statusAnimator = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_DIMMED).apply {
366 repeatMode = ValueAnimator.REVERSE
367 repeatCount = ValueAnimator.INFINITE
368 duration = 500L
369 interpolator = Interpolators.LINEAR
370 startDelay = 900L
371 start()
372 }
373 } else {
374 val fadeOut = ObjectAnimator.ofFloat(status, "alpha", 0f).apply {
375 duration = 200L
376 interpolator = Interpolators.LINEAR
377 addListener(object : AnimatorListenerAdapter() {
378 override fun onAnimationEnd(animation: Animator?) {
379 statusRowUpdater.invoke()
380 }
381 })
382 }
383 val fadeIn = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_ENABLED).apply {
384 duration = 200L
385 interpolator = Interpolators.LINEAR
386 }
387 statusAnimator = AnimatorSet().apply {
388 playSequentially(fadeOut, fadeIn)
389 addListener(object : AnimatorListenerAdapter() {
390 override fun onAnimationEnd(animation: Animator?) {
391 status.alpha = STATUS_ALPHA_ENABLED
392 statusAnimator = null
393 }
394 })
395 start()
396 }
397 }
398 }
399
400 private fun updateStatusRow(
401 enabled: Boolean,
402 text: CharSequence,
403 drawable: Drawable,
404 color: ColorStateList,
405 control: Control?
406 ) {
407 setEnabled(enabled)
408
409 status.text = text
Matt Pietal395723d2020-06-12 13:56:48 -0400410 updateContentDescription()
411
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400412 status.setTextColor(color)
413
414 control?.getCustomIcon()?.let {
415 // do not tint custom icons, assume the intended icon color is correct
416 icon.imageTintList = null
417 icon.setImageIcon(it)
418 } ?: run {
Matt Pietal798d1132020-06-01 09:47:42 -0400419 if (drawable is StateListDrawable) {
420 // Only reset the drawable if it is a different resource, as it will interfere
421 // with the image state and animation.
422 if (icon.drawable == null || !(icon.drawable is StateListDrawable)) {
423 icon.setImageDrawable(drawable)
424 }
425 val state = if (enabled) ATTR_ENABLED else ATTR_DISABLED
426 icon.setImageState(state, true)
427 } else {
428 icon.setImageDrawable(drawable)
429 }
Matt Pietaldc6bb1d2020-05-14 13:33:41 -0400430
431 // do not color app icons
432 if (deviceType != DeviceTypes.TYPE_ROUTINE) {
433 icon.imageTintList = color
434 }
435 }
436 }
437
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500438 private fun setEnabled(enabled: Boolean) {
Matt Pietal22231792020-01-23 09:51:09 -0500439 status.setEnabled(enabled)
440 icon.setEnabled(enabled)
441 }
442}