blob: 81b5f369856702e233e6cd7c9a579e27d0b92186 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.controls.ui
import android.content.Context
import android.graphics.drawable.ClipDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable
import android.service.controls.Control
import android.service.controls.DeviceTypes
import android.service.controls.actions.BooleanAction
import android.service.controls.actions.ControlAction
import android.service.controls.actions.FloatAction
import android.service.controls.templates.ControlTemplate
import android.service.controls.templates.RangeTemplate
import android.service.controls.templates.ToggleRangeTemplate
import android.service.controls.templates.ToggleTemplate
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.R
private const val MIN_LEVEL = 0
private const val MAX_LEVEL = 10000
class ControlViewHolder(
val layout: ViewGroup,
val controlsController: ControlsController
) {
val icon: ImageView = layout.requireViewById(R.id.icon)
val status: TextView = layout.requireViewById(R.id.status)
val statusExtra: TextView = layout.requireViewById(R.id.status_extra)
val title: TextView = layout.requireViewById(R.id.title)
val subtitle: TextView = layout.requireViewById(R.id.subtitle)
val context: Context = layout.getContext()
val clipLayer: ClipDrawable
val gd: GradientDrawable
lateinit var cws: ControlWithState
init {
val ld = layout.getBackground() as LayerDrawable
ld.mutate()
clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) as ClipDrawable
gd = clipLayer.getDrawable() as GradientDrawable
}
fun bindData(cws: ControlWithState) {
this.cws = cws
val (status, template) = cws.control?.let {
title.setText(it.getTitle())
subtitle.setText(it.getSubtitle())
Pair(it.getStatus(), it.getControlTemplate())
} ?: run {
title.setText(cws.ci.controlTitle)
subtitle.setText("")
Pair(Control.STATUS_UNKNOWN, ControlTemplate.NO_TEMPLATE)
}
findBehavior(status, template).apply(this, cws)
}
fun action(action: ControlAction) {
controlsController.action(cws.ci, action)
}
private fun findBehavior(status: Int, template: ControlTemplate): Behavior {
return when {
status == Control.STATUS_UNKNOWN -> UnknownBehavior()
template is ToggleTemplate -> ToggleTemplateBehavior()
template is ToggleRangeTemplate -> ToggleRangeTemplateBehavior()
else -> {
object : Behavior {
override fun apply(cvh: ControlViewHolder, cws: ControlWithState) {
cvh.status.setText(cws.control?.getStatusText())
cvh.applyRenderInfo(findRenderInfo(cws.ci.deviceType, false))
}
}
}
}
}
internal fun applyRenderInfo(ri: RenderInfo) {
val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
val bg = context.getResources().getColorStateList(ri.background, context.getTheme())
status.setTextColor(fg)
statusExtra.setTextColor(fg)
icon.setImageIcon(Icon.createWithResource(context, ri.iconResourceId))
icon.setImageTintList(fg)
gd.setColor(bg)
}
fun setEnabled(enabled: Boolean) {
status.setEnabled(enabled)
icon.setEnabled(enabled)
}
}
private interface Behavior {
fun apply(cvh: ControlViewHolder, cws: ControlWithState)
fun findRenderInfo(deviceType: Int, isActive: Boolean): RenderInfo =
deviceRenderMap.getOrDefault(deviceType, unknownDeviceMap).getValue(isActive)
}
private class UnknownBehavior : Behavior {
override fun apply(cvh: ControlViewHolder, cws: ControlWithState) {
cvh.status.setText("Loading...")
cvh.applyRenderInfo(findRenderInfo(cws.ci.deviceType, false))
}
}
private class ToggleRangeTemplateBehavior : Behavior {
lateinit var clipLayer: Drawable
lateinit var template: ToggleRangeTemplate
lateinit var control: Control
lateinit var cvh: ControlViewHolder
lateinit var rangeTemplate: RangeTemplate
lateinit var statusExtra: TextView
lateinit var status: TextView
lateinit var context: Context
override fun apply(cvh: ControlViewHolder, cws: ControlWithState) {
this.control = cws.control!!
this.cvh = cvh
statusExtra = cvh.statusExtra
status = cvh.status
status.setText(control.getStatusText())
context = status.getContext()
cvh.layout.setOnTouchListener(ToggleRangeTouchListener())
val ld = cvh.layout.getBackground() as LayerDrawable
clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
template = control.getControlTemplate() as ToggleRangeTemplate
rangeTemplate = template.getRange()
val checked = template.isChecked()
val deviceType = control.getDeviceType()
updateRange((rangeTemplate.getCurrentValue() / 100.0f), checked)
cvh.setEnabled(checked)
cvh.applyRenderInfo(findRenderInfo(deviceType, checked))
}
fun toggle() {
cvh.action(BooleanAction(template.getTemplateId(), !template.isChecked()))
val nextLevel = if (template.isChecked()) MIN_LEVEL else MAX_LEVEL
clipLayer.setLevel(nextLevel)
}
fun beginUpdateRange() {
status.setVisibility(View.GONE)
statusExtra.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
.getDimensionPixelSize(R.dimen.control_status_expanded).toFloat())
}
fun updateRange(f: Float, checked: Boolean) {
clipLayer.setLevel(if (checked) (MAX_LEVEL * f).toInt() else MIN_LEVEL)
if (checked && f < 100.0f && f > 0.0f) {
statusExtra.setText("" + (f * 100.0).toInt() + "%")
statusExtra.setVisibility(View.VISIBLE)
} else {
statusExtra.setText("")
statusExtra.setVisibility(View.GONE)
}
}
fun endUpdateRange(f: Float) {
statusExtra.setText(" - " + (f * 100.0).toInt() + "%")
val newValue = rangeTemplate.getMinValue() +
(f * (rangeTemplate.getMaxValue() - rangeTemplate.getMinValue()))
statusExtra.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
.getDimensionPixelSize(R.dimen.control_status_normal).toFloat())
status.setVisibility(View.VISIBLE)
cvh.action(FloatAction(rangeTemplate.getTemplateId(), findNearestStep(newValue)))
}
fun findNearestStep(value: Float): Float {
var minDiff = 1000f
var f = rangeTemplate.getMinValue()
while (f <= rangeTemplate.getMaxValue()) {
val currentDiff = Math.abs(value - f)
if (currentDiff < minDiff) {
minDiff = currentDiff
} else {
return f - rangeTemplate.getStepValue()
}
f += rangeTemplate.getStepValue()
}
return rangeTemplate.getMaxValue()
}
inner class ToggleRangeTouchListener() : View.OnTouchListener {
private var initialTouchX: Float = 0.0f
private var initialTouchY: Float = 0.0f
private var isDragging: Boolean = false
private val minDragDiff = 20
override fun onTouch(v: View, e: MotionEvent): Boolean {
when (e.getActionMasked()) {
MotionEvent.ACTION_DOWN -> setupTouch(e)
MotionEvent.ACTION_MOVE -> detectDrag(v, e)
MotionEvent.ACTION_UP -> endTouch(v, e)
}
return true
}
private fun setupTouch(e: MotionEvent) {
initialTouchX = e.getX()
initialTouchY = e.getY()
}
private fun detectDrag(v: View, e: MotionEvent) {
val xDiff = Math.abs(e.getX() - initialTouchX)
val yDiff = Math.abs(e.getY() - initialTouchY)
if (xDiff < minDragDiff) {
isDragging = false
} else {
if (!isDragging) {
this@ToggleRangeTemplateBehavior.beginUpdateRange()
}
v.getParent().requestDisallowInterceptTouchEvent(true)
isDragging = true
if (yDiff > xDiff) {
endTouch(v, e)
} else {
val percent = Math.max(0.0f, Math.min(1.0f, e.getX() / v.getWidth()))
this@ToggleRangeTemplateBehavior.updateRange(percent, true)
}
}
}
private fun endTouch(v: View, e: MotionEvent) {
if (!isDragging) {
this@ToggleRangeTemplateBehavior.toggle()
} else {
val percent = Math.max(0.0f, Math.min(1.0f, e.getX() / v.getWidth()))
this@ToggleRangeTemplateBehavior.endUpdateRange(percent)
}
initialTouchX = 0.0f
initialTouchY = 0.0f
isDragging = false
}
}
}
private class ToggleTemplateBehavior : Behavior {
lateinit var clipLayer: Drawable
lateinit var template: ToggleTemplate
lateinit var control: Control
lateinit var cvh: ControlViewHolder
lateinit var context: Context
lateinit var status: TextView
override fun apply(cvh: ControlViewHolder, cws: ControlWithState) {
this.control = cws.control!!
this.cvh = cvh
status = cvh.status
status.setText(control.getStatusText())
cvh.layout.setOnClickListener(View.OnClickListener() { toggle() })
val ld = cvh.layout.getBackground() as LayerDrawable
clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
template = control.getControlTemplate() as ToggleTemplate
val checked = template.isChecked()
val deviceType = control.getDeviceType()
clipLayer.setLevel(if (checked) MAX_LEVEL else MIN_LEVEL)
cvh.setEnabled(checked)
cvh.applyRenderInfo(findRenderInfo(deviceType, checked))
}
fun toggle() {
cvh.action(BooleanAction(template.getTemplateId(), !template.isChecked()))
val nextLevel = if (template.isChecked()) MIN_LEVEL else MAX_LEVEL
clipLayer.setLevel(nextLevel)
}
}
internal data class RenderInfo(val iconResourceId: Int, val foreground: Int, val background: Int)
private val unknownDeviceMap = mapOf(
false to RenderInfo(
R.drawable.ic_light_off_gm2_24px,
R.color.unknown_foreground,
R.color.unknown_foreground),
true to RenderInfo(
R.drawable.ic_lightbulb_outline_gm2_24px,
R.color.unknown_foreground,
R.color.unknown_foreground)
)
private val deviceRenderMap = mapOf<Int, Map<Boolean, RenderInfo>>(
DeviceTypes.TYPE_UNKNOWN to unknownDeviceMap,
DeviceTypes.TYPE_LIGHT to mapOf(
false to RenderInfo(
R.drawable.ic_light_off_gm2_24px,
R.color.light_foreground,
R.color.light_background),
true to RenderInfo(
R.drawable.ic_lightbulb_outline_gm2_24px,
R.color.light_foreground,
R.color.light_background)
),
DeviceTypes.TYPE_THERMOSTAT to mapOf(
false to RenderInfo(
R.drawable.ic_device_thermostat_gm2_24px,
R.color.light_foreground,
R.color.light_background),
true to RenderInfo(
R.drawable.ic_device_thermostat_gm2_24px,
R.color.light_foreground,
R.color.light_background)
),
DeviceTypes.TYPE_CAMERA to mapOf(
false to RenderInfo(
R.drawable.ic_videocam_gm2_24px,
R.color.light_foreground,
R.color.light_background),
true to RenderInfo(
R.drawable.ic_videocam_gm2_24px,
R.color.light_foreground,
R.color.light_background)
),
DeviceTypes.TYPE_LOCK to mapOf(
false to RenderInfo(
R.drawable.ic_lock_open_gm2_24px,
R.color.lock_foreground,
R.color.lock_background),
true to RenderInfo(
R.drawable.ic_lock_gm2_24px,
R.color.lock_foreground,
R.color.lock_background)
),
DeviceTypes.TYPE_SWITCH to mapOf(
false to RenderInfo(
R.drawable.ic_switches_gm2_24px,
R.color.lock_foreground,
R.color.lock_background),
true to RenderInfo(
R.drawable.ic_switches_gm2_24px,
R.color.lock_foreground,
R.color.lock_background)
),
DeviceTypes.TYPE_OUTLET to mapOf(
false to RenderInfo(
R.drawable.ic_power_off_gm2_24px,
R.color.lock_foreground,
R.color.lock_background),
true to RenderInfo(
R.drawable.ic_power_gm2_24px,
R.color.lock_foreground,
R.color.lock_background)
)
)