1 /*
2  * Copyright (C) 2021 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.qs.tileimpl
18 
19 import android.animation.ArgbEvaluator
20 import android.animation.PropertyValuesHolder
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.content.res.ColorStateList
24 import android.content.res.Configuration
25 import android.content.res.Resources.ID_NULL
26 import android.graphics.drawable.Drawable
27 import android.graphics.drawable.RippleDrawable
28 import android.os.Trace
29 import android.service.quicksettings.Tile
30 import android.text.TextUtils
31 import android.util.Log
32 import android.util.TypedValue
33 import android.view.Gravity
34 import android.view.LayoutInflater
35 import android.view.View
36 import android.view.ViewGroup
37 import android.view.accessibility.AccessibilityEvent
38 import android.view.accessibility.AccessibilityNodeInfo
39 import android.widget.Button
40 import android.widget.ImageView
41 import android.widget.LinearLayout
42 import android.widget.Switch
43 import android.widget.TextView
44 import androidx.annotation.VisibleForTesting
45 import com.android.settingslib.Utils
46 import com.android.systemui.FontSizeUtils
47 import com.android.systemui.R
48 import com.android.systemui.animation.LaunchableView
49 import com.android.systemui.animation.LaunchableViewDelegate
50 import com.android.systemui.plugins.qs.QSIconView
51 import com.android.systemui.plugins.qs.QSTile
52 import com.android.systemui.plugins.qs.QSTile.BooleanState
53 import com.android.systemui.plugins.qs.QSTileView
54 import com.android.systemui.qs.logging.QSLogger
55 import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
56 import java.util.Objects
57 
58 private const val TAG = "QSTileViewImpl"
59 open class QSTileViewImpl @JvmOverloads constructor(
60     context: Context,
61     private val _icon: QSIconView,
62     private val collapsed: Boolean = false
63 ) : QSTileView(context), HeightOverrideable, LaunchableView {
64 
65     companion object {
66         private const val INVALID = -1
67         private const val BACKGROUND_NAME = "background"
68         private const val LABEL_NAME = "label"
69         private const val SECONDARY_LABEL_NAME = "secondaryLabel"
70         private const val CHEVRON_NAME = "chevron"
71         const val UNAVAILABLE_ALPHA = 0.3f
72         @VisibleForTesting
73         internal const val TILE_STATE_RES_PREFIX = "tile_states_"
74     }
75 
76     private var _position: Int = INVALID
77 
78     override fun setPosition(position: Int) {
79         _position = position
80     }
81 
82     override var heightOverride: Int = HeightOverrideable.NO_OVERRIDE
83         set(value) {
84             if (field == value) return
85             field = value
86             updateHeight()
87         }
88 
89     override var squishinessFraction: Float = 1f
90         set(value) {
91             if (field == value) return
92             field = value
93             updateHeight()
94         }
95 
96     private val colorActive = Utils.getColorAttrDefaultColor(context, R.attr.shadeActive)
97     private val colorInactive = Utils.getColorAttrDefaultColor(context, R.attr.shadeInactive)
98     private val colorUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.shadeDisabled)
99 
100     private val colorLabelActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
101     private val colorLabelInactive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive)
102     private val colorLabelUnavailable =
103         Utils.getColorAttrDefaultColor(context, R.attr.outline)
104 
105     private val colorSecondaryLabelActive =
106         Utils.getColorAttrDefaultColor(context, R.attr.onShadeActiveVariant)
107     private val colorSecondaryLabelInactive =
108             Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant)
109     private val colorSecondaryLabelUnavailable =
110         Utils.getColorAttrDefaultColor(context, R.attr.outline)
111 
112     private lateinit var label: TextView
113     protected lateinit var secondaryLabel: TextView
114     private lateinit var labelContainer: IgnorableChildLinearLayout
115     protected lateinit var sideView: ViewGroup
116     private lateinit var customDrawableView: ImageView
117     private lateinit var chevronView: ImageView
118     private var mQsLogger: QSLogger? = null
119 
120     /**
121      * Controls if tile background is set to a [RippleDrawable] see [setClickable]
122      */
123     protected var showRippleEffect = true
124 
125     private lateinit var ripple: RippleDrawable
126     private lateinit var colorBackgroundDrawable: Drawable
127     private var paintColor: Int = 0
128     private val singleAnimator: ValueAnimator = ValueAnimator().apply {
129         setDuration(QS_ANIM_LENGTH)
130         addUpdateListener { animation ->
131             setAllColors(
132                 // These casts will throw an exception if some property is missing. We should
133                 // always have all properties.
134                 animation.getAnimatedValue(BACKGROUND_NAME) as Int,
135                 animation.getAnimatedValue(LABEL_NAME) as Int,
136                 animation.getAnimatedValue(SECONDARY_LABEL_NAME) as Int,
137                 animation.getAnimatedValue(CHEVRON_NAME) as Int
138             )
139         }
140     }
141 
142     private var accessibilityClass: String? = null
143     private var stateDescriptionDeltas: CharSequence? = null
144     private var lastStateDescription: CharSequence? = null
145     private var tileState = false
146     private var lastState = INVALID
147     private val launchableViewDelegate = LaunchableViewDelegate(
148         this,
149         superSetVisibility = { super.setVisibility(it) },
150     )
151     private var lastDisabledByPolicy = false
152 
153     private val locInScreen = IntArray(2)
154 
155     init {
156         val typedValue = TypedValue()
157         if (!getContext().theme.resolveAttribute(R.attr.isQsTheme, typedValue, true)) {
158             throw IllegalStateException("QSViewImpl must be inflated with a theme that contains " +
159                     "Theme.SystemUI.QuickSettings")
160         }
161         setId(generateViewId())
162         orientation = LinearLayout.HORIZONTAL
163         gravity = Gravity.CENTER_VERTICAL or Gravity.START
164         importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
165         clipChildren = false
166         clipToPadding = false
167         isFocusable = true
168         background = createTileBackground()
169         setColor(getBackgroundColorForState(QSTile.State.DEFAULT_STATE))
170 
171         val padding = resources.getDimensionPixelSize(R.dimen.qs_tile_padding)
172         val startPadding = resources.getDimensionPixelSize(R.dimen.qs_tile_start_padding)
173         setPaddingRelative(startPadding, padding, padding, padding)
174 
175         val iconSize = resources.getDimensionPixelSize(R.dimen.qs_icon_size)
176         addView(_icon, LayoutParams(iconSize, iconSize))
177 
178         createAndAddLabels()
179         createAndAddSideView()
180     }
181 
182     override fun onConfigurationChanged(newConfig: Configuration?) {
183         super.onConfigurationChanged(newConfig)
184         updateResources()
185     }
186 
187     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
188         Trace.traceBegin(Trace.TRACE_TAG_APP, "QSTileViewImpl#onMeasure")
189         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
190         Trace.endSection()
191     }
192 
193     override fun resetOverride() {
194         heightOverride = HeightOverrideable.NO_OVERRIDE
195         updateHeight()
196     }
197 
198     fun setQsLogger(qsLogger: QSLogger) {
199         mQsLogger = qsLogger
200     }
201 
202     fun updateResources() {
203         FontSizeUtils.updateFontSize(label, R.dimen.qs_tile_text_size)
204         FontSizeUtils.updateFontSize(secondaryLabel, R.dimen.qs_tile_text_size)
205 
206         val iconSize = context.resources.getDimensionPixelSize(R.dimen.qs_icon_size)
207         _icon.layoutParams.apply {
208             height = iconSize
209             width = iconSize
210         }
211 
212         val padding = resources.getDimensionPixelSize(R.dimen.qs_tile_padding)
213         val startPadding = resources.getDimensionPixelSize(R.dimen.qs_tile_start_padding)
214         setPaddingRelative(startPadding, padding, padding, padding)
215 
216         val labelMargin = resources.getDimensionPixelSize(R.dimen.qs_label_container_margin)
217         (labelContainer.layoutParams as MarginLayoutParams).apply {
218             marginStart = labelMargin
219         }
220 
221         (sideView.layoutParams as MarginLayoutParams).apply {
222             marginStart = labelMargin
223         }
224         (chevronView.layoutParams as MarginLayoutParams).apply {
225             height = iconSize
226             width = iconSize
227         }
228 
229         val endMargin = resources.getDimensionPixelSize(R.dimen.qs_drawable_end_margin)
230         (customDrawableView.layoutParams as MarginLayoutParams).apply {
231             height = iconSize
232             marginEnd = endMargin
233         }
234     }
235 
236     private fun createAndAddLabels() {
237         labelContainer = LayoutInflater.from(context)
238                 .inflate(R.layout.qs_tile_label, this, false) as IgnorableChildLinearLayout
239         label = labelContainer.requireViewById(R.id.tile_label)
240         secondaryLabel = labelContainer.requireViewById(R.id.app_label)
241         if (collapsed) {
242             labelContainer.ignoreLastView = true
243             // Ideally, it'd be great if the parent could set this up when measuring just this child
244             // instead of the View class having to support this. However, due to the mysteries of
245             // LinearLayout's double measure pass, we cannot overwrite `measureChild` or any of its
246             // sibling methods to have special behavior for labelContainer.
247             labelContainer.forceUnspecifiedMeasure = true
248             secondaryLabel.alpha = 0f
249         }
250         setLabelColor(getLabelColorForState(QSTile.State.DEFAULT_STATE))
251         setSecondaryLabelColor(getSecondaryLabelColorForState(QSTile.State.DEFAULT_STATE))
252         addView(labelContainer)
253     }
254 
255     private fun createAndAddSideView() {
256         sideView = LayoutInflater.from(context)
257                 .inflate(R.layout.qs_tile_side_icon, this, false) as ViewGroup
258         customDrawableView = sideView.requireViewById(R.id.customDrawable)
259         chevronView = sideView.requireViewById(R.id.chevron)
260         setChevronColor(getChevronColorForState(QSTile.State.DEFAULT_STATE))
261         addView(sideView)
262     }
263 
264     fun createTileBackground(): Drawable {
265         ripple = mContext.getDrawable(R.drawable.qs_tile_background) as RippleDrawable
266         colorBackgroundDrawable = ripple.findDrawableByLayerId(R.id.background)
267         return ripple
268     }
269 
270     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
271         super.onLayout(changed, l, t, r, b)
272         updateHeight()
273     }
274 
275     private fun updateHeight() {
276         val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) {
277             heightOverride
278         } else {
279             measuredHeight
280         }
281         // Limit how much we affect the height, so we don't have rounding artifacts when the tile
282         // is too short.
283         val constrainedSquishiness = constrainSquishiness(squishinessFraction)
284         bottom = top + (actualHeight * constrainedSquishiness).toInt()
285         scrollY = (actualHeight - height) / 2
286     }
287 
288     override fun updateAccessibilityOrder(previousView: View?): View {
289         accessibilityTraversalAfter = previousView?.id ?: ID_NULL
290         return this
291     }
292 
293     override fun getIcon(): QSIconView {
294         return _icon
295     }
296 
297     override fun getIconWithBackground(): View {
298         return icon
299     }
300 
301     override fun init(tile: QSTile) {
302         init(
303                 { v: View? -> tile.click(this) },
304                 { view: View? ->
305                     tile.longClick(this)
306                     true
307                 }
308         )
309     }
310 
311     private fun init(
312         click: OnClickListener?,
313         longClick: OnLongClickListener?
314     ) {
315         setOnClickListener(click)
316         onLongClickListener = longClick
317     }
318 
319     override fun onStateChanged(state: QSTile.State) {
320         // We cannot use the handler here because sometimes, the views are not attached (if they
321         // are in a page that the ViewPager hasn't attached). Instead, we use a runnable where
322         // all its instances are `equal` to each other, so they can be used to remove them from the
323         // queue.
324         // This means that at any given time there's at most one enqueued runnable to change state.
325         // However, as we only ever care about the last state posted, this is fine.
326         val runnable = StateChangeRunnable(state.copy())
327         removeCallbacks(runnable)
328         post(runnable)
329     }
330 
331     override fun getDetailY(): Int {
332         return top + height / 2
333     }
334 
335     override fun hasOverlappingRendering(): Boolean {
336         // Avoid layers for this layout - we don't need them.
337         return false
338     }
339 
340     override fun setClickable(clickable: Boolean) {
341         super.setClickable(clickable)
342         background = if (clickable && showRippleEffect) {
343             ripple.also {
344                 // In case that the colorBackgroundDrawable was used as the background, make sure
345                 // it has the correct callback instead of null
346                 colorBackgroundDrawable.callback = it
347             }
348         } else {
349             colorBackgroundDrawable
350         }
351     }
352 
353     override fun getLabelContainer(): View {
354         return labelContainer
355     }
356 
357     override fun getLabel(): View {
358         return label
359     }
360 
361     override fun getSecondaryLabel(): View {
362         return secondaryLabel
363     }
364 
365     override fun getSecondaryIcon(): View {
366         return sideView
367     }
368 
369     override fun setShouldBlockVisibilityChanges(block: Boolean) {
370         launchableViewDelegate.setShouldBlockVisibilityChanges(block)
371     }
372 
373     override fun setVisibility(visibility: Int) {
374         launchableViewDelegate.setVisibility(visibility)
375     }
376 
377     // Accessibility
378 
379     override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
380         super.onInitializeAccessibilityEvent(event)
381         if (!TextUtils.isEmpty(accessibilityClass)) {
382             event.className = accessibilityClass
383         }
384         if (event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION &&
385                 stateDescriptionDeltas != null) {
386             event.text.add(stateDescriptionDeltas)
387             stateDescriptionDeltas = null
388         }
389     }
390 
391     override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
392         super.onInitializeAccessibilityNodeInfo(info)
393         // Clear selected state so it is not announce by talkback.
394         info.isSelected = false
395         info.text = if (TextUtils.isEmpty(secondaryLabel.text)) {
396             "${label.text}"
397         } else {
398             "${label.text}, ${secondaryLabel.text}"
399         }
400         if (lastDisabledByPolicy) {
401             info.addAction(
402                     AccessibilityNodeInfo.AccessibilityAction(
403                             AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
404                             resources.getString(
405                                 R.string.accessibility_tile_disabled_by_policy_action_description
406                             )
407                     )
408             )
409         }
410         if (!TextUtils.isEmpty(accessibilityClass)) {
411             info.className = if (lastDisabledByPolicy) {
412                 Button::class.java.name
413             } else {
414                 accessibilityClass
415             }
416             if (Switch::class.java.name == accessibilityClass) {
417                 info.isChecked = tileState
418                 info.isCheckable = true
419                 if (isLongClickable) {
420                     info.addAction(
421                             AccessibilityNodeInfo.AccessibilityAction(
422                                     AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
423                                     resources.getString(
424                                             R.string.accessibility_long_click_tile)))
425                 }
426             }
427         }
428         if (_position != INVALID) {
429             info.collectionItemInfo =
430                 AccessibilityNodeInfo.CollectionItemInfo(_position, 1, 0, 1, false)
431         }
432     }
433 
434     override fun toString(): String {
435         val sb = StringBuilder(javaClass.simpleName).append('[')
436         sb.append("locInScreen=(${locInScreen[0]}, ${locInScreen[1]})")
437         sb.append(", iconView=$_icon")
438         sb.append(", tileState=$tileState")
439         sb.append("]")
440         return sb.toString()
441     }
442 
443     // HANDLE STATE CHANGES RELATED METHODS
444 
445     protected open fun handleStateChanged(state: QSTile.State) {
446         val allowAnimations = animationsEnabled()
447         isClickable = state.state != Tile.STATE_UNAVAILABLE
448         isLongClickable = state.handlesLongClick
449         icon.setIcon(state, allowAnimations)
450         contentDescription = state.contentDescription
451 
452         // State handling and description
453         val stateDescription = StringBuilder()
454         val arrayResId = SubtitleArrayMapping.getSubtitleId(state.spec)
455         val stateText = state.getStateText(arrayResId, resources)
456         state.secondaryLabel = state.getSecondaryLabel(stateText)
457         if (!TextUtils.isEmpty(stateText)) {
458             stateDescription.append(stateText)
459         }
460         if (state.disabledByPolicy && state.state != Tile.STATE_UNAVAILABLE) {
461             stateDescription.append(", ")
462             stateDescription.append(getUnavailableText(state.spec))
463         }
464         if (!TextUtils.isEmpty(state.stateDescription)) {
465             stateDescription.append(", ")
466             stateDescription.append(state.stateDescription)
467             if (lastState != INVALID && state.state == lastState &&
468                     state.stateDescription != lastStateDescription) {
469                 stateDescriptionDeltas = state.stateDescription
470             }
471         }
472 
473         setStateDescription(stateDescription.toString())
474         lastStateDescription = state.stateDescription
475 
476         accessibilityClass = if (state.state == Tile.STATE_UNAVAILABLE) {
477             null
478         } else {
479             state.expandedAccessibilityClassName
480         }
481 
482         if (state is BooleanState) {
483             val newState = state.value
484             if (tileState != newState) {
485                 tileState = newState
486             }
487         }
488         //
489 
490         // Labels
491         if (!Objects.equals(label.text, state.label)) {
492             label.text = state.label
493         }
494         if (!Objects.equals(secondaryLabel.text, state.secondaryLabel)) {
495             secondaryLabel.text = state.secondaryLabel
496             secondaryLabel.visibility = if (TextUtils.isEmpty(state.secondaryLabel)) {
497                 GONE
498             } else {
499                 VISIBLE
500             }
501         }
502 
503         // Colors
504         if (state.state != lastState || state.disabledByPolicy != lastDisabledByPolicy) {
505             singleAnimator.cancel()
506             mQsLogger?.logTileBackgroundColorUpdateIfInternetTile(
507                     state.spec,
508                     state.state,
509                     state.disabledByPolicy,
510                     getBackgroundColorForState(state.state, state.disabledByPolicy))
511             if (allowAnimations) {
512                 singleAnimator.setValues(
513                         colorValuesHolder(
514                                 BACKGROUND_NAME,
515                                 paintColor,
516                                 getBackgroundColorForState(state.state, state.disabledByPolicy)
517                         ),
518                         colorValuesHolder(
519                                 LABEL_NAME,
520                                 label.currentTextColor,
521                                 getLabelColorForState(state.state, state.disabledByPolicy)
522                         ),
523                         colorValuesHolder(
524                                 SECONDARY_LABEL_NAME,
525                                 secondaryLabel.currentTextColor,
526                                 getSecondaryLabelColorForState(state.state, state.disabledByPolicy)
527                         ),
528                         colorValuesHolder(
529                                 CHEVRON_NAME,
530                                 chevronView.imageTintList?.defaultColor ?: 0,
531                                 getChevronColorForState(state.state, state.disabledByPolicy)
532                         )
533                     )
534                 singleAnimator.start()
535             } else {
536                 setAllColors(
537                     getBackgroundColorForState(state.state, state.disabledByPolicy),
538                     getLabelColorForState(state.state, state.disabledByPolicy),
539                     getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
540                     getChevronColorForState(state.state, state.disabledByPolicy)
541                 )
542             }
543         }
544 
545         // Right side icon
546         loadSideViewDrawableIfNecessary(state)
547 
548         label.isEnabled = !state.disabledByPolicy
549 
550         lastState = state.state
551         lastDisabledByPolicy = state.disabledByPolicy
552     }
553 
554     private fun setAllColors(
555         backgroundColor: Int,
556         labelColor: Int,
557         secondaryLabelColor: Int,
558         chevronColor: Int
559     ) {
560         setColor(backgroundColor)
561         setLabelColor(labelColor)
562         setSecondaryLabelColor(secondaryLabelColor)
563         setChevronColor(chevronColor)
564     }
565 
566     private fun setColor(color: Int) {
567         colorBackgroundDrawable.mutate().setTint(color)
568         paintColor = color
569     }
570 
571     private fun setLabelColor(color: Int) {
572         label.setTextColor(color)
573     }
574 
575     private fun setSecondaryLabelColor(color: Int) {
576         secondaryLabel.setTextColor(color)
577     }
578 
579     private fun setChevronColor(color: Int) {
580         chevronView.imageTintList = ColorStateList.valueOf(color)
581     }
582 
583     private fun loadSideViewDrawableIfNecessary(state: QSTile.State) {
584         if (state.sideViewCustomDrawable != null) {
585             customDrawableView.setImageDrawable(state.sideViewCustomDrawable)
586             customDrawableView.visibility = VISIBLE
587             chevronView.visibility = GONE
588         } else if (state !is BooleanState || state.forceExpandIcon) {
589             customDrawableView.setImageDrawable(null)
590             customDrawableView.visibility = GONE
591             chevronView.visibility = VISIBLE
592         } else {
593             customDrawableView.setImageDrawable(null)
594             customDrawableView.visibility = GONE
595             chevronView.visibility = GONE
596         }
597     }
598 
599     private fun getUnavailableText(spec: String?): String {
600         val arrayResId = SubtitleArrayMapping.getSubtitleId(spec)
601         return resources.getStringArray(arrayResId)[Tile.STATE_UNAVAILABLE]
602     }
603 
604     /*
605      * The view should not be animated if it's not on screen and no part of it is visible.
606      */
607     protected open fun animationsEnabled(): Boolean {
608         if (!isShown) {
609             return false
610         }
611         if (alpha != 1f) {
612             return false
613         }
614         getLocationOnScreen(locInScreen)
615         return locInScreen.get(1) >= -height
616     }
617 
618     private fun getBackgroundColorForState(state: Int, disabledByPolicy: Boolean = false): Int {
619         return when {
620             state == Tile.STATE_UNAVAILABLE || disabledByPolicy -> colorUnavailable
621             state == Tile.STATE_ACTIVE -> colorActive
622             state == Tile.STATE_INACTIVE -> colorInactive
623             else -> {
624                 Log.e(TAG, "Invalid state $state")
625                 0
626             }
627         }
628     }
629 
630     private fun getLabelColorForState(state: Int, disabledByPolicy: Boolean = false): Int {
631         return when {
632             state == Tile.STATE_UNAVAILABLE || disabledByPolicy -> colorLabelUnavailable
633             state == Tile.STATE_ACTIVE -> colorLabelActive
634             state == Tile.STATE_INACTIVE -> colorLabelInactive
635             else -> {
636                 Log.e(TAG, "Invalid state $state")
637                 0
638             }
639         }
640     }
641 
642     private fun getSecondaryLabelColorForState(state: Int, disabledByPolicy: Boolean = false): Int {
643         return when {
644             state == Tile.STATE_UNAVAILABLE || disabledByPolicy -> colorSecondaryLabelUnavailable
645             state == Tile.STATE_ACTIVE -> colorSecondaryLabelActive
646             state == Tile.STATE_INACTIVE -> colorSecondaryLabelInactive
647             else -> {
648                 Log.e(TAG, "Invalid state $state")
649                 0
650             }
651         }
652     }
653 
654     private fun getChevronColorForState(state: Int, disabledByPolicy: Boolean = false): Int =
655             getSecondaryLabelColorForState(state, disabledByPolicy)
656 
657     @VisibleForTesting
658     internal fun getCurrentColors(): List<Int> = listOf(
659             paintColor,
660             label.currentTextColor,
661             secondaryLabel.currentTextColor,
662             chevronView.imageTintList?.defaultColor ?: 0
663     )
664 
665     inner class StateChangeRunnable(private val state: QSTile.State) : Runnable {
666         override fun run() {
667             handleStateChanged(state)
668         }
669 
670         // We want all instances of this runnable to be equal to each other, so they can be used to
671         // remove previous instances from the Handler/RunQueue of this view
672         override fun equals(other: Any?): Boolean {
673             return other is StateChangeRunnable
674         }
675 
676         // This makes sure that all instances have the same hashcode (because they are `equal`)
677         override fun hashCode(): Int {
678             return StateChangeRunnable::class.hashCode()
679         }
680     }
681 }
682 
683 fun constrainSquishiness(squish: Float): Float {
684     return 0.1f + squish * 0.9f
685 }
686 
687 private fun colorValuesHolder(name: String, vararg values: Int): PropertyValuesHolder {
688     return PropertyValuesHolder.ofInt(name, *values).apply {
689         setEvaluator(ArgbEvaluator.getInstance())
690     }
691 }
692