Merge "Controls UI - Stateless controls" into rvc-dev am: dbc90fad28 am: 02ac81a897

Change-Id: I4260a01e0082987fc6da0e8f8afd8fda96ba0e0f
This commit is contained in:
Automerger Merge Worker
2020-03-10 16:58:55 +00:00
17 changed files with 212 additions and 89 deletions

View File

@@ -16,10 +16,12 @@
package com.android.systemui.controls
import android.content.ComponentName
import android.service.controls.Control
data class ControlStatus(
val control: Control,
val component: ComponentName,
var favorite: Boolean,
val removed: Boolean = false
)
)

View File

@@ -238,7 +238,11 @@ class ControlsControllerImpl @Inject constructor (
}
val removed = findRemoved(favoritesForComponentKeys.toSet(), controls)
val controlsWithFavorite = controls.map {
ControlStatus(it, it.controlId in favoritesForComponentKeys)
ControlStatus(
it,
componentName,
it.controlId in favoritesForComponentKeys
)
}
val loadData = createLoadDataObject(
Favorites.getControlsForComponent(componentName)
@@ -290,7 +294,7 @@ class ControlsControllerImpl @Inject constructor (
.setTitle(controlInfo.controlTitle)
.setDeviceType(controlInfo.deviceType)
.build()
return ControlStatus(control, true, setRemoved)
return ControlStatus(control, componentName, true, setRemoved)
}
private fun findRemoved(favoriteKeys: Set<String>, list: List<Control>): Set<String> {

View File

@@ -16,8 +16,8 @@
package com.android.systemui.controls.management
import android.content.ComponentName
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.service.controls.DeviceTypes
import android.view.LayoutInflater
import android.view.View
@@ -147,7 +147,7 @@ private class ControlHolder(view: View, val favoriteCallback: ModelFavoriteChang
override fun bindData(wrapper: ElementWrapper) {
wrapper as ControlWrapper
val data = wrapper.controlStatus
val renderInfo = getRenderInfo(data.control.deviceType)
val renderInfo = getRenderInfo(data.component, data.control.deviceType)
title.text = data.control.title
subtitle.text = data.control.subtitle
favorite.isChecked = data.favorite
@@ -160,16 +160,17 @@ private class ControlHolder(view: View, val favoriteCallback: ModelFavoriteChang
}
private fun getRenderInfo(
component: ComponentName,
@DeviceTypes.DeviceType deviceType: Int
): RenderInfo {
return RenderInfo.lookup(deviceType, true)
return RenderInfo.lookup(itemView.context, component, deviceType, true)
}
private fun applyRenderInfo(ri: RenderInfo) {
val context = itemView.context
val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme())
icon.setImageIcon(Icon.createWithResource(context, ri.iconResourceId))
icon.setImageDrawable(ri.icon)
icon.setImageTintList(fg)
}
}
@@ -191,4 +192,4 @@ class MarginItemDecorator(
right = sideMargins
}
}
}
}

View File

@@ -21,7 +21,6 @@ import android.app.Dialog
import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.graphics.drawable.Icon
import android.os.Bundle
import android.os.UserHandle
import android.service.controls.Control
@@ -137,11 +136,10 @@ class ControlsRequestDialog @Inject constructor(
}
fun createDialog(label: CharSequence): Dialog {
val renderInfo = RenderInfo.lookup(control.deviceType, true)
val renderInfo = RenderInfo.lookup(this, component, control.deviceType, true)
val frame = LayoutInflater.from(this).inflate(R.layout.controls_dialog, null).apply {
requireViewById<ImageView>(R.id.icon).apply {
setImageIcon(Icon.createWithResource(context, renderInfo.iconResourceId))
setImageDrawable(renderInfo.icon)
setImageTintList(
context.resources.getColorStateList(renderInfo.foreground, context.theme))
}
@@ -176,4 +174,4 @@ class ControlsRequestDialog @Inject constructor(
}
finish()
}
}
}

View File

