Controls UI - Support RangeTemplate and TemperatureControlTemplate

Support RangeTemplate within ToggleRangeSupport by removing toggle
functionality. Add back the ability to drag on a device that is off,
as the recently defined behavior specifies that it should turn the
device on. Fully support sub templates of TemperatureControlTemplate.

Bug: 155494213
Test: mock app, use AC_UNIT and BLINDS
Change-Id: Ifd8ee67956325358de7b035dfce27faab9fe49ce
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/Behavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/Behavior.kt
index 275c778..842c39b 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/Behavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/Behavior.kt
@@ -30,6 +30,9 @@
 
     /**
      * Will be invoked on every update provided to the Control
+     *
+     * @param cws ControlWithState, as loaded from favorites and/or the application
+     * @param colorOffset An additional flag to control rendering color. See [RenderInfo]
      */
-    fun bind(cws: ControlWithState)
+    fun bind(cws: ControlWithState, colorOffset: Int = 0)
 }
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 2653ce0..8dc3971 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
@@ -28,6 +28,7 @@
 import android.service.controls.DeviceTypes
 import android.service.controls.actions.ControlAction
 import android.service.controls.templates.ControlTemplate
+import android.service.controls.templates.RangeTemplate
 import android.service.controls.templates.StatelessTemplate
 import android.service.controls.templates.TemperatureControlTemplate
 import android.service.controls.templates.ToggleRangeTemplate
@@ -69,6 +70,25 @@
 
         const val MIN_LEVEL = 0
         const val MAX_LEVEL = 10000
+
+        fun findBehaviorClass(
+            status: Int,
+            template: ControlTemplate,
+            deviceType: Int
+        ): KClass<out Behavior> {
+            return when {
+                status == Control.STATUS_UNKNOWN -> StatusBehavior::class
+                status == Control.STATUS_ERROR -> StatusBehavior::class
+                status == Control.STATUS_NOT_FOUND -> StatusBehavior::class
+                deviceType == DeviceTypes.TYPE_CAMERA -> TouchBehavior::class
+                template is ToggleTemplate -> ToggleBehavior::class
+                template is StatelessTemplate -> TouchBehavior::class
+                template is ToggleRangeTemplate -> ToggleRangeBehavior::class
+                template is RangeTemplate -> ToggleRangeBehavior::class
+                template is TemperatureControlTemplate -> TemperatureControlBehavior::class
+                else -> DefaultBehavior::class
+            }
+        }
     }
 
     private val toggleBackgroundIntensity: Float = layout.context.resources
@@ -123,18 +143,7 @@
             })
         }
 
-        val clazz = findBehavior(controlStatus, template, deviceType)
-        if (behavior == null || behavior!!::class != clazz) {
-            // Behavior changes can signal a change in template from the app or
-            // first time setup
-            behavior = clazz.java.newInstance()
-            behavior?.initialize(this)
-
-            // let behaviors define their own, if necessary, and clear any existing ones
-            layout.setAccessibilityDelegate(null)
-        }
-
-        behavior?.bind(cws)
+        behavior = bindBehavior(behavior, findBehaviorClass(controlStatus, template, deviceType))
 
         layout.setContentDescription("${title.text} ${subtitle.text} ${status.text}")
     }
@@ -190,25 +199,30 @@
 
     fun usePanel(): Boolean = deviceType in ControlViewHolder.FORCE_PANEL_DEVICES
 
