• 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.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.service.quicksettings.Tile
29 import android.text.TextUtils
30 import android.util.Log
31 import android.view.Gravity
32 import android.view.LayoutInflater
33 import android.view.View
34 import android.view.ViewGroup
35 import android.view.accessibility.AccessibilityEvent
36 import android.view.accessibility.AccessibilityNodeInfo
37 import android.widget.ImageView
38 import android.widget.LinearLayout
39 import android.widget.Switch
40 import android.widget.TextView
41 import androidx.annotation.VisibleForTesting
42 import com.android.settingslib.Utils
43 import com.android.systemui.FontSizeUtils
44 import com.android.systemui.R
45 import com.android.systemui.plugins.qs.QSIconView
46 import com.android.systemui.plugins.qs.QSTile
47 import com.android.systemui.plugins.qs.QSTile.BooleanState
48 import com.android.systemui.plugins.qs.QSTileView
49 import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
50 import java.util.Objects
51 
52 private const val TAG = "QSTileViewImpl"
53 open class QSTileViewImpl @JvmOverloads constructor(
54     context: Context,
55     private val _icon: QSIconView,
56     private val collapsed: Boolean = false
57 ) : QSTileView(context), HeightOverrideable {
58 
59     companion object {
60         private const val INVALID = -1
61         private const val BACKGROUND_NAME = "background"
62         private const val LABEL_NAME = "label"
63         private const val SECONDARY_LABEL_NAME = "secondaryLabel"
64         private const val CHEVRON_NAME = "chevron"
65         const val UNAVAILABLE_ALPHA = 0.3f
66         @VisibleForTesting
67         internal const val TILE_STATE_RES_PREFIX = "tile_states_"
68     }
69 
70     override var heightOverride: Int = HeightOverrideable.NO_OVERRIDE
71 
72     private val colorActive = Utils.getColorAttrDefaultColor(context,
73             com.android.internal.R.attr.colorAccentPrimary)
74     private val colorInactive = Utils.getColorAttrDefaultColor(context, R.attr.offStateColor)
75     private val colorUnavailable = Utils.applyAlpha(UNAVAILABLE_ALPHA, colorInactive)
76 
77     private val colorLabelActive =
78             Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimaryInverse)
79     private val colorLabelInactive =
80             Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
81     private val colorLabelUnavailable = Utils.applyAlpha(UNAVAILABLE_ALPHA, colorLabelInactive)
82 
83     private val colorSecondaryLabelActive =
84             Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondaryInverse)
85     private val colorSecondaryLabelInactive =
86             Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary)
87     private val colorSecondaryLabelUnavailable =
88             Utils.applyAlpha(UNAVAILABLE_ALPHA, colorSecondaryLabelInactive)
89 
90     private lateinit var label: TextView
91     protected lateinit var secondaryLabel: TextView
92     private lateinit var labelContainer: IgnorableChildLinearLayout
93     protected lateinit var sideView: ViewGroup
94     private lateinit var customDrawableView: ImageView
95     private lateinit var chevronView: ImageView
96 
97     protected var showRippleEffect = true
98 
99     private lateinit var ripple: RippleDrawable
100     private lateinit var colorBackgroundDrawable: Drawable
101     private var paintColor: Int = 0
102     private val singleAnimator: ValueAnimator = ValueAnimator().apply {
103         setDuration(QS_ANIM_LENGTH)
104         addUpdateListener { animation ->
105             setAllColors(
106                 // These casts will throw an exception if some property is missing. We should
107                 // always have all properties.
108                 animation.getAnimatedValue(BACKGROUND_NAME) as Int,
109                 animation.getAnimatedValue(LABEL_NAME) as Int,
110                 animation.getAnimatedValue(SECONDARY_LABEL_NAME) as Int,
111                 animation.getAnimatedValue(CHEVRON_NAME) as Int
112             )
113         }
114     }
115 
116     private var accessibilityClass: String? = null
117     private var stateDescriptionDeltas: CharSequence? = null
118     private var lastStateDescription: CharSequence? = null
119     private var tileState = false
120     private var lastState = INVALID
121 
122     private val locInScreen = IntArray(2)
123 
124     init {
125         setId(generateViewId())
126         orientation = LinearLayout.HORIZONTAL
127         gravity = Gravity.CENTER_VERTICAL or Gravity.START
128         importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
129         clipChildren = false
130         clipToPadding = false
131         isFocusable = true
132         background = createTileBackground()
133         setColor(getBackgroundColorForState(QSTile.State.DEFAULT_STATE))
134 
135         val padding = resources.getDimensionPixelSize(R.dimen.qs_tile_padding)
136         val startPadding = resources.getDimensionPixelSize(R.dimen.qs_tile_start_padding)
137         setPaddingRelative(startPadding, padding, padding, padding)
138 
139         val iconSize = resources.getDimensionPixelSize(R.dimen.qs_icon_size)
140         addView(_icon, LayoutParams(iconSize, iconSize))
141 
142         createAndAddLabels()
143         createAndAddSideView()
144     }
145 
146     override fun onConfigurationChanged(newConfig: Configuration?) {
147         super.onConfigurationChanged(newConfig)
148         updateResources()
149     }
150 
151     fun updateResources() {
152         FontSizeUtils.updateFontSize(label, R.dimen.qs_tile_text_size)
153         FontSizeUtils.updateFontSize(secondaryLabel, R.dimen.qs_tile_text_size)
154 
155         val iconSize = context.resources.getDimensionPixelSize(R.dimen.qs_icon_size)
156         _icon.layoutParams.apply {
157             height = iconSize
158             width = iconSize
159         }
160 
161         val padding = resources.getDimensionPixelSize(R.dimen.qs_tile_padding)
162         val startPadding = resources.getDimensionPixelSize(R.dimen.qs_tile_start_padding)
163         setPaddingRelative(startPadding, padding, padding, padding)
164 
165         val labelMargin = resources.getDimensionPixelSize(R.dimen.qs_label_container_margin)
166         (labelContainer.layoutParams as MarginLayoutParams).apply {
167             marginStart = labelMargin
168         }
169 
170         (sideView.layoutParams as MarginLayoutParams).apply {
171             marginStart = labelMargin
172         }
173         (chevronView.layoutParams as MarginLayoutParams).apply {
174             height = iconSize
175             width = iconSize
176         }
177 
178         val endMargin = resources.getDimensionPixelSize(R.dimen.qs_drawable_end_margin)
179         (customDrawableView.layoutParams as MarginLayoutParams).apply {
180             height = iconSize
181             marginEnd = endMargin
182         }
183     }
184 
185     private fun createAndAddLabels() {
186         labelContainer = LayoutInflater.from(context)
187                 .inflate(R.layout.qs_tile_label, this, false) as IgnorableChildLinearLayout
188         label = labelContainer.requireViewById(R.id.tile_label)
189         secondaryLabel = labelContainer.requireViewById(R.id.app_label)
190         if (collapsed) {
191             labelContainer.ignoreLastView = true
192             // Ideally, it'd be great if the parent could set this up when measuring just this child
193             // instead of the View class having to support this. However, due to the mysteries of
194             // LinearLayout's double measure pass, we cannot overwrite `measureChild` or any of its
195             // sibling methods to have special behavior for labelContainer.
196             labelContainer.forceUnspecifiedMeasure = true
197             secondaryLabel.alpha = 0f
198         }
199         setLabelColor(getLabelColorForState(QSTile.State.DEFAULT_STATE))
200         setSecondaryLabelColor(getSecondaryLabelColorForState(QSTile.State.DEFAULT_STATE))
201         addView(labelContainer)
202     }
203 
204     private fun createAndAddSideView() {
205         sideView = LayoutInflater.from(context)
206                 .inflate(R.layout.qs_tile_side_icon, this, false) as ViewGroup
207         customDrawableView = sideView.requireViewById(R.id.customDrawable)
208         chevronView = sideView.requireViewById(R.id.chevron)
209         setChevronColor(getChevronColorForState(QSTile.State.DEFAULT_STATE))
210         addView(sideView)
211     }
212 
213     fun createTileBackground(): Drawable {
214         ripple = mContext.getDrawable(R.drawable.qs_tile_background) as RippleDrawable
215         colorBackgroundDrawable = ripple.findDrawableByLayerId(R.id.background)
216         return ripple
217     }
218 
219     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
220         super.onLayout(changed, l, t, r, b)
221         if (heightOverride != HeightOverrideable.NO_OVERRIDE) {
222             bottom = top + heightOverride
223         }
224     }
225 
226     override fun updateAccessibilityOrder(previousView: View?): View {
227         accessibilityTraversalAfter = previousView?.id ?: ID_NULL
228         return this
229     }
230 
231     override fun getIcon(): QSIconView {
232         return _icon
233     }
234 
235     override fun getIconWithBackground(): View {
236         return icon
237     }
238 
239     override fun init(tile: QSTile) {
240         init(
241                 { v: View? -> tile.click(this) },
242                 { view: View? ->
243                     tile.longClick(this)
244                     true
245                 }
246         )
247     }
248 
249     private fun init(
250         click: OnClickListener?,
251         longClick: OnLongClickListener?
252     ) {
253         setOnClickListener(click)
254         onLongClickListener = longClick
255     }
256 
257     override fun onStateChanged(state: QSTile.State) {
258         post {
259             handleStateChanged(state)
260         }
261     }
262 
263     override fun getDetailY(): Int {
264         return top + height / 2
265     }
266 
267     override fun hasOverlappingRendering(): Boolean {
268         // Avoid layers for this layout - we don't need them.
269         return false
270     }
271 
272     override fun setClickable(clickable: Boolean) {
273         super.setClickable(clickable)
274         background = if (clickable && showRippleEffect) {
275             ripple.also {
276                 // In case that the colorBackgroundDrawable was used as the background, make sure
277                 // it has the correct callback instead of null
278                 colorBackgroundDrawable.callback = it
279             }
280         } else {
281             colorBackgroundDrawable
282         }
283     }
284 
285     override fun getLabelContainer(): View {
286         return labelContainer
287     }
288 
289     override fun getSecondaryLabel(): View {
290         return secondaryLabel
291     }
292 
293     override fun getSecondaryIcon(): View {
294         return sideView
295     }
296 
297     // Accessibility
298 
299     override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
300         super.onInitializeAccessibilityEvent(event)
301         if (!TextUtils.isEmpty(accessibilityClass)) {
302             event.className = accessibilityClass
303         }
304         if (event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION &&
305                 stateDescriptionDeltas != null) {
306             event.text.add(stateDescriptionDeltas)
307             stateDescriptionDeltas = null
308         }
309     }
310 
311     override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
312         super.onInitializeAccessibilityNodeInfo(info)
313         // Clear selected state so it is not announce by talkback.
314         info.isSelected = false
315         if (!TextUtils.isEmpty(accessibilityClass)) {
316             info.className = accessibilityClass
317             if (Switch::class.java.name == accessibilityClass) {
318                 val label = resources.getString(
319                         if (tileState) R.string.switch_bar_on else R.string.switch_bar_off)
320                 // Set the text here for tests in
321                 // android.platform.test.scenario.sysui.quicksettings. Can be removed when
322                 // UiObject2 has a new getStateDescription() API and tests are updated.
323                 info.text = label
324                 info.isChecked = tileState
325                 info.isCheckable = true
326                 if (isLongClickable) {
327                     info.addAction(
328                             AccessibilityNodeInfo.AccessibilityAction(
329                                     AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
330                                     resources.getString(
331                                             R.string.accessibility_long_click_tile)))
332                 }
333             }
334         }
335     }
336 
337     override fun toString(): String {
338         val sb = StringBuilder(javaClass.simpleName).append('[')
339         sb.append("locInScreen=(${locInScreen[0]}, ${locInScreen[1]})")
340         sb.append(", iconView=$_icon")
341         sb.append(", tileState=$tileState")
342         sb.append("]")
343         return sb.toString()
344     }
345 
346     // HANDLE STATE CHANGES RELATED METHODS
347 
348     protected open fun handleStateChanged(state: QSTile.State) {
349         val allowAnimations = animationsEnabled()
350         showRippleEffect = state.showRippleEffect
351         isClickable = state.state != Tile.STATE_UNAVAILABLE
352         isLongClickable = state.handlesLongClick
353         icon.setIcon(state, allowAnimations)
354         contentDescription = state.contentDescription
355 
356         // State handling and description
357         val stateDescription = StringBuilder()
358         val stateText = getStateText(state)
359         if (!TextUtils.isEmpty(stateText)) {
360             stateDescription.append(stateText)
361             if (TextUtils.isEmpty(state.secondaryLabel)) {
362                 state.secondaryLabel = stateText
363             }
364         }
365         if (!TextUtils.isEmpty(state.stateDescription)) {
366             stateDescription.append(", ")
367             stateDescription.append(state.stateDescription)
368             if (lastState != INVALID && state.state == lastState &&
369                     state.stateDescription != lastStateDescription) {
370                 stateDescriptionDeltas = state.stateDescription
371             }
372         }
373 
374         setStateDescription(stateDescription.toString())
375         lastStateDescription = state.stateDescription
376 
377         accessibilityClass = if (state.state == Tile.STATE_UNAVAILABLE) {
378             null
379         } else {
380             state.expandedAccessibilityClassName
381         }
382 
383         if (state is BooleanState) {
384             val newState = state.value
385             if (tileState != newState) {
386                 tileState = newState
387             }
388         }
389         //
390 
391         // Labels
392         if (!Objects.equals(label.text, state.label)) {
393             label.text = state.label
394         }
395         if (!Objects.equals(secondaryLabel.text, state.secondaryLabel)) {
396             secondaryLabel.text = state.secondaryLabel
397             secondaryLabel.visibility = if (TextUtils.isEmpty(state.secondaryLabel)) {
398                 GONE
399             } else {
400                 VISIBLE
401             }
402         }
403 
404         // Colors
405         if (state.state != lastState) {
406             singleAnimator.cancel()
407             if (allowAnimations) {
408                 singleAnimator.setValues(
409                         colorValuesHolder(
410                                 BACKGROUND_NAME,
411                                 paintColor,
412                                 getBackgroundColorForState(state.state)
413                         ),
414                         colorValuesHolder(
415                                 LABEL_NAME,
416                                 label.currentTextColor,
417                                 getLabelColorForState(state.state)
418                         ),
419                         colorValuesHolder(
420                                 SECONDARY_LABEL_NAME,
421                                 secondaryLabel.currentTextColor,
422                                 getSecondaryLabelColorForState(state.state)
423                         ),
424                         colorValuesHolder(
425                                 CHEVRON_NAME,
426                                 chevronView.imageTintList?.defaultColor ?: 0,
427                                 getChevronColorForState(state.state)
428                         )
429                     )
430                 singleAnimator.start()
431             } else {
432                 setAllColors(
433                     getBackgroundColorForState(state.state),
434                     getLabelColorForState(state.state),
435                     getSecondaryLabelColorForState(state.state),
436                     getChevronColorForState(state.state)
437                 )
438             }
439         }
440 
441         // Right side icon
442         loadSideViewDrawableIfNecessary(state)
443 
444         label.isEnabled = !state.disabledByPolicy
445 
446         lastState = state.state
447     }
448 
449     private fun setAllColors(
450         backgroundColor: Int,
451         labelColor: Int,
452         secondaryLabelColor: Int,
453         chevronColor: Int
454     ) {
455         setColor(backgroundColor)
456         setLabelColor(labelColor)
457         setSecondaryLabelColor(secondaryLabelColor)
458         setChevronColor(chevronColor)
459     }
460 
461     private fun setColor(color: Int) {
462         colorBackgroundDrawable.setTint(color)
463         paintColor = color
464     }
465 
466     private fun setLabelColor(color: Int) {
467         label.setTextColor(color)
468     }
469 
470     private fun setSecondaryLabelColor(color: Int) {
471         secondaryLabel.setTextColor(color)
472     }
473 
474     private fun setChevronColor(color: Int) {
475         chevronView.imageTintList = ColorStateList.valueOf(color)
476     }
477 
478     private fun loadSideViewDrawableIfNecessary(state: QSTile.State) {
479         if (state.sideViewCustomDrawable != null) {
480             customDrawableView.setImageDrawable(state.sideViewCustomDrawable)
481             customDrawableView.visibility = VISIBLE
482             chevronView.visibility = GONE
483         } else if (state !is BooleanState || state.forceExpandIcon) {
484             customDrawableView.setImageDrawable(null)
485             customDrawableView.visibility = GONE
486             chevronView.visibility = VISIBLE
487         } else {
488             customDrawableView.setImageDrawable(null)
489             customDrawableView.visibility = GONE
490             chevronView.visibility = GONE
491         }
492     }
493 
494     private fun getStateText(state: QSTile.State): String {
495         if (state.disabledByPolicy) {
496             return context.getString(R.string.tile_disabled)
497         }
498 
499         return if (state.state == Tile.STATE_UNAVAILABLE || state is BooleanState) {
500             var arrayResId = SubtitleArrayMapping.getSubtitleId(state.spec)
501             val array = resources.getStringArray(arrayResId)
502             array[state.state]
503         } else {
504             ""
505         }
506     }
507 
508     /*
509      * The view should not be animated if it's not on screen and no part of it is visible.
510      */
511     protected open fun animationsEnabled(): Boolean {
512         if (!isShown) {
513             return false
514         }
515         if (alpha != 1f) {
516             return false
517         }
518         getLocationOnScreen(locInScreen)
519         return locInScreen.get(1) >= -height
520     }
521 
522     private fun getBackgroundColorForState(state: Int): Int {
523         return when (state) {
524             Tile.STATE_ACTIVE -> colorActive
525             Tile.STATE_INACTIVE -> colorInactive
526             Tile.STATE_UNAVAILABLE -> colorUnavailable
527             else -> {
528                 Log.e(TAG, "Invalid state $state")
529                 0
530             }
531         }
532     }
533 
534     private fun getLabelColorForState(state: Int): Int {
535         return when (state) {
536             Tile.STATE_ACTIVE -> colorLabelActive
537             Tile.STATE_INACTIVE -> colorLabelInactive
538             Tile.STATE_UNAVAILABLE -> colorLabelUnavailable
539             else -> {
540                 Log.e(TAG, "Invalid state $state")
541                 0
542             }
543         }
544     }
545 
546     private fun getSecondaryLabelColorForState(state: Int): Int {
547         return when (state) {
548             Tile.STATE_ACTIVE -> colorSecondaryLabelActive
549             Tile.STATE_INACTIVE -> colorSecondaryLabelInactive
550             Tile.STATE_UNAVAILABLE -> colorSecondaryLabelUnavailable
551             else -> {
552                 Log.e(TAG, "Invalid state $state")
553                 0
554             }
555         }
556     }
557 
558     private fun getChevronColorForState(state: Int): Int = getSecondaryLabelColorForState(state)
559 }
560 
561 @VisibleForTesting
562 internal object SubtitleArrayMapping {
563     private val subtitleIdsMap = mapOf<String?, Int>(
564         "internet" to R.array.tile_states_internet,
565         "wifi" to R.array.tile_states_wifi,
566         "cell" to R.array.tile_states_cell,
567         "battery" to R.array.tile_states_battery,
568         "dnd" to R.array.tile_states_dnd,
569         "flashlight" to R.array.tile_states_flashlight,
570         "rotation" to R.array.tile_states_rotation,
571         "bt" to R.array.tile_states_bt,
572         "airplane" to R.array.tile_states_airplane,
573         "location" to R.array.tile_states_location,
574         "hotspot" to R.array.tile_states_hotspot,
575         "inversion" to R.array.tile_states_inversion,
576         "saver" to R.array.tile_states_saver,
577         "dark" to R.array.tile_states_dark,
578         "work" to R.array.tile_states_work,
579         "cast" to R.array.tile_states_cast,
580         "night" to R.array.tile_states_night,
581         "screenrecord" to R.array.tile_states_screenrecord,
582         "reverse" to R.array.tile_states_reverse,
583         "reduce_brightness" to R.array.tile_states_reduce_brightness,
584         "cameratoggle" to R.array.tile_states_cameratoggle,
585         "mictoggle" to R.array.tile_states_mictoggle,
586         "controls" to R.array.tile_states_controls,
587         "wallet" to R.array.tile_states_wallet,
588         "alarm" to R.array.tile_states_alarm
589     )
590 
getSubtitleIdnull591     fun getSubtitleId(spec: String?): Int {
592         return subtitleIdsMap.getOrDefault(spec, R.array.tile_states_default)
593     }
594 }
595 
colorValuesHoldernull596 private fun colorValuesHolder(name: String, vararg values: Int): PropertyValuesHolder {
597     return PropertyValuesHolder.ofInt(name, *values).apply {
598         setEvaluator(ArgbEvaluator.getInstance())
599     }
600 }