Merge "Controls UI - Loading animations" into rvc-dev
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
index 17e4234..f979bbb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
@@ -18,10 +18,15 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
 import android.animation.ValueAnimator
+import android.annotation.ColorRes
 import android.app.Dialog
 import android.content.Context
+import android.content.res.ColorStateList
 import android.graphics.drawable.ClipDrawable
+import android.graphics.drawable.Drawable
 import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.LayerDrawable
 import android.service.controls.Control
@@ -34,6 +39,7 @@
 import android.service.controls.templates.ToggleRangeTemplate
 import android.service.controls.templates.ToggleTemplate
 import android.util.MathUtils
+import android.util.TypedValue
 import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
@@ -63,6 +69,8 @@
         private const val UPDATE_DELAY_IN_MILLIS = 3000L
         private const val ALPHA_ENABLED = 255
         private const val ALPHA_DISABLED = 0
+        private const val STATUS_ALPHA_ENABLED = 1f
+        private const val STATUS_ALPHA_DIMMED = 0.45f
         private val FORCE_PANEL_DEVICES = setOf(
             DeviceTypes.TYPE_THERMOSTAT,
             DeviceTypes.TYPE_CAMERA
@@ -94,9 +102,11 @@
     private val toggleBackgroundIntensity: Float = layout.context.resources
             .getFraction(R.fraction.controls_toggle_bg_intensity, 1, 1)
     private var stateAnimator: ValueAnimator? = null
+    private var statusAnimator: Animator? = null
     private val baseLayer: GradientDrawable
     val icon: ImageView = layout.requireViewById(R.id.icon)
-    val status: TextView = layout.requireViewById(R.id.status)
+    private val status: TextView = layout.requireViewById(R.id.status)
+    private var nextStatusText: CharSequence = ""
     val title: TextView = layout.requireViewById(R.id.title)
     val subtitle: TextView = layout.requireViewById(R.id.subtitle)
     val context: Context = layout.getContext()
@@ -105,6 +115,7 @@
     var cancelUpdate: Runnable? = null
     var behavior: Behavior? = null
     var lastAction: ControlAction? = null
+    var isLoading = false
     private var lastChallengeDialog: Dialog? = null
     private val onDialogCancel: () -> Unit = { lastChallengeDialog = null }
 
@@ -144,6 +155,7 @@
             })
         }
 
+        isLoading = false
         behavior = bindBehavior(behavior, findBehaviorClass(controlStatus, template, deviceType))
         updateContentDescription()
     }
@@ -189,11 +201,11 @@
         val previousText = status.getText()
 
         cancelUpdate = uiExecutor.executeDelayed({
-                status.setText(previousText)
-                updateContentDescription()
-            }, UPDATE_DELAY_IN_MILLIS)
+            setStatusText(previousText)
+            updateContentDescription()
+        }, UPDATE_DELAY_IN_MILLIS)
 
-        status.setText(tempStatus)
+        setStatusText(tempStatus)
         updateContentDescription()
     }
 
@@ -231,18 +243,50 @@
     }
 
     internal fun applyRenderInfo(enabled: Boolean, offset: Int, animated: Boolean = true) {
-        setEnabled(enabled)
-
         val ri = RenderInfo.lookup(context, cws.componentName, deviceType, enabled, offset)
-
         val fg = context.resources.getColorStateList(ri.foreground, context.theme)
+        val newText = nextStatusText
+        nextStatusText = ""
+        val control = cws.control
+
+        var shouldAnimate = animated
+        if (newText == status.text) {
+            shouldAnimate = false
+        }
+        animateStatusChange(shouldAnimate) {
+            updateStatusRow(enabled, newText, ri.icon, fg, control)
+        }
+
+        animateBackgroundChange(shouldAnimate, enabled, ri.enabledBackground)
+    }
+
+    fun getStatusText() = status.text
+
+    fun setStatusTextSize(textSize: Float) =
+        status.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
+
+    fun setStatusText(text: CharSequence, immediately: Boolean = false) {
+        if (immediately) {
+            status.alpha = STATUS_ALPHA_ENABLED
+            status.text = text
+            nextStatusText = ""
+        } else {
+            nextStatusText = text
+        }
+    }
+
+    private fun animateBackgroundChange(
+        animated: Boolean,
+        enabled: Boolean,
+        @ColorRes bgColor: Int
+    ) {
         val bg = context.resources.getColor(R.color.control_default_background, context.theme)
         var (newClipColor, newAlpha) = if (enabled) {
             // allow color overrides for the enabled state only
             val color = cws.control?.getCustomColor()?.let {
                 val state = intArrayOf(android.R.attr.state_enabled)
                 it.getColorForState(state, it.getDefaultColor())
-            } ?: context.resources.getColor(ri.enabledBackground, context.theme)
+            } ?: context.resources.getColor(bgColor, context.theme)
             listOf(color, ALPHA_ENABLED)
         } else {
             listOf(
@@ -251,21 +295,6 @@
             )
         }
 