-    private fun findBehavior(
-        status: Int,
-        template: ControlTemplate,
-        deviceType: Int
-    ): KClass<out Behavior> {
-        return when {
-            status == Control.STATUS_UNKNOWN -> StatusBehavior::class
-            status == Control.STATUS_ERROR -> StatusBehavior::class
-            status == Control.STATUS_NOT_FOUND -> StatusBehavior::class
-            deviceType == DeviceTypes.TYPE_CAMERA -> TouchBehavior::class
-            template is ToggleTemplate -> ToggleBehavior::class
-            template is StatelessTemplate -> TouchBehavior::class
-            template is ToggleRangeTemplate -> ToggleRangeBehavior::class
-            template is TemperatureControlTemplate -> TemperatureControlBehavior::class
-            else -> DefaultBehavior::class
+    fun bindBehavior(
+        existingBehavior: Behavior?,
+        clazz: KClass<out Behavior>,
+        offset: Int = 0
+    ): Behavior {
+        val behavior = if (existingBehavior == null || existingBehavior!!::class != clazz) {
+            // Behavior changes can signal a change in template from the app or
+            // first time setup
+            val newBehavior = clazz.java.newInstance()
+            newBehavior.initialize(this)
+
+            // let behaviors define their own, if necessary, and clear any existing ones
+            layout.setAccessibilityDelegate(null)
+            newBehavior
+        } else {
+            existingBehavior
+        }
+
+        return behavior.also {
+            it.bind(cws, offset)
         }
     }
 
-    internal fun applyRenderInfo(enabled: Boolean, offset: Int = 0, animated: Boolean = true) {
+    internal fun applyRenderInfo(enabled: Boolean, offset: Int, animated: Boolean = true) {
         setEnabled(enabled)
 
         val ri = RenderInfo.lookup(context, cws.componentName, deviceType, enabled, offset)
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 e850a6a..722ade9 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DefaultBehavior.kt
@@ -23,8 +23,8 @@
         this.cvh = cvh
     }
 
-    override fun bind(cws: ControlWithState) {
+    override fun bind(cws: ControlWithState, colorOffset: Int) {
         cvh.status.setText(cws.control?.getStatusText() ?: "")
-        cvh.applyRenderInfo(false)
+        cvh.applyRenderInfo(false, colorOffset)
     }
 }
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 49c4408..d8dceba 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
@@ -27,7 +27,7 @@
         this.cvh = cvh
     }
 
-    override fun bind(cws: ControlWithState) {
+    override fun bind(cws: ControlWithState, colorOffset: Int) {
         val status = cws.control?.status ?: Control.STATUS_UNKNOWN
         val msg = when (status) {
             Control.STATUS_ERROR -> R.string.controls_error_generic
@@ -35,6 +35,6 @@
             else -> com.android.internal.R.string.loading
         }
         cvh.status.setText(cvh.context.getString(msg))
-        cvh.applyRenderInfo(false)
+        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 b4d0e63..2795c7a 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt
@@ -19,6 +19,7 @@
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
 import android.service.controls.Control
+import android.service.controls.templates.ControlTemplate
 import android.service.controls.templates.TemperatureControlTemplate
 
 import com.android.systemui.R
@@ -29,17 +30,13 @@
     lateinit var clipLayer: Drawable
     lateinit var control: Control
     lateinit var cvh: ControlViewHolder
-    lateinit var template: TemperatureControlTemplate
+    var subBehavior: Behavior? = null
 
     override fun initialize(cvh: ControlViewHolder) {
         this.cvh = cvh
-
-        cvh.layout.setOnClickListener { _ ->
-            cvh.controlActionCoordinator.touch(cvh, template.getTemplateId(), control)
-        }
     }
 
-    override fun bind(cws: ControlWithState) {
+    override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
 
         cvh.status.setText(control.getStatusText())
@@ -47,11 +44,32 @@
         val ld = cvh.layout.getBackground() as LayerDrawable
         clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
 
-        template = control.getControlTemplate() as TemperatureControlTemplate
-
+        val template = control.getControlTemplate() as TemperatureControlTemplate
         val activeMode = template.getCurrentActiveMode()
-        val enabled = activeMode != 0 && activeMode != TemperatureControlTemplate.MODE_OFF
-        clipLayer.setLevel(if (enabled) MAX_LEVEL else MIN_LEVEL)
-        cvh.applyRenderInfo(enabled, activeMode)
+        val subTemplate = template.getTemplate()
+        if (subTemplate == ControlTemplate.getNoTemplateObject() ||
+            subTemplate == ControlTemplate.getErrorTemplate()) {
+            // No sub template is specified, apply a default look with basic touch interaction.
+            // Treat an error as no template.
+            val enabled = activeMode != 0 && activeMode != TemperatureControlTemplate.MODE_OFF
+            clipLayer.setLevel(if (enabled) MAX_LEVEL else MIN_LEVEL)
+            cvh.applyRenderInfo(enabled, activeMode)
+
+            cvh.layout.setOnClickListener { _ ->
+                cvh.controlActionCoordinator.touch(cvh, template.getTemplateId(), control)
+            }
+        } else {
+            // A sub template has been specified, use this as the default behavior for user
+            // interactions (touch, range)
+            subBehavior = cvh.bindBehavior(
+                subBehavior,
+                ControlViewHolder.findBehaviorClass(
+                    control.status,
+                    subTemplate,
+                    control.deviceType
+                ),
+                activeMode
+            )
+        }
     }
 }
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 3e16698..c432c09 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt
@@ -19,7 +19,9 @@
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
 import android.service.controls.Control
+import android.service.controls.templates.TemperatureControlTemplate
 import android.service.controls.templates.ToggleTemplate
+import android.util.Log
 import android.view.View
 import com.android.systemui.R
 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL
@@ -39,17 +41,25 @@
         })
     }
 
-    override fun bind(cws: ControlWithState) {
+    override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
 
         cvh.status.setText(control.getStatusText())
-        template = control.getControlTemplate() as ToggleTemplate
+        val controlTemplate = control.getControlTemplate()
+        template = when (controlTemplate) {
+            is ToggleTemplate -> controlTemplate
+            is TemperatureControlTemplate -> controlTemplate.getTemplate() as ToggleTemplate
+            else -> {
+                Log.e(ControlsUiController.TAG, "Unsupported template type: $controlTemplate")
+                return
+            }
+        }
 
         val ld = cvh.layout.getBackground() as LayerDrawable
         clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
         clipLayer.level = MAX_LEVEL
 
         val checked = template.isChecked()
-        cvh.applyRenderInfo(checked)
+        cvh.applyRenderInfo(checked, colorOffset)
     }
 }
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 3dc0ff3..a09ed09 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt
@@ -25,7 +25,9 @@
 import android.os.Bundle
 import android.service.controls.Control
 import android.service.controls.actions.FloatAction
+import android.service.controls.templates.ControlTemplate
 import android.service.controls.templates.RangeTemplate
+import android.service.controls.templates.TemperatureControlTemplate
 import android.service.controls.templates.ToggleRangeTemplate
 import android.util.Log
 import android.util.MathUtils
@@ -44,10 +46,14 @@
 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL
 import java.util.IllegalFormatException
 
+/**
+ * Supports [ToggleRangeTemplate] and [RangeTemplate], as well as when one of those templates is
+ * defined as the subtemplate in [TemperatureControlTemplate].
+ */
 class ToggleRangeBehavior : Behavior {
     private var rangeAnimator: ValueAnimator? = null
     lateinit var clipLayer: Drawable
-    lateinit var template: ToggleRangeTemplate
+    lateinit var templateId: String
     lateinit var control: Control
     lateinit var cvh: ControlViewHolder
     lateinit var rangeTemplate: RangeTemplate
@@ -55,6 +61,9 @@
     lateinit var context: Context
     var currentStatusText: CharSequence = ""
     var currentRangeValue: String = ""
+    var isChecked: Boolean = false
+    var isToggleable: Boolean = false
+    var colorOffset: Int = 0
 
     companion object {
         private const val DEFAULT_FORMAT = "%.1f"
@@ -65,7 +74,7 @@
         status = cvh.status
         context = status.getContext()
 
-        cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */)
+        cvh.applyRenderInfo(false /* enabled */, colorOffset, false /* animated */)
 
         val gestureListener = ToggleRangeGestureListener(cvh.layout)
         val gestureDetector = GestureDetector(context, gestureListener)
@@ -86,8 +95,40 @@
         }
     }
 
-    override fun bind(cws: ControlWithState) {
+    private fun setup(template: ToggleRangeTemplate) {
+        rangeTemplate = template.getRange()
+        isToggleable = true
+        isChecked = template.isChecked()
+    }
+
+    private fun setup(template: RangeTemplate) {
+        rangeTemplate = template
+
+        // only show disabled state when value is at the minimum
+        isChecked = rangeTemplate.currentValue != rangeTemplate.minValue
+    }
+
+    private fun setupTemplate(template: ControlTemplate): Boolean {
+        return when (template) {
+            is ToggleRangeTemplate -> {
+                setup(template)
+                true
+            }
+            is RangeTemplate -> {
+                setup(template)
+                true
+            }
+            is TemperatureControlTemplate -> setupTemplate(template.getTemplate())
+            else -> {
+                Log.e(ControlsUiController.TAG, "Unsupported template type: $template")
+                false
+            }
+        }
+    }
+
+    override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
+        this.colorOffset = colorOffset
 
         currentStatusText = control.getStatusText()
         status.setText(currentStatusText)
@@ -99,13 +140,14 @@
         val ld = cvh.layout.getBackground() as LayerDrawable
         clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
 
-        template = control.getControlTemplate() as ToggleRangeTemplate
-        rangeTemplate = template.getRange()
+        val template = control.getControlTemplate()
+        if (!setupTemplate(template)) return
+        templateId = template.getTemplateId()
 
-        val checked = template.isChecked()
-        updateRange(rangeToLevelValue(rangeTemplate.currentValue), checked, /* isDragging */ false)
+        updateRange(rangeToLevelValue(rangeTemplate.currentValue), isChecked,
+            /* isDragging */ false)
 
-        cvh.applyRenderInfo(checked)
+        cvh.applyRenderInfo(isChecked, colorOffset)
 
         /*
          * This is custom widget behavior, so add a new accessibility delegate to
@@ -141,9 +183,12 @@
             ): Boolean {
                 val handled = when (action) {
                     AccessibilityNodeInfo.ACTION_CLICK -> {
-                        cvh.controlActionCoordinator.toggle(cvh, template.getTemplateId(),
-                            template.isChecked())
-                        true
+                        if (!isToggleable) {
+                            false
+                        } else {
+                            cvh.controlActionCoordinator.toggle(cvh, templateId, isChecked)
+                            true
+                        }
                     }
                     AccessibilityNodeInfo.ACTION_LONG_CLICK -> {
                         cvh.controlActionCoordinator.longPress(cvh)
@@ -157,7 +202,7 @@
                             val value = arguments.getFloat(
                                 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)
                             val level = rangeToLevelValue(value)
-                            updateRange(level, template.isChecked(), /* isDragging */ true)
+                            updateRange(level, isChecked, /* isDragging */ true)
                             endUpdateRange()
                             true
                         }
@@ -182,7 +227,14 @@
     }
 
     fun updateRange(level: Int, checked: Boolean, isDragging: Boolean) {
-        val newLevel = if (checked) Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, level)) else MIN_LEVEL
+        val newLevel = Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, level))
+
+        // If the current level is at the minimum and the user is dragging, set the control to
+        // the enabled state to indicate their intention to enable the device. This will update
+        // control colors to support dragging.
+        if (clipLayer.level == MIN_LEVEL && newLevel > MIN_LEVEL) {
+            cvh.applyRenderInfo(checked, colorOffset, false /* animated */)
+        }
 
         rangeAnimator?.cancel()
         if (isDragging) {
@@ -282,7 +334,7 @@
             if (isDragging) {
                 return
             }
-            cvh.controlActionCoordinator.longPress(this@ToggleRangeBehavior.cvh)
+            cvh.controlActionCoordinator.longPress(cvh)
         }
 
         override fun onScroll(
@@ -291,26 +343,21 @@
             xDiff: Float,
             yDiff: Float
         ): Boolean {
-            if (!template.isChecked) {
-                return false
-            }
             if (!isDragging) {
                 v.getParent().requestDisallowInterceptTouchEvent(true)
-                this@ToggleRangeBehavior.beginUpdateRange()
+                beginUpdateRange()
                 isDragging = true
             }
 
             val ratioDiff = -xDiff / v.width
             val changeAmount = ((MAX_LEVEL - MIN_LEVEL) * ratioDiff).toInt()
-            this@ToggleRangeBehavior.updateRange(clipLayer.level + changeAmount,
-                    checked = true, isDragging = true)
+            updateRange(clipLayer.level + changeAmount, checked = true, isDragging = true)
             return true
         }
 
         override fun onSingleTapUp(e: MotionEvent): Boolean {
-            val th = this@ToggleRangeBehavior
-            cvh.controlActionCoordinator.toggle(th.cvh, th.template.getTemplateId(),
-                    th.template.isChecked())
+            if (!isToggleable) return false
+            cvh.controlActionCoordinator.toggle(cvh, templateId, isChecked)
             return true
         }
     }
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 7ae3df7..8ce2e61 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt
@@ -44,7 +44,7 @@
         })
     }
 
-    override fun bind(cws: ControlWithState) {
+    override fun bind(cws: ControlWithState, colorOffset: Int) {
         this.control = cws.control!!
         cvh.status.setText(control.getStatusText())
         template = control.getControlTemplate()
@@ -53,6 +53,6 @@
         clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
         clipLayer.setLevel(MIN_LEVEL)
 
-        cvh.applyRenderInfo(false)
+        cvh.applyRenderInfo(false, colorOffset)
     }
 }