• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.annotation.SuppressLint
23 import android.content.Context
24 import android.content.res.ColorStateList
25 import android.content.res.Configuration
26 import android.content.res.Resources.ID_NULL
27 import android.graphics.Color
28 import android.graphics.PorterDuff
29 import android.graphics.Rect
30 import android.graphics.drawable.Drawable
31 import android.graphics.drawable.GradientDrawable
32 import android.graphics.drawable.LayerDrawable
33 import android.graphics.drawable.RippleDrawable
34 import android.os.Trace
35 import android.service.quicksettings.Tile
36 import android.text.TextUtils
37 import android.util.Log
38 import android.util.TypedValue
39 import android.view.Gravity
40 import android.view.LayoutInflater
41 import android.view.MotionEvent
42 import android.view.View
43 import android.view.ViewConfiguration
44 import android.view.ViewGroup
45 import android.view.accessibility.AccessibilityEvent
46 import android.view.accessibility.AccessibilityNodeInfo
47 import android.view.animation.AccelerateDecelerateInterpolator
48 import android.widget.Button
49 import android.widget.ImageView
50 import android.widget.LinearLayout
51 import android.widget.Switch
52 import android.widget.TextView
53 import androidx.annotation.VisibleForTesting
54 import androidx.core.animation.doOnCancel
55 import androidx.core.animation.doOnEnd
56 import androidx.core.animation.doOnStart
57 import androidx.core.graphics.drawable.updateBounds
58 import com.android.app.tracing.traceSection
59 import com.android.settingslib.Utils
60 import com.android.systemui.Flags
61 import com.android.systemui.FontSizeUtils
62 import com.android.systemui.animation.Expandable
63 import com.android.systemui.animation.LaunchableView
64 import com.android.systemui.animation.LaunchableViewDelegate
65 import com.android.systemui.haptics.qs.QSLongPressEffect
66 import com.android.systemui.plugins.qs.QSIconView
67 import com.android.systemui.plugins.qs.QSTile
68 import com.android.systemui.plugins.qs.QSTile.AdapterState
69 import com.android.systemui.plugins.qs.QSTileView
70 import com.android.systemui.qs.logging.QSLogger
71 import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
72 import com.android.systemui.res.R
73 import java.util.Objects
74 
75 private const val TAG = "QSTileViewImpl"
76 
77 open class QSTileViewImpl
78 @JvmOverloads
79 constructor(
80     context: Context,
81     private val collapsed: Boolean = false,
82     private val longPressEffect: QSLongPressEffect? = null,
83 ) : QSTileView(context), HeightOverrideable, LaunchableView {
84 
85     companion object {
86         private const val INVALID = -1
87         private const val BACKGROUND_NAME = "background"
88         private const val LABEL_NAME = "label"
89         private const val SECONDARY_LABEL_NAME = "secondaryLabel"
90         private const val CHEVRON_NAME = "chevron"
91         private const val OVERLAY_NAME = "overlay"
92         const val UNAVAILABLE_ALPHA = 0.3f
93         @VisibleForTesting internal const val TILE_STATE_RES_PREFIX = "tile_states_"
94         @VisibleForTesting internal const val LONG_PRESS_EFFECT_WIDTH_SCALE = 1.1f
95         @VisibleForTesting internal const val LONG_PRESS_EFFECT_HEIGHT_SCALE = 1.2f
96         internal val EMPTY_RECT = Rect()
97     }
98 
99     private val icon: QSIconViewImpl = QSIconViewImpl(context)
100     private var position: Int = INVALID
101 
102     override fun setPosition(position: Int) {
103         this.position = position
104     }
105 
106     override var heightOverride: Int = HeightOverrideable.NO_OVERRIDE
107         set(value) {
108             if (field == value) return
109             field = value
110             if (longPressEffect?.state != QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) {
111                 updateHeight()
112             }
113         }
114 
115     override var squishinessFraction: Float = 1f
116         set(value) {
117             if (field == value) return
118             field = value
119             updateHeight()
120         }
121 
122     private val colorActive = Utils.getColorAttrDefaultColor(context, R.attr.shadeActive)
123     private val colorInactive = Utils.getColorAttrDefaultColor(context, R.attr.shadeInactive)
124     private val colorUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.shadeDisabled)
125 
126     private val overlayColorActive =
127         Utils.applyAlpha(
128             /* alpha= */ 0.11f,
129             Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive),
130         )
131     private val overlayColorInactive =
132         Utils.applyAlpha(
133             /* alpha= */ 0.08f,
134             Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive),
135         )
136 
137     private val colorLabelActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
138     private val colorLabelInactive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive)
139     private val colorLabelUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.outline)
140 
141     private val colorSecondaryLabelActive =
142         Utils.getColorAttrDefaultColor(context, R.attr.onShadeActiveVariant)
143     private val colorSecondaryLabelInactive =
144         Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant)
145     private val colorSecondaryLabelUnavailable =
146         Utils.getColorAttrDefaultColor(context, R.attr.outline)
147 
148     private lateinit var label: TextView
149     protected lateinit var secondaryLabel: TextView
150     private lateinit var labelContainer: IgnorableChildLinearLayout
151     protected lateinit var sideView: ViewGroup
152     private lateinit var customDrawableView: ImageView
153     private lateinit var chevronView: ImageView
154     private var mQsLogger: QSLogger? = null
155 
156     /** Controls if tile background is set to a [RippleDrawable] see [setClickable] */
157     protected var showRippleEffect = true
158 
159     private lateinit var qsTileBackground: RippleDrawable
160     private lateinit var qsTileFocusBackground: Drawable
161     private lateinit var backgroundDrawable: LayerDrawable
162     private lateinit var backgroundBaseDrawable: Drawable
163     private lateinit var backgroundOverlayDrawable: Drawable
164 
165     private var backgroundColor: Int = 0
166     private var backgroundOverlayColor: Int = 0
167 
168     private val singleAnimator: ValueAnimator =
169         ValueAnimator().apply {
170             setDuration(QS_ANIM_LENGTH)
171             addUpdateListener { animation ->
172                 setAllColors(
173                     // These casts will throw an exception if some property is missing. We should
174                     // always have all properties.
175                     animation.getAnimatedValue(BACKGROUND_NAME) as Int,
176                     animation.getAnimatedValue(LABEL_NAME) as Int,
177                     animation.getAnimatedValue(SECONDARY_LABEL_NAME) as Int,
178                     animation.getAnimatedValue(CHEVRON_NAME) as Int,
179                     animation.getAnimatedValue(OVERLAY_NAME) as Int,
180                 )
181             }
182         }
183 
184     private var accessibilityClass: String? = null
185     private var stateDescriptionDeltas: CharSequence? = null
186     private var lastStateDescription: CharSequence? = null
187     private var tileState = false
188     private var lastState = INVALID
189     private var lastIconTint = 0
190     private val launchableViewDelegate =
191         LaunchableViewDelegate(this, superSetVisibility = { super.setVisibility(it) })
192     private var lastDisabledByPolicy = false
193 
194     private val locInScreen = IntArray(2)
195 
196     /** Visuo-haptic long-press effects */
197     private var longPressEffectAnimator: ValueAnimator? = null
198     var haveLongPressPropertiesBeenReset = true
199         private set
200 
201     private var paddingForLaunch = Rect()
202     private var initialLongPressProperties: QSLongPressProperties? = null
203     private var finalLongPressProperties: QSLongPressProperties? = null
204     private val colorEvaluator = ArgbEvaluator.getInstance()
205     val isLongPressEffectInitialized: Boolean
206         get() = longPressEffect?.hasInitialized == true
207 
208     val areLongPressEffectPropertiesSet: Boolean
209         get() = initialLongPressProperties != null && finalLongPressProperties != null
210 
211     init {
212         val typedValue = TypedValue()
213         if (!getContext().theme.resolveAttribute(R.attr.isQsTheme, typedValue, true)) {
214             throw IllegalStateException(
215                 "QSViewImpl must be inflated with a theme that contains " +
216                     "Theme.SystemUI.QuickSettings"
217             )
218         }
219         setId(generateViewId())
220         orientation = LinearLayout.HORIZONTAL
221         gravity = Gravity.CENTER_VERTICAL or Gravity.START
222         importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
223         clipChildren = false
224         clipToPadding = false
225         isFocusable = true
226         background = createTileBackground()
227         setColor(getBackgroundColorForState(QSTile.State.DEFAULT_STATE))
228 
229         val padding = resources.getDimensionPixelSize(R.dimen.qs_tile_padding)
230         val startPadding = resources.getDimensionPixelSize(R.dimen.qs_tile_start_padding)
231         setPaddingRelative(startPadding, padding, padding, padding)
232 
233         val iconSize = resources.getDimensionPixelSize(R.dimen.qs_icon_size)
234         addView(icon, LayoutParams(iconSize, iconSize))
235 
236         createAndAddLabels()
237         createAndAddSideView()
238     }
239 
240     override fun onConfigurationChanged(newConfig: Configuration?) {
241         super.onConfigurationChanged(newConfig)
242         updateResources()
243     }
244 
245     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
246         Trace.traceBegin(Trace.TRACE_TAG_APP, "QSTileViewImpl#onMeasure")
247         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
248         Trace.endSection()
249     }
250 
251     override fun resetOverride() {
252         heightOverride = HeightOverrideable.NO_OVERRIDE
253         updateHeight()
254     }
255 
256     fun setQsLogger(qsLogger: QSLogger) {
257         mQsLogger = qsLogger
258     }
259 
260     fun updateResources() {
261         FontSizeUtils.updateFontSize(label, R.dimen.qs_tile_text_size)
262         FontSizeUtils.updateFontSize(secondaryLabel, R.dimen.qs_tile_text_size)
263 
264         val iconSize = context.resources.getDimensionPixelSize(R.dimen.qs_icon_size)
265         icon.layoutParams.apply {
266             height = iconSize
267             width = iconSize
268         }
269 
270         val padding = resources.getDimensionPixelSize(R.dimen.qs_tile_padding)
271         val startPadding = resources.getDimensionPixelSize(R.dimen.qs_tile_start_padding)
272         setPaddingRelative(startPadding, padding, padding, padding)
273 
274         val labelMargin = resources.getDimensionPixelSize(R.dimen.qs_label_container_margin)
275         (labelContainer.layoutParams as MarginLayoutParams).apply { marginStart = labelMargin }
276 
277         (sideView.layoutParams as MarginLayoutParams).apply { marginStart = labelMargin }
278         (chevronView.layoutParams as MarginLayoutParams).apply {
279             height = iconSize
280             width = iconSize
281         }
282 
283         val endMargin = resources.getDimensionPixelSize(R.dimen.qs_drawable_end_margin)
284         (customDrawableView.layoutParams as MarginLayoutParams).apply {
285             height = iconSize
286             marginEnd = endMargin
287         }
288 
289         background = createTileBackground()
290         setColor(backgroundColor)
291         setOverlayColor(backgroundOverlayColor)
292     }
293 
294     private fun createAndAddLabels() {
295         labelContainer =
296             LayoutInflater.from(context).inflate(R.layout.qs_tile_label, this, false)
297                 as IgnorableChildLinearLayout
298         label = labelContainer.requireViewById(R.id.tile_label)
299         secondaryLabel = labelContainer.requireViewById(R.id.app_label)
300         if (collapsed) {
301             labelContainer.ignoreLastView = true
302             // Ideally, it'd be great if the parent could set this up when measuring just this child
303             // instead of the View class having to support this. However, due to the mysteries of
304             // LinearLayout's double measure pass, we cannot overwrite `measureChild` or any of its
305             // sibling methods to have special behavior for labelContainer.
306             labelContainer.forceUnspecifiedMeasure = true
307             secondaryLabel.alpha = 0f
308         }
309         setLabelColor(getLabelColorForState(QSTile.State.DEFAULT_STATE))
310         setSecondaryLabelColor(getSecondaryLabelColorForState(QSTile.State.DEFAULT_STATE))
311         addView(labelContainer)
312     }
313 
314     private fun createAndAddSideView() {
315         sideView =
316             LayoutInflater.from(context).inflate(R.layout.qs_tile_side_icon, this, false)
317                 as ViewGroup
318         customDrawableView = sideView.requireViewById(R.id.customDrawable)
319         chevronView = sideView.requireViewById(R.id.chevron)
320         setChevronColor(getChevronColorForState(QSTile.State.DEFAULT_STATE))
321         addView(sideView)
322     }
323 
324     private fun createTileBackground(): Drawable {
325         qsTileBackground =
326             if (Flags.qsTileFocusState()) {
327                 mContext.getDrawable(R.drawable.qs_tile_background_flagged) as RippleDrawable
328             } else {
329                 mContext.getDrawable(R.drawable.qs_tile_background) as RippleDrawable
330             }
331         qsTileFocusBackground = mContext.getDrawable(R.drawable.qs_tile_focused_background)!!
332         backgroundDrawable =
333             qsTileBackground.findDrawableByLayerId(R.id.background) as LayerDrawable
334         backgroundBaseDrawable =
335             backgroundDrawable.findDrawableByLayerId(R.id.qs_tile_background_base)
336         backgroundOverlayDrawable =
337             backgroundDrawable.findDrawableByLayerId(R.id.qs_tile_background_overlay)
338         backgroundOverlayDrawable.mutate().setTintMode(PorterDuff.Mode.SRC)
339         return qsTileBackground
340     }
341 
342     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
343         super.onLayout(changed, l, t, r, b)
344         updateHeight()
345         maybeUpdateLongPressEffectWidth(measuredWidth.toFloat())
346     }
347 
348     private fun maybeUpdateLongPressEffectWidth(width: Float) {
349         if (!isLongClickable || longPressEffect == null) return
350 
351         initialLongPressProperties?.width = width
352         finalLongPressProperties?.width = LONG_PRESS_EFFECT_WIDTH_SCALE * width
353 
354         val deltaW = (LONG_PRESS_EFFECT_WIDTH_SCALE - 1f) * width
355         paddingForLaunch.left = -deltaW.toInt() / 2
356         paddingForLaunch.right = deltaW.toInt() / 2
357     }
358 
359     private fun maybeUpdateLongPressEffectHeight(height: Float) {
360         if (!isLongClickable || longPressEffect == null) return
361 
362         initialLongPressProperties?.height = height
363         finalLongPressProperties?.height = LONG_PRESS_EFFECT_HEIGHT_SCALE * height
364 
365         val deltaH = (LONG_PRESS_EFFECT_HEIGHT_SCALE - 1f) * height
366         paddingForLaunch.top = -deltaH.toInt() / 2
367         paddingForLaunch.bottom = deltaH.toInt() / 2
368     }
369 
370     override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
371         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
372         if (Flags.qsTileFocusState()) {
373             if (gainFocus) {
374                 qsTileFocusBackground.setBounds(0, 0, width, height)
375                 overlay.add(qsTileFocusBackground)
376             } else {
377                 overlay.clear()
378             }
379         }
380     }
381 
382     private fun updateHeight() {
383         val actualHeight =
384             if (heightOverride != HeightOverrideable.NO_OVERRIDE) {
385                 heightOverride
386             } else {
387                 measuredHeight
388             }
389         // Limit how much we affect the height, so we don't have rounding artifacts when the tile
390         // is too short.
391         val constrainedSquishiness = constrainSquishiness(squishinessFraction)
392         bottom = top + (actualHeight * constrainedSquishiness).toInt()
393         scrollY = (actualHeight - height) / 2
394         maybeUpdateLongPressEffectHeight(actualHeight.toFloat())
395     }
396 
397     override fun updateAccessibilityOrder(previousView: View?): View {
398         accessibilityTraversalAfter = previousView?.id ?: ID_NULL
399         return this
400     }
401 
402     override fun getIcon(): QSIconView {
403         return icon
404     }
405 
406     override fun getIconWithBackground(): View {
407         return icon
408     }
409 
410     override fun init(tile: QSTile) {
411         if (longPressEffect != null) {
412             isHapticFeedbackEnabled = false
413             longPressEffect.qsTile = tile
414             longPressEffect.createExpandableFromView(this)
415             initLongPressEffectCallback()
416             init(
417                 { _: View -> longPressEffect.onTileClick() },
418                 { _: View ->
419                     longPressEffect.onTileLongClick()
420                     true
421                 }, // Haptics and long-clicks are handled by [QSLongPressEffect]
422             )
423         } else {
424             val expandable = Expandable.fromView(this)
425             init(
426                 { _: View? -> tile.click(expandable) },
427                 { _: View? ->
428                     tile.longClick(expandable)
429                     true
430                 },
431             )
432         }
433     }
434 
435     private fun initLongPressEffectCallback() {
436         longPressEffect?.callback =
437             object : QSLongPressEffect.Callback {
438 
439                 override fun onResetProperties() {
440                     resetLongPressEffectProperties()
441                 }
442 
443                 override fun onEffectFinishedReversing() {
444                     // The long-press effect properties finished at the same starting point.
445                     // This is the same as if the properties were reset
446                     haveLongPressPropertiesBeenReset = true
447                 }
448 
449                 override fun onStartAnimator() {
450                     if (longPressEffectAnimator?.isRunning != true) {
451                         longPressEffectAnimator =
452                             ValueAnimator.ofFloat(0f, 1f).apply {
453                                 this.duration = longPressEffect?.effectDuration?.toLong() ?: 0L
454                                 interpolator = AccelerateDecelerateInterpolator()
455 
456                                 doOnStart { longPressEffect?.handleAnimationStart() }
457                                 addUpdateListener {
458                                     val value = animatedValue as Float
459                                     if (value == 0f) {
460                                         bringToFront()
461                                     } else {
462                                         updateLongPressEffectProperties(value)
463                                     }
464                                 }
465                                 doOnEnd { longPressEffect?.handleAnimationComplete() }
466                                 doOnCancel { longPressEffect?.handleAnimationCancel() }
467                                 start()
468                             }
469                     }
470                 }
471 
472                 override fun onReverseAnimator(playHaptics: Boolean) {
473                     longPressEffectAnimator?.let {
474                         val pausedProgress = it.animatedFraction
475                         if (playHaptics) longPressEffect?.playReverseHaptics(pausedProgress)
476                         it.reverse()
477                     }
478                 }
479 
480                 override fun onCancelAnimator() {
481                     resetLongPressEffectProperties()
482                     longPressEffectAnimator?.cancel()
483                 }
484             }
485     }
486 
487     private fun init(click: OnClickListener?, longClick: OnLongClickListener?) {
488         setOnClickListener(click)
489         onLongClickListener = longClick
490     }
491 
492     override fun onStateChanged(state: QSTile.State) {
493         // We cannot use the handler here because sometimes, the views are not attached (if they
494         // are in a page that the ViewPager hasn't attached). Instead, we use a runnable where
495         // all its instances are `equal` to each other, so they can be used to remove them from the
496         // queue.
497         // This means that at any given time there's at most one enqueued runnable to change state.
498         // However, as we only ever care about the last state posted, this is fine.
499         val runnable = StateChangeRunnable(state.copy())
500         removeCallbacks(runnable)
501         post(runnable)
502     }
503 
504     override fun getDetailY(): Int {
505         return top + height / 2
506     }
507 
508     override fun hasOverlappingRendering(): Boolean {
509         // Avoid layers for this layout - we don't need them.
510         return false
511     }
512 
513     override fun setClickable(clickable: Boolean) {
514         super.setClickable(clickable)
515         if (!Flags.qsTileFocusState()) {
516             background =
517                 if (clickable && showRippleEffect) {
518                     qsTileBackground.also {
519                         // In case that the colorBackgroundDrawable was used as the background, make
520                         // sure
521                         // it has the correct callback instead of null
522                         backgroundDrawable.callback = it
523                     }
524                 } else {
525                     backgroundDrawable
526                 }
527         }
528     }
529 
530     override fun getLabelContainer(): View {
531         return labelContainer
532     }
533 
534     override fun getLabel(): View {
535         return label
536     }
537 
538     override fun getSecondaryLabel(): View {
539         return secondaryLabel
540     }
541 
542     override fun getSecondaryIcon(): View {
543         return sideView
544     }
545 
546     override fun setShouldBlockVisibilityChanges(block: Boolean) {
547         launchableViewDelegate.setShouldBlockVisibilityChanges(block)
548     }
549 
550     override fun setVisibility(visibility: Int) {
551         launchableViewDelegate.setVisibility(visibility)
552     }
553 
554     // Accessibility
555 
556     override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
557         super.onInitializeAccessibilityEvent(event)
558         if (!TextUtils.isEmpty(accessibilityClass)) {
559             event.className = accessibilityClass
560         }
561         if (
562             event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION &&
563                 stateDescriptionDeltas != null
564         ) {
565             event.text.add(stateDescriptionDeltas)
566             stateDescriptionDeltas = null
567         }
568     }
569 
570     override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
571         super.onInitializeAccessibilityNodeInfo(info)
572         // Clear selected state so it is not announce by talkback.
573         info.isSelected = false
574         info.text =
575             if (TextUtils.isEmpty(secondaryLabel.text)) {
576                 "${label.text}"
577             } else {
578                 "${label.text}, ${secondaryLabel.text}"
579             }
580         if (lastDisabledByPolicy) {
581             info.addAction(
582                 AccessibilityNodeInfo.AccessibilityAction(
583                     AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
584                     resources.getString(
585                         R.string.accessibility_tile_disabled_by_policy_action_description
586                     ),
587                 )
588             )
589         } else {
590             if (isLongClickable) {
591                 info.addAction(
592                     AccessibilityNodeInfo.AccessibilityAction(
593                         AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
594                         resources.getString(R.string.accessibility_long_click_tile),
595                     )
596                 )
597             }
598         }
599         if (!TextUtils.isEmpty(accessibilityClass)) {
600             info.className =
601                 if (lastDisabledByPolicy) {
602                     Button::class.java.name
603                 } else {
604                     accessibilityClass
605                 }
606             if (Switch::class.java.name == accessibilityClass) {
607                 info.isChecked = tileState
608                 info.isCheckable = true
609             }
610         }
611         if (position != INVALID) {
612             info.collectionItemInfo =
613                 AccessibilityNodeInfo.CollectionItemInfo(position, 1, 0, 1, false)
614         }
615     }
616 
617     override fun toString(): String {
618         val sb = StringBuilder(javaClass.simpleName).append('[')
619         sb.append("locInScreen=(${locInScreen[0]}, ${locInScreen[1]})")
620         sb.append(", iconView=$icon")
621         sb.append(", tileState=$tileState")
622         sb.append("]")
623         return sb.toString()
624     }
625 
626     @SuppressLint("ClickableViewAccessibility")
627     override fun onTouchEvent(event: MotionEvent?): Boolean {
628         // let the View run the onTouch logic for click and long-click detection
629         val result = super.onTouchEvent(event)
630         if (longPressEffect != null) {
631             when (event?.actionMasked) {
632                 MotionEvent.ACTION_DOWN -> {
633                     longPressEffect.handleActionDown()
634                     if (isLongClickable) {
635                         postDelayed(
636                             { longPressEffect.handleTimeoutComplete() },
637                             ViewConfiguration.getTapTimeout().toLong(),
638                         )
639                     }
640                 }
641                 MotionEvent.ACTION_UP -> longPressEffect.handleActionUp()
642                 MotionEvent.ACTION_CANCEL -> longPressEffect.handleActionCancel()
643             }
644         }
645         return result
646     }
647 
648     // HANDLE STATE CHANGES RELATED METHODS
649     protected open fun handleStateChanged(state: QSTile.State) {
650         val allowAnimations = animationsEnabled()
651         isClickable = state.state != Tile.STATE_UNAVAILABLE
652         isLongClickable = state.handlesLongClick
653         icon.setIcon(state, allowAnimations)
654         contentDescription = state.contentDescription
655 
656         // State handling and description
657         val stateDescription = StringBuilder()
658         val arrayResId = SubtitleArrayMapping.getSubtitleId(state.spec)
659         val stateText = state.getStateText(arrayResId, resources)
660         state.secondaryLabel = state.getSecondaryLabel(stateText)
661         if (!TextUtils.isEmpty(stateText)) {
662             stateDescription.append(stateText)
663         }
664         if (state.disabledByPolicy && state.state != Tile.STATE_UNAVAILABLE) {
665             stateDescription.append(", ")
666             stateDescription.append(getUnavailableText(state.spec))
667         }
668         if (!TextUtils.isEmpty(state.stateDescription)) {
669             stateDescription.append(", ")
670             stateDescription.append(state.stateDescription)
671             if (
672                 lastState != INVALID &&
673                     state.state == lastState &&
674                     state.stateDescription != lastStateDescription
675             ) {
676                 stateDescriptionDeltas = state.stateDescription
677             }
678         }
679 
680         setStateDescription(stateDescription.toString())
681         lastStateDescription = state.stateDescription
682 
683         accessibilityClass =
684             if (state.state == Tile.STATE_UNAVAILABLE) {
685                 null
686             } else {
687                 state.expandedAccessibilityClassName
688             }
689 
690         if (state is AdapterState) {
691             val newState = state.value
692             if (tileState != newState) {
693                 tileState = newState
694             }
695         }
696 
697         // Labels
698         if (!Objects.equals(label.text, state.label)) {
699             label.text = state.label
700         }
701         if (!Objects.equals(secondaryLabel.text, state.secondaryLabel)) {
702             secondaryLabel.text = state.secondaryLabel
703             secondaryLabel.visibility =
704                 if (TextUtils.isEmpty(state.secondaryLabel)) {
705                     GONE
706                 } else {
707                     VISIBLE
708                 }
709         }
710 
711         // Colors
712         if (state.state != lastState || state.disabledByPolicy != lastDisabledByPolicy) {
713             singleAnimator.cancel()
714             mQsLogger?.logTileBackgroundColorUpdateIfInternetTile(
715                 state.spec,
716                 state.state,
717                 state.disabledByPolicy,
718                 getBackgroundColorForState(state.state, state.disabledByPolicy),
719             )
720             if (allowAnimations) {
721                 singleAnimator.setValues(
722                     colorValuesHolder(
723                         BACKGROUND_NAME,
724                         backgroundColor,
725                         getBackgroundColorForState(state.state, state.disabledByPolicy),
726                     ),
727                     colorValuesHolder(
728                         LABEL_NAME,
729                         label.currentTextColor,
730                         getLabelColorForState(state.state, state.disabledByPolicy),
731                     ),
732                     colorValuesHolder(
733                         SECONDARY_LABEL_NAME,
734                         secondaryLabel.currentTextColor,
735                         getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
736                     ),
737                     colorValuesHolder(
738                         CHEVRON_NAME,
739                         chevronView.imageTintList?.defaultColor ?: 0,
740                         getChevronColorForState(state.state, state.disabledByPolicy),
741                     ),
742                     colorValuesHolder(
743                         OVERLAY_NAME,
744                         backgroundOverlayColor,
745                         getOverlayColorForState(state.state),
746                     ),
747                 )
748                 singleAnimator.start()
749             } else {
750                 setAllColors(
751                     getBackgroundColorForState(state.state, state.disabledByPolicy),
752                     getLabelColorForState(state.state, state.disabledByPolicy),
753                     getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
754                     getChevronColorForState(state.state, state.disabledByPolicy),
755                     getOverlayColorForState(state.state),
756                 )
757             }
758         }
759 
760         // Right side icon
761         loadSideViewDrawableIfNecessary(state)
762 
763         label.isEnabled = !state.disabledByPolicy
764 
765         lastState = state.state
766         lastDisabledByPolicy = state.disabledByPolicy
767         lastIconTint = icon.getColor(state)
768 
769         // Long-press effects
770         updateLongPressEffect(state.handlesLongClick)
771     }
772 
773     private fun updateLongPressEffect(handlesLongClick: Boolean) {
774         // The long press effect in the tile can't be updated if it is still running
775         if (
776             longPressEffect?.state != QSLongPressEffect.State.IDLE &&
777                 longPressEffect?.state != QSLongPressEffect.State.CLICKED
778         )
779             return
780 
781         longPressEffect.qsTile?.state?.handlesLongClick = handlesLongClick
782         if (handlesLongClick && longPressEffect.initializeEffect(longPressEffectDuration)) {
783             showRippleEffect = false
784             longPressEffect.qsTile?.state?.state = lastState // Store the tile's state
785             longPressEffect.resetState()
786             initializeLongPressProperties(measuredHeight, measuredWidth)
787         } else {
788             // Long-press effects might have been enabled before but the new state does not
789             // handle a long-press. In this case, we go back to the behaviour of a regular tile
790             // and clean-up the resources
791             showRippleEffect = isClickable
792             initialLongPressProperties = null
793             finalLongPressProperties = null
794         }
795     }
796 
797     private fun setAllColors(
798         backgroundColor: Int,
799         labelColor: Int,
800         secondaryLabelColor: Int,
801         chevronColor: Int,
802         overlayColor: Int,
803     ) {
804         setColor(backgroundColor)
805         setLabelColor(labelColor)
806         setSecondaryLabelColor(secondaryLabelColor)
807         setChevronColor(chevronColor)
808         setOverlayColor(overlayColor)
809     }
810 
811     private fun setColor(color: Int) {
812         backgroundBaseDrawable.mutate().setTint(color)
813         backgroundColor = color
814     }
815 
816     private fun setLabelColor(color: Int) {
817         label.setTextColor(color)
818     }
819 
820     private fun setSecondaryLabelColor(color: Int) {
821         secondaryLabel.setTextColor(color)
822     }
823 
824     private fun setChevronColor(color: Int) {
825         chevronView.imageTintList = ColorStateList.valueOf(color)
826     }
827 
828     private fun setOverlayColor(overlayColor: Int) {
829         backgroundOverlayDrawable.setTint(overlayColor)
830         backgroundOverlayColor = overlayColor
831     }
832 
833     private fun loadSideViewDrawableIfNecessary(state: QSTile.State) {
834         if (state.sideViewCustomDrawable != null) {
835             customDrawableView.setImageDrawable(state.sideViewCustomDrawable)
836             customDrawableView.visibility = VISIBLE
837             chevronView.visibility = GONE
838         } else if (state !is AdapterState || state.forceExpandIcon) {
839             customDrawableView.setImageDrawable(null)
840             customDrawableView.visibility = GONE
841             chevronView.visibility = VISIBLE
842         } else {
843             customDrawableView.setImageDrawable(null)
844             customDrawableView.visibility = GONE
845             chevronView.visibility = GONE
846         }
847     }
848 
849     private fun getUnavailableText(spec: String?): String {
850         val arrayResId = SubtitleArrayMapping.getSubtitleId(spec)
851         return resources.getStringArray(arrayResId)[Tile.STATE_UNAVAILABLE]
852     }
853 
854     /*
855      * The view should not be animated if it's not on screen and no part of it is visible.
856      */
857     protected open fun animationsEnabled(): Boolean {
858         if (!isShown) {
859             return false
860         }
861         if (alpha != 1f) {
862             return false
863         }
864         getLocationOnScreen(locInScreen)
865         return locInScreen.get(1) >= -height
866     }
867 
868     private fun getBackgroundColorForState(state: Int, disabledByPolicy: Boolean = false): Int {
869         return when {
870             state == Tile.STATE_UNAVAILABLE || disabledByPolicy -> colorUnavailable
871             state == Tile.STATE_ACTIVE -> colorActive
872             state == Tile.STATE_INACTIVE -> colorInactive
873             else -> {
874                 Log.e(TAG, "Invalid state $state")
875                 0
876             }
877         }
878     }
879 
880     private fun getLabelColorForState(state: Int, disabledByPolicy: Boolean = false): Int {
881         return when {
882             state == Tile.STATE_UNAVAILABLE || disabledByPolicy -> colorLabelUnavailable
883             state == Tile.STATE_ACTIVE -> colorLabelActive
884             state == Tile.STATE_INACTIVE -> colorLabelInactive
885             else -> {
886                 Log.e(TAG, "Invalid state $state")
887                 0
888             }
889         }
890     }
891 
892     private fun getSecondaryLabelColorForState(state: Int, disabledByPolicy: Boolean = false): Int {
893         return when {
894             state == Tile.STATE_UNAVAILABLE || disabledByPolicy -> colorSecondaryLabelUnavailable
895             state == Tile.STATE_ACTIVE -> colorSecondaryLabelActive
896             state == Tile.STATE_INACTIVE -> colorSecondaryLabelInactive
897             else -> {
898                 Log.e(TAG, "Invalid state $state")
899                 0
900             }
901         }
902     }
903 
904     private fun getChevronColorForState(state: Int, disabledByPolicy: Boolean = false): Int =
905         getSecondaryLabelColorForState(state, disabledByPolicy)
906 
907     private fun getOverlayColorForState(state: Int): Int {
908         return when (state) {
909             Tile.STATE_ACTIVE -> overlayColorActive
910             Tile.STATE_INACTIVE -> overlayColorInactive
911             else -> Color.TRANSPARENT
912         }
913     }
914 
915     override fun onActivityLaunchAnimationEnd() {
916         longPressEffect?.resetState()
917         if (longPressEffect != null && !haveLongPressPropertiesBeenReset) {
918             resetLongPressEffectProperties()
919         }
920     }
921 
922     private fun prepareForLaunch() {
923         val startingHeight = initialLongPressProperties?.height?.toInt() ?: 0
924         val startingWidth = initialLongPressProperties?.width?.toInt() ?: 0
925         val deltaH = finalLongPressProperties?.height?.minus(startingHeight)?.toInt() ?: 0
926         val deltaW = finalLongPressProperties?.width?.minus(startingWidth)?.toInt() ?: 0
927         paddingForLaunch.left = -deltaW / 2
928         paddingForLaunch.top = -deltaH / 2
929         paddingForLaunch.right = deltaW / 2
930         paddingForLaunch.bottom = deltaH / 2
931     }
932 
933     override fun getPaddingForLaunchAnimation(): Rect =
934         if (longPressEffect?.state == QSLongPressEffect.State.LONG_CLICKED) {
935             paddingForLaunch
936         } else {
937             EMPTY_RECT
938         }
939 
940     fun updateLongPressEffectProperties(effectProgress: Float) {
941         if (!isLongClickable || longPressEffect == null) return
942 
943         if (haveLongPressPropertiesBeenReset) haveLongPressPropertiesBeenReset = false
944 
945         // Dimensions change
946         val newHeight =
947             interpolateFloat(
948                     effectProgress,
949                     initialLongPressProperties?.height ?: 0f,
950                     finalLongPressProperties?.height ?: 0f,
951                 )
952                 .toInt()
953         val newWidth =
954             interpolateFloat(
955                     effectProgress,
956                     initialLongPressProperties?.width ?: 0f,
957                     finalLongPressProperties?.width ?: 0f,
958                 )
959                 .toInt()
960 
961         val startingHeight = initialLongPressProperties?.height?.toInt() ?: 0
962         val startingWidth = initialLongPressProperties?.width?.toInt() ?: 0
963         val deltaH = (newHeight - startingHeight) / 2
964         val deltaW = (newWidth - startingWidth) / 2
965 
966         background.updateBounds(
967             left = -deltaW,
968             top = -deltaH,
969             right = newWidth - deltaW,
970             bottom = newHeight - deltaH,
971         )
972 
973         // Radius change
974         val newRadius =
975             interpolateFloat(
976                 effectProgress,
977                 initialLongPressProperties?.cornerRadius ?: 0f,
978                 finalLongPressProperties?.cornerRadius ?: 0f,
979             )
980         changeCornerRadius(newRadius)
981 
982         // Color change
983         setAllColors(
984             colorEvaluator.evaluate(
985                 effectProgress,
986                 initialLongPressProperties?.backgroundColor ?: 0,
987                 finalLongPressProperties?.backgroundColor ?: 0,
988             ) as Int,
989             colorEvaluator.evaluate(
990                 effectProgress,
991                 initialLongPressProperties?.labelColor ?: 0,
992                 finalLongPressProperties?.labelColor ?: 0,
993             ) as Int,
994             colorEvaluator.evaluate(
995                 effectProgress,
996                 initialLongPressProperties?.secondaryLabelColor ?: 0,
997                 finalLongPressProperties?.secondaryLabelColor ?: 0,
998             ) as Int,
999             colorEvaluator.evaluate(
1000                 effectProgress,
1001                 initialLongPressProperties?.chevronColor ?: 0,
1002                 finalLongPressProperties?.chevronColor ?: 0,
1003             ) as Int,
1004             colorEvaluator.evaluate(
1005                 effectProgress,
1006                 initialLongPressProperties?.overlayColor ?: 0,
1007                 finalLongPressProperties?.overlayColor ?: 0,
1008             ) as Int,
1009         )
1010         icon.setTint(
1011             icon.mIcon as ImageView,
1012             colorEvaluator.evaluate(
1013                 effectProgress,
1014                 initialLongPressProperties?.iconColor ?: 0,
1015                 finalLongPressProperties?.iconColor ?: 0,
1016             ) as Int,
1017         )
1018     }
1019 
1020     private fun interpolateFloat(fraction: Float, start: Float, end: Float): Float =
1021         start + fraction * (end - start)
1022 
1023     fun resetLongPressEffectProperties() {
1024         background.updateBounds(
1025             left = 0,
1026             top = 0,
1027             right = initialLongPressProperties?.width?.toInt() ?: measuredWidth,
1028             bottom = initialLongPressProperties?.height?.toInt() ?: measuredHeight,
1029         )
1030         changeCornerRadius(resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat())
1031         setAllColors(
1032             getBackgroundColorForState(lastState, lastDisabledByPolicy),
1033             getLabelColorForState(lastState, lastDisabledByPolicy),
1034             getSecondaryLabelColorForState(lastState, lastDisabledByPolicy),
1035             getChevronColorForState(lastState, lastDisabledByPolicy),
1036             getOverlayColorForState(lastState),
1037         )
1038         icon.setTint(icon.mIcon as ImageView, lastIconTint)
1039         haveLongPressPropertiesBeenReset = true
1040     }
1041 
1042     @VisibleForTesting
1043     fun initializeLongPressProperties(startingHeight: Int, startingWidth: Int) {
1044         initialLongPressProperties =
1045             QSLongPressProperties(
1046                 height = startingHeight.toFloat(),
1047                 width = startingWidth.toFloat(),
1048                 resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat(),
1049                 getBackgroundColorForState(lastState),
1050                 getLabelColorForState(lastState),
1051                 getSecondaryLabelColorForState(lastState),
1052                 getChevronColorForState(lastState),
1053                 getOverlayColorForState(lastState),
1054                 lastIconTint,
1055             )
1056 
1057         finalLongPressProperties =
1058             QSLongPressProperties(
1059                 height = LONG_PRESS_EFFECT_HEIGHT_SCALE * startingHeight,
1060                 width = LONG_PRESS_EFFECT_WIDTH_SCALE * startingWidth,
1061                 resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat() - 20,
1062                 getBackgroundColorForState(Tile.STATE_ACTIVE),
1063                 getLabelColorForState(Tile.STATE_ACTIVE),
1064                 getSecondaryLabelColorForState(Tile.STATE_ACTIVE),
1065                 getChevronColorForState(Tile.STATE_ACTIVE),
1066                 getOverlayColorForState(Tile.STATE_ACTIVE),
1067                 Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive),
1068             )
1069         prepareForLaunch()
1070     }
1071 
1072     private fun changeCornerRadius(radius: Float) {
1073         for (i in 0 until backgroundDrawable.numberOfLayers) {
1074             val layer = backgroundDrawable.getDrawable(i)
1075             if (layer is GradientDrawable) {
1076                 layer.cornerRadius = radius
1077             }
1078         }
1079     }
1080 
1081     @VisibleForTesting
1082     internal fun getCurrentColors(): List<Int> =
1083         listOf(
1084             backgroundColor,
1085             label.currentTextColor,
1086             secondaryLabel.currentTextColor,
1087             chevronView.imageTintList?.defaultColor ?: 0,
1088         )
1089 
1090     inner class StateChangeRunnable(private val state: QSTile.State) : Runnable {
1091         override fun run() {
1092             var traceTag = "QSTileViewImpl#handleStateChanged"
1093             if (!state.spec.isNullOrEmpty()) {
1094                 traceTag += ":"
1095                 traceTag += state.spec
1096             }
1097             traceSection(traceTag.take(Trace.MAX_SECTION_NAME_LEN)) { handleStateChanged(state) }
1098         }
1099 
1100         // We want all instances of this runnable to be equal to each other, so they can be used to
1101         // remove previous instances from the Handler/RunQueue of this view
1102         override fun equals(other: Any?): Boolean {
1103             return other is StateChangeRunnable
1104         }
1105 
1106         // This makes sure that all instances have the same hashcode (because they are `equal`)
1107         override fun hashCode(): Int {
1108             return StateChangeRunnable::class.hashCode()
1109         }
1110     }
1111 }
1112 
constrainSquishinessnull1113 fun constrainSquishiness(squish: Float): Float {
1114     return 0.1f + squish * 0.9f
1115 }
1116 
colorValuesHoldernull1117 private fun colorValuesHolder(name: String, vararg values: Int): PropertyValuesHolder {
1118     return PropertyValuesHolder.ofInt(name, *values).apply {
1119         setEvaluator(ArgbEvaluator.getInstance())
1120     }
1121 }
1122