@@ -20,6 +20,7 @@ import android.app.PendingIntent
import android.content.Intent
import android.provider.Settings
import android.service.controls.actions.BooleanAction
import android.service.controls.actions.CommandAction
import android.util.Log
import android.view.HapticFeedbackConstants
@@ -36,6 +37,10 @@ object ControlActionCoordinator {
cvh.clipLayer.setLevel(nextLevel)
}
fun touch(cvh: ControlViewHolder, templateId: String) {
cvh.action(CommandAction(templateId))
}
fun longPress(cvh: ControlViewHolder) {
// Long press snould only be called when there is valid control state, otherwise ignore
cvh.cws.control?.let {

View File

@@ -19,11 +19,11 @@ package com.android.systemui.controls.ui
import android.content.Context
import android.graphics.BlendMode
import android.graphics.drawable.ClipDrawable
import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable
import android.service.controls.Control
import android.service.controls.actions.ControlAction
import android.service.controls.templates.ControlTemplate
import android.service.controls.templates.StatelessTemplate
import android.service.controls.templates.TemperatureControlTemplate
import android.service.controls.templates.ToggleRangeTemplate
import android.service.controls.templates.ToggleTemplate
@@ -122,19 +122,25 @@ class ControlViewHolder(
return when {
status == Control.STATUS_UNKNOWN -> UnknownBehavior::class
template is ToggleTemplate -> ToggleBehavior::class
template is StatelessTemplate -> TouchBehavior::class
template is ToggleRangeTemplate -> ToggleRangeBehavior::class
template is TemperatureControlTemplate -> TemperatureControlBehavior::class
else -> DefaultBehavior::class
}
}
internal fun applyRenderInfo(ri: RenderInfo) {
internal fun applyRenderInfo(enabled: Boolean, offset: Int = 0) {
setEnabled(enabled)
val deviceType = cws.control?.let { it.getDeviceType() } ?: cws.ci.deviceType
val ri = RenderInfo.lookup(context, cws.componentName, deviceType, enabled, offset)
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.setImageDrawable(ri.icon)
icon.setImageTintList(fg)
clipLayer.getDrawable().apply {
@@ -143,7 +149,7 @@ class ControlViewHolder(
}
}
fun setEnabled(enabled: Boolean) {
private fun setEnabled(enabled: Boolean) {
status.setEnabled(enabled)
icon.setEnabled(enabled)
}

View File

@@ -188,36 +188,23 @@ class ControlsUiControllerImpl @Inject constructor (
parent.removeAllViews()
controlViewsById.clear()
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.controls_with_favorites, parent, true)
createListView()
createDropDown(items)
}
val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup
var lastRow: ViewGroup = createRow(inflater, listView)
selectedStructure.controls.forEach {
if (lastRow.getChildCount() == 2) {
lastRow = createRow(inflater, listView)
}
val item = inflater.inflate(
R.layout.controls_base_item, lastRow, false) as ViewGroup
lastRow.addView(item)
val cvh = ControlViewHolder(item, controlsController.get(), uiExecutor, bgExecutor)
val key = ControlKey(selectedStructure.componentName, it.controlId)
cvh.bindData(controlsById.getValue(key))
controlViewsById.put(key, cvh)
}
// add spacer if necessary to keep control size consistent
if ((selectedStructure.controls.size % 2) == 1) {
lastRow.addView(Space(context), LinearLayout.LayoutParams(0, 0, 1f))
private fun createDropDown(items: List<SelectionItem>) {
items.forEach {
RenderInfo.registerComponentIcon(it.componentName, it.icon)
}
val itemsByComponent = items.associateBy { it.componentName }
var adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
val listItems = allStructures.mapNotNull {
itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
}
val itemsWithStructure = allStructures.mapNotNull {
itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
}
val selectionItem = findSelectionItem(selectedStructure, itemsWithStructure) ?: items[0]
addAll(listItems + addControlsItem)
var adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
addAll(itemsWithStructure + addControlsItem)
}
/*
@@ -225,16 +212,15 @@ class ControlsUiControllerImpl @Inject constructor (
* for this dialog. Use a textView with the ListPopupWindow to achieve
* a similar effect
*/
val item = adapter.findSelectionItem(selectedStructure) ?: adapter.getItem(0)
parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
setText(item.getTitle())
setText(selectionItem.getTitle())
// override the default color on the dropdown drawable
(getBackground() as LayerDrawable).getDrawable(1)
.setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
}
parent.requireViewById<ImageView>(R.id.app_icon).apply {
setContentDescription(item.getTitle())
setImageDrawable(item.icon)
setContentDescription(selectionItem.getTitle())
setImageDrawable(selectionItem.icon)
}
val anchor = parent.requireViewById<ViewGroup>(R.id.controls_header)
anchor.setOnClickListener(object : View.OnClickListener {
@@ -272,6 +258,36 @@ class ControlsUiControllerImpl @Inject constructor (
})
}
private fun createListView() {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.controls_with_favorites, parent, true)
val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup
var lastRow: ViewGroup = createRow(inflater, listView)
selectedStructure.controls.forEach {
if (lastRow.getChildCount() == 2) {
lastRow = createRow(inflater, listView)
}
val baseLayout = inflater.inflate(
R.layout.controls_base_item, lastRow, false) as ViewGroup
lastRow.addView(baseLayout)
val cvh = ControlViewHolder(
baseLayout,
controlsController.get(),
uiExecutor,
bgExecutor
)
val key = ControlKey(selectedStructure.componentName, it.controlId)
cvh.bindData(controlsById.getValue(key))
controlViewsById.put(key, cvh)
}
// add spacer if necessary to keep control size consistent
if ((selectedStructure.controls.size % 2) == 1) {
lastRow.addView(Space(context), LinearLayout.LayoutParams(0, 0, 1f))
}
}
private fun loadPreference(structures: List<StructureInfo>): StructureInfo {
if (structures.isEmpty()) return EMPTY_STRUCTURE
@@ -320,6 +336,8 @@ class ControlsUiControllerImpl @Inject constructor (
controlsById.clear()
controlViewsById.clear()
controlsListingController.get().removeCallback(listingCallback)
RenderInfo.clearCache()
}
override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
@@ -358,6 +376,11 @@ class ControlsUiControllerImpl @Inject constructor (
listView.addView(row)
return row
}
private fun findSelectionItem(si: StructureInfo, items: List<SelectionItem>): SelectionItem? =
items.firstOrNull {
it.componentName == si.componentName && it.structure == si.structure
}
}
private data class SelectionItem(
@@ -388,17 +411,4 @@ private class ItemAdapter(
}
return view
}
fun findSelectionItem(si: StructureInfo): SelectionItem? {
var i = 0
while (i < getCount()) {
val item = getItem(i)
if (item.componentName == si.componentName &&
item.structure == si.structure) {
return item
}
i++
}
return null
}
}

View File

@@ -25,7 +25,6 @@ class DefaultBehavior : Behavior {
override fun bind(cws: ControlWithState) {
cvh.status.setText(cws.control?.getStatusText() ?: "")
cvh.setEnabled(false)
cvh.applyRenderInfo(RenderInfo.lookup(cws.ci.deviceType, false))
cvh.applyRenderInfo(false)
}
}

View File

@@ -16,8 +16,14 @@
package com.android.systemui.controls.ui
import android.annotation.MainThread
import android.content.ComponentName
import android.content.Context
import android.graphics.drawable.Drawable
import android.service.controls.DeviceTypes
import android.service.controls.templates.TemperatureControlTemplate
import android.util.ArrayMap
import android.util.SparseArray
import com.android.systemui.R
@@ -31,18 +37,54 @@ data class IconState(val disabledResourceId: Int, val enabledResourceId: Int) {
}
}
data class RenderInfo(val iconResourceId: Int, val foreground: Int, val background: Int) {
data class RenderInfo(val icon: Drawable, val foreground: Int, val background: Int) {
companion object {
fun lookup(deviceType: Int, enabled: Boolean): RenderInfo {
val iconState = deviceIconMap.getValue(deviceType)
const val APP_ICON_ID = -1
private val iconMap = SparseArray<Drawable>()
private val appIconMap = ArrayMap<ComponentName, Drawable>()
@MainThread
fun lookup(
context: Context,
componentName: ComponentName,
deviceType: Int,
enabled: Boolean,
offset: Int = 0
): RenderInfo {
val (fg, bg) = deviceColorMap.getValue(deviceType)
return RenderInfo(iconState[enabled], fg, bg)
val iconKey = if (offset > 0) {
deviceType * BUCKET_SIZE + offset
} else deviceType
val iconState = deviceIconMap.getValue(iconKey)
val resourceId = iconState[enabled]
var icon: Drawable? = null
if (resourceId == APP_ICON_ID) {
icon = appIconMap.get(componentName)
if (icon == null) {
icon = context.resources
.getDrawable(R.drawable.ic_device_unknown_gm2_24px, null)
appIconMap.put(componentName, icon)
}
} else {
icon = iconMap.get(resourceId)
if (icon == null) {
icon = context.resources.getDrawable(resourceId, null)
iconMap.put(resourceId, icon)
}
}
return RenderInfo(icon!!, fg, bg)
}
fun lookup(deviceType: Int, offset: Int, enabled: Boolean): RenderInfo {
val key = deviceType * BUCKET_SIZE + offset
return lookup(key, enabled)
fun registerComponentIcon(componentName: ComponentName, icon: Drawable) {
appIconMap.put(componentName, icon)
}
fun clearCache() {
iconMap.clear()
appIconMap.clear()
}
}
}
@@ -116,6 +158,10 @@ private val deviceIconMap = mapOf<Int, IconState>(
DeviceTypes.TYPE_MOP to IconState(
R.drawable.ic_vacuum_gm2_24px,
R.drawable.ic_vacuum_gm2_24px
),
DeviceTypes.TYPE_ROUTINE to IconState(
RenderInfo.APP_ICON_ID,
RenderInfo.APP_ICON_ID
)
).withDefault {
IconState(

View File

@@ -47,10 +47,7 @@ class TemperatureControlBehavior : Behavior {
val activeMode = template.getCurrentActiveMode()
val enabled = activeMode != 0 && activeMode != TemperatureControlTemplate.MODE_OFF
val deviceType = control.getDeviceType()
clipLayer.setLevel(if (enabled) MAX_LEVEL else MIN_LEVEL)
cvh.setEnabled(enabled)
cvh.applyRenderInfo(RenderInfo.lookup(deviceType, activeMode, enabled))
cvh.applyRenderInfo(enabled, activeMode)
}
}

View File

@@ -34,7 +34,7 @@ class ToggleBehavior : Behavior {
override fun initialize(cvh: ControlViewHolder) {
this.cvh = cvh
cvh.setEnabled(false)
cvh.applyRenderInfo(false)
cvh.layout.setOnClickListener(View.OnClickListener() {
ControlActionCoordinator.toggle(cvh, template.getTemplateId(), template.isChecked())
@@ -51,10 +51,7 @@ class ToggleBehavior : Behavior {
clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
val checked = template.isChecked()
val deviceType = control.getDeviceType()
clipLayer.setLevel(if (checked) MAX_LEVEL else MIN_LEVEL)
cvh.setEnabled(checked)
cvh.applyRenderInfo(RenderInfo.lookup(deviceType, checked))
cvh.applyRenderInfo(checked)
}
}

View File

@@ -56,7 +56,7 @@ class ToggleRangeBehavior : Behavior {
status = cvh.status
context = status.getContext()
cvh.setEnabled(false)
cvh.applyRenderInfo(false)
val gestureListener = ToggleRangeGestureListener(cvh.layout)
val gestureDetector = GestureDetector(context, gestureListener)
@@ -89,14 +89,11 @@ class ToggleRangeBehavior : Behavior {
rangeTemplate = template.getRange()
val checked = template.isChecked()
val deviceType = control.getDeviceType()
val currentRatio = rangeTemplate.getCurrentValue() /
(rangeTemplate.getMaxValue() - rangeTemplate.getMinValue())
updateRange(currentRatio, checked)
cvh.setEnabled(checked)
cvh.applyRenderInfo(RenderInfo.lookup(deviceType, checked))
cvh.applyRenderInfo(checked)
}
fun beginUpdateRange() {

View File

@@ -0,0 +1,58 @@
/*
* 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.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.view.View
import android.service.controls.Control
import android.service.controls.templates.StatelessTemplate
import com.android.systemui.R
import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL
/**
* Supports touch events, but has no notion of state as the {@link ToggleBehavior} does. Must be
* used with {@link StatelessTemplate}.
*/
class TouchBehavior : Behavior {
lateinit var clipLayer: Drawable
lateinit var template: StatelessTemplate
lateinit var control: Control
lateinit var cvh: ControlViewHolder
override fun initialize(cvh: ControlViewHolder) {
this.cvh = cvh
cvh.applyRenderInfo(false)
cvh.layout.setOnClickListener(View.OnClickListener() {
ControlActionCoordinator.touch(cvh, template.getTemplateId())
})
}
override fun bind(cws: ControlWithState) {
this.control = cws.control!!
cvh.status.setText(control.getStatusText())
template = control.getControlTemplate() as StatelessTemplate
val ld = cvh.layout.getBackground() as LayerDrawable
clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
clipLayer.setLevel(MIN_LEVEL)
cvh.applyRenderInfo(false)
}
}

View File

@@ -25,7 +25,6 @@ class UnknownBehavior : Behavior {
override fun bind(cws: ControlWithState) {
cvh.status.setText(cvh.context.getString(com.android.internal.R.string.loading))
cvh.setEnabled(false)
cvh.applyRenderInfo(RenderInfo.lookup(cws.ci.deviceType, false))
cvh.applyRenderInfo(false)
}
}

View File

@@ -234,7 +234,7 @@ class ControlsControllerImplTest : SysuiTestCase() {
loaded = true
assertEquals(1, controls.size)
val controlStatus = controls[0]
assertEquals(ControlStatus(control, false), controlStatus)
assertEquals(ControlStatus(control, TEST_COMPONENT, false), controlStatus)
assertTrue(favorites.isEmpty())
assertFalse(data.errorOnLoad)
@@ -265,10 +265,10 @@ class ControlsControllerImplTest : SysuiTestCase() {
loaded = true
assertEquals(2, controls.size)
val controlStatus = controls.first { it.control.controlId == TEST_CONTROL_ID }
assertEquals(ControlStatus(control, true), controlStatus)
assertEquals(ControlStatus(control, TEST_COMPONENT, true), controlStatus)
val controlStatus2 = controls.first { it.control.controlId == TEST_CONTROL_ID_2 }
assertEquals(ControlStatus(control2, false), controlStatus2)
assertEquals(ControlStatus(control2, TEST_COMPONENT, false), controlStatus2)
assertEquals(1, favorites.size)
assertEquals(TEST_CONTROL_ID, favorites[0])

View File

@@ -17,6 +17,7 @@
package com.android.systemui.controls.management
import android.app.PendingIntent
import android.content.ComponentName
import android.service.controls.Control
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
@@ -78,6 +79,7 @@ class AllModelTest : SysuiTestCase() {
Control.StatelessBuilder("$idPrefix$it", pendingIntent)
.setZone(zoneMap(it))
.build(),
ComponentName("", ""),
it in favoritesIndices
)
}
@@ -189,4 +191,4 @@ class AllModelTest : SysuiTestCase() {
assertTrue(sameControl(it.first, it.second))
}
}
}
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.controls.management
import android.app.PendingIntent
import android.content.ComponentName
import android.service.controls.Control
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
@@ -70,6 +71,7 @@ open class FavoriteModelTest : SysuiTestCase() {
Control.StatelessBuilder("$idPrefix$it", pendingIntent)
.setZone((it % 3).toString())
.build(),
ComponentName("", ""),
it in favoritesIndices
)
}
@@ -195,4 +197,4 @@ class FavoriteModelParameterizedTest(val from: Int, val to: Int) : FavoriteModel
verifyNoMoreInteractions(allAdapter, favoritesAdapter)
}
}
}