-        status.setTextColor(fg)
-
-        cws.control?.getCustomIcon()?.let {
-            // do not tint custom icons, assume the intended icon color is correct
-            icon.imageTintList = null
-            icon.setImageIcon(it)
-        } ?: run {
-            icon.setImageDrawable(ri.icon)
-
-            // do not color app icons
-            if (deviceType != DeviceTypes.TYPE_ROUTINE) {
-                icon.imageTintList = fg
-            }
-        }
-
         (clipLayer.getDrawable() as GradientDrawable).apply {
             val newBaseColor = if (behavior is ToggleRangeBehavior) {
                 ColorUtils.blendARGB(bg, newClipColor, toggleBackgroundIntensity)
@@ -303,6 +332,77 @@
         }
     }
 
+    private fun animateStatusChange(animated: Boolean, statusRowUpdater: () -> Unit) {
+        statusAnimator?.cancel()
+
+        if (!animated) {
+            statusRowUpdater.invoke()
+            return
+        }
+
+        if (isLoading) {
+            statusRowUpdater.invoke()
+            statusAnimator = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_DIMMED).apply {
+                repeatMode = ValueAnimator.REVERSE
+                repeatCount = ValueAnimator.INFINITE
+                duration = 500L
+                interpolator = Interpolators.LINEAR
+                startDelay = 900L
+                start()
+            }
+        } else {
+            val fadeOut = ObjectAnimator.ofFloat(status, "alpha", 0f).apply {
+                duration = 200L
+                interpolator = Interpolators.LINEAR
+                addListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator?) {
+                        statusRowUpdater.invoke()
+                    }
+                })
+            }
+            val fadeIn = ObjectAnimator.ofFloat(status, "alpha", STATUS_ALPHA_ENABLED).apply {
+                duration = 200L
+                interpolator = Interpolators.LINEAR
+            }
+            statusAnimator = AnimatorSet().apply {
+                playSequentially(fadeOut, fadeIn)
+                addListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator?) {
+                        status.alpha = STATUS_ALPHA_ENABLED
+                        statusAnimator = null
+                    }
+                })
+                start()
+            }
+        }
+    }
+
+    private fun updateStatusRow(
+        enabled: Boolean,
+        text: CharSequence,
+        drawable: Drawable,
+        color: ColorStateList,
+        control: Control?
+    ) {
+        setEnabled(enabled)
+
+        status.text = text
+        status.setTextColor(color)
+
+        control?.getCustomIcon()?.let {
+            // do not tint custom icons, assume the intended icon color is correct
+            icon.imageTintList = null
+            icon.setImageIcon(it)
+        } ?: run {
+            icon.setImageDrawable(drawable)
+
+            // do not color app icons
+            if (deviceType != DeviceTypes.TYPE_ROUTINE) {
+                icon.imageTintList = color
+            }
+        }
+    }
+
     private fun setEnabled(enabled: Boolean) {
         status.setEnabled(enabled)
         icon.setEnabled(enabled)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt
index 722ade9..0c8e3ff 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt
@@ -24,7 +24,7 @@
     }
 
     override fun bind(cws: ControlWithState, colorOffset: Int) {
-        cvh.status.setText(cws.control?.getStatusText() ?: "")
+        cvh.setStatusText(cws.control?.getStatusText() ?: "")
         cvh.applyRenderInfo(false, colorOffset)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
index 124df32..ba331f4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt
@@ -97,6 +97,8 @@
 private const val THERMOSTAT_RANGE = DeviceTypes.TYPE_THERMOSTAT * BUCKET_SIZE
 
 private val deviceColorMap = mapOf<Int, Pair<Int, Int>>(
+    (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_OFF) to
+        Pair(R.color.control_default_foreground, R.color.control_default_background),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_HEAT) to
         Pair(R.color.thermo_heat_foreground, R.color.control_enabled_thermo_heat_background),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_COOL) to
@@ -108,13 +110,9 @@
 }
 
 private val deviceIconMap = mapOf<Int, IconState>(
-    THERMOSTAT_RANGE to IconState(
-        R.drawable.ic_device_thermostat_off,
-        R.drawable.ic_device_thermostat_on
-    ),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_OFF) to IconState(
         R.drawable.ic_device_thermostat_off,
-        R.drawable.ic_device_thermostat_on
+        R.drawable.ic_device_thermostat_off
     ),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_HEAT) to IconState(
         R.drawable.ic_device_thermostat_off,
@@ -130,7 +128,7 @@
     ),
     (THERMOSTAT_RANGE + TemperatureControlTemplate.MODE_ECO) to IconState(
         R.drawable.ic_device_thermostat_off,
-        R.drawable.ic_device_thermostat_on
+        R.drawable.ic_device_thermostat_off
     ),
     DeviceTypes.TYPE_THERMOSTAT to IconState(
         R.drawable.ic_device_thermostat_off,
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
index d8dceba..bf3835d 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
@@ -32,9 +32,12 @@
         val msg = when (status) {
             Control.STATUS_ERROR -> R.string.controls_error_generic
             Control.STATUS_NOT_FOUND -> R.string.controls_error_removed
-            else -> com.android.internal.R.string.loading
+            else -> {
+                cvh.isLoading = true
+                com.android.internal.R.string.loading
+            }
         }
-        cvh.status.setText(cvh.context.getString(msg))
+        cvh.setStatusText(cvh.context.getString(msg))
         cvh.applyRenderInfo(false, colorOffset)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt
index 2795c7a..a7dc09b 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt
@@ -39,7 +39,7 @@
     override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
 
-        cvh.status.setText(control.getStatusText())
+        cvh.setStatusText(control.getStatusText())
 
         val ld = cvh.layout.getBackground() as LayerDrawable
         clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt
index c432c09..dc7247c 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt
@@ -34,7 +34,6 @@
 
     override fun initialize(cvh: ControlViewHolder) {
         this.cvh = cvh
-        cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */)
 
         cvh.layout.setOnClickListener(View.OnClickListener() {
             cvh.controlActionCoordinator.toggle(cvh, template.getTemplateId(), template.isChecked())
@@ -44,7 +43,7 @@
     override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
 
-        cvh.status.setText(control.getStatusText())
+        cvh.setStatusText(control.getStatusText())
         val controlTemplate = control.getControlTemplate()
         template = when (controlTemplate) {
             is ToggleTemplate -> controlTemplate
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
index a09ed09..1f0ca9b 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
@@ -31,7 +31,6 @@
 import android.service.controls.templates.ToggleRangeTemplate
 import android.util.Log
 import android.util.MathUtils
-import android.util.TypedValue
 import android.view.GestureDetector
 import android.view.GestureDetector.SimpleOnGestureListener
 import android.view.MotionEvent
@@ -39,7 +38,6 @@
 import android.view.ViewGroup
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityNodeInfo
-import android.widget.TextView
 import com.android.systemui.Interpolators
 import com.android.systemui.R
 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL
@@ -57,7 +55,6 @@
     lateinit var control: Control
     lateinit var cvh: ControlViewHolder
     lateinit var rangeTemplate: RangeTemplate
-    lateinit var status: TextView
     lateinit var context: Context
     var currentStatusText: CharSequence = ""
     var currentRangeValue: String = ""
@@ -71,10 +68,7 @@
 
     override fun initialize(cvh: ControlViewHolder) {
         this.cvh = cvh
-        status = cvh.status
-        context = status.getContext()
-
-        cvh.applyRenderInfo(false /* enabled */, colorOffset, false /* animated */)
+        context = cvh.context
 
         val gestureListener = ToggleRangeGestureListener(cvh.layout)
         val gestureDetector = GestureDetector(context, gestureListener)
@@ -131,7 +125,6 @@
         this.colorOffset = colorOffset
 
         currentStatusText = control.getStatusText()
-        status.setText(currentStatusText)
 
         // ControlViewHolder sets a long click listener, but we want to handle touch in
         // here instead, otherwise we'll have state conflicts.
@@ -222,7 +215,7 @@
     }
 
     fun beginUpdateRange() {
-        status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
+        cvh.setStatusTextSize(context.getResources()
                 .getDimensionPixelSize(R.dimen.control_status_expanded).toFloat())
     }
 
@@ -261,14 +254,13 @@
             val newValue = levelToRangeValue(newLevel)
             currentRangeValue = format(rangeTemplate.getFormatString().toString(),
                     DEFAULT_FORMAT, newValue)
-            val text = if (isDragging) {
-                currentRangeValue
+            if (isDragging) {
+                cvh.setStatusText(currentRangeValue, /* immediately */ true)
             } else {
-                "$currentStatusText $currentRangeValue"
+                cvh.setStatusText("$currentStatusText $currentRangeValue")
             }
-            status.setText(text)
         } else {
-            status.setText(currentStatusText)
+            cvh.setStatusText(currentStatusText)
         }
     }
 
@@ -296,9 +288,9 @@
     }
 
     fun endUpdateRange() {
-        status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
+        cvh.setStatusTextSize(context.getResources()
                 .getDimensionPixelSize(R.dimen.control_status_normal).toFloat())
-        status.setText("$currentStatusText $currentRangeValue")
+        cvh.setStatusText("$currentStatusText $currentRangeValue", /* immediately */ true)
         cvh.action(FloatAction(rangeTemplate.getTemplateId(),
             findNearestStep(levelToRangeValue(clipLayer.getLevel()))))
     }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt
index 8ce2e61..48f9458 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt
@@ -23,6 +23,7 @@
 import android.service.controls.templates.ControlTemplate
 
 import com.android.systemui.R
+import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL
 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL
 
 /**
@@ -37,7 +38,6 @@
 
     override fun initialize(cvh: ControlViewHolder) {
         this.cvh = cvh
-        cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */)
 
         cvh.layout.setOnClickListener(View.OnClickListener() {
             cvh.controlActionCoordinator.touch(cvh, template.getTemplateId(), control)
@@ -46,13 +46,14 @@
 
     override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
-        cvh.status.setText(control.getStatusText())
+        cvh.setStatusText(control.getStatusText())
         template = control.getControlTemplate()
 
         val ld = cvh.layout.getBackground() as LayerDrawable
         clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
-        clipLayer.setLevel(MIN_LEVEL)
 
-        cvh.applyRenderInfo(false, colorOffset)
+        val enabled = if (colorOffset > 0) true else false
+        clipLayer.setLevel(if (enabled) MAX_LEVEL else MIN_LEVEL)
+        cvh.applyRenderInfo(enabled, colorOffset)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 8e1854a..6274467 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -2221,7 +2221,7 @@
             mBackgroundDrawable.setAlpha(0);
             float xOffset = mGlobalActionsLayout.getAnimationOffsetX();
             ObjectAnimator alphaAnimator =
-                    ObjectAnimator.ofFloat(mContainer, "transitionAlpha", 0f, 1f);
+                    ObjectAnimator.ofFloat(mContainer, "alpha", 0f, 1f);
             alphaAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
             alphaAnimator.setDuration(183);
             alphaAnimator.addUpdateListener((animation) -> {
@@ -2234,8 +2234,8 @@
 
             ObjectAnimator xAnimator =
                     ObjectAnimator.ofFloat(mContainer, "translationX", xOffset, 0f);
-            alphaAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
-            alphaAnimator.setDuration(350);
+            xAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+            xAnimator.setDuration(350);
 
             AnimatorSet animatorSet = new AnimatorSet();
             animatorSet.playTogether(alphaAnimator, xAnimator);
@@ -2247,7 +2247,7 @@
             dismissWithAnimation(() -> {
                 mContainer.setTranslationX(0);
                 ObjectAnimator alphaAnimator =
-                        ObjectAnimator.ofFloat(mContainer, "transitionAlpha", 1f, 0f);
+                        ObjectAnimator.ofFloat(mContainer, "alpha", 1f, 0f);
                 alphaAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
                 alphaAnimator.setDuration(233);
                 alphaAnimator.addUpdateListener((animation) -> {
@@ -2261,8 +2261,8 @@
                 float xOffset = mGlobalActionsLayout.getAnimationOffsetX();
                 ObjectAnimator xAnimator =
                         ObjectAnimator.ofFloat(mContainer, "translationX", 0f, xOffset);
-                alphaAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
-                alphaAnimator.setDuration(350);
+                xAnimator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+                xAnimator.setDuration(350);
 
                 AnimatorSet animatorSet = new AnimatorSet();
                 animatorSet.playTogether(alphaAnimator, xAnimator);