From 22231794990bdaf0caec854c56968b0b497a05a4 Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Thu, 23 Jan 2020 09:51:09 -0500 Subject: [PATCH] Controls UI - Basic impl The start of basic controls implementation. Support for lights, locks, and basic thermostat information. Token support is needed only for mocking at the moment and will be removed. Bug: 148207527 Test: SystemUiTests Change-Id: If25c57735ebd314a978d3969934d781eafc4f89c --- .../service/controls/TokenProvider.aidl | 7 + .../SystemUI/res/color/light_background.xml | 6 + .../SystemUI/res/color/light_foreground.xml | 6 + .../SystemUI/res/color/lock_background.xml | 6 + .../SystemUI/res/color/lock_foreground.xml | 6 + .../SystemUI/res/color/unknown_foreground.xml | 6 + .../res/drawable/control_background.xml | 32 ++ .../SystemUI/res/drawable/control_layer.xml | 22 + .../control_no_favorites_background.xml | 22 + .../ic_device_thermostat_gm2_24px.xml | 9 + .../res/drawable/ic_light_off_gm2_24px.xml | 19 + .../res/drawable/ic_lock_gm2_24px.xml | 9 + .../res/drawable/ic_lock_open_gm2_24px.xml | 12 + .../SystemUI/res/drawable/ic_more_vert.xml | 24 ++ .../res/drawable/ic_power_gm2_24px.xml | 9 + .../res/drawable/ic_power_off_gm2_24px.xml | 9 + .../ic_power_settings_new_gm2_24px.xml | 9 + .../res/drawable/ic_switches_gm2_24px.xml | 9 + .../res/drawable/ic_vacuum_gm2_24px.xml | 9 + .../res/drawable/ic_videocam_gm2_24px.xml | 9 + .../res/layout/controls_base_item.xml | 79 ++++ .../res/layout/controls_no_favorites.xml | 18 + packages/SystemUI/res/layout/controls_row.xml | 22 + .../res/layout/controls_with_favorites.xml | 35 ++ .../res/layout/global_actions_grid_v2.xml | 15 +- packages/SystemUI/res/values/colors.xml | 12 +- packages/SystemUI/res/values/dimens.xml | 7 + packages/SystemUI/res/values/strings.xml | 3 + .../systemui/controls/ui/ControlViewHolder.kt | 400 ++++++++++++++++++ .../systemui/controls/ui/ControlWithState.kt | 31 ++ .../controls/ui/ControlsUiController.kt | 5 +- .../controls/ui/ControlsUiControllerImpl.kt | 191 ++++++++- .../globalactions/GlobalActionsDialog.java | 28 +- 33 files changed, 1060 insertions(+), 26 deletions(-) create mode 100644 core/java/android/service/controls/TokenProvider.aidl create mode 100644 packages/SystemUI/res/color/light_background.xml create mode 100644 packages/SystemUI/res/color/light_foreground.xml create mode 100644 packages/SystemUI/res/color/lock_background.xml create mode 100644 packages/SystemUI/res/color/lock_foreground.xml create mode 100644 packages/SystemUI/res/color/unknown_foreground.xml create mode 100644 packages/SystemUI/res/drawable/control_background.xml create mode 100644 packages/SystemUI/res/drawable/control_layer.xml create mode 100644 packages/SystemUI/res/drawable/control_no_favorites_background.xml create mode 100644 packages/SystemUI/res/drawable/ic_device_thermostat_gm2_24px.xml create mode 100644 packages/SystemUI/res/drawable/ic_light_off_gm2_24px.xml create mode 100644 packages/SystemUI/res/drawable/ic_lock_gm2_24px.xml create mode 100644 packages/SystemUI/res/drawable/ic_lock_open_gm2_24px.xml create mode 100644 packages/SystemUI/res/drawable/ic_more_vert.xml create mode 100644 packages/SystemUI/res/drawable/ic_power_gm2_24px.xml create mode 100644 packages/SystemUI/res/drawable/ic_power_off_gm2_24px.xml create mode 100644 packages/SystemUI/res/drawable/ic_power_settings_new_gm2_24px.xml create mode 100644 packages/SystemUI/res/drawable/ic_switches_gm2_24px.xml create mode 100644 packages/SystemUI/res/drawable/ic_vacuum_gm2_24px.xml create mode 100644 packages/SystemUI/res/drawable/ic_videocam_gm2_24px.xml create mode 100644 packages/SystemUI/res/layout/controls_base_item.xml create mode 100644 packages/SystemUI/res/layout/controls_no_favorites.xml create mode 100644 packages/SystemUI/res/layout/controls_row.xml create mode 100644 packages/SystemUI/res/layout/controls_with_favorites.xml create mode 100644 packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt create mode 100644 packages/SystemUI/src/com/android/systemui/controls/ui/ControlWithState.kt diff --git a/core/java/android/service/controls/TokenProvider.aidl b/core/java/android/service/controls/TokenProvider.aidl new file mode 100644 index 0000000000000..8f4b7953f6598 --- /dev/null +++ b/core/java/android/service/controls/TokenProvider.aidl @@ -0,0 +1,7 @@ +package android.service.controls; + +/** @hide */ +interface TokenProvider { + void setAuthToken(String token); + String getAccountName(); +} \ No newline at end of file diff --git a/packages/SystemUI/res/color/light_background.xml b/packages/SystemUI/res/color/light_background.xml new file mode 100644 index 0000000000000..2effd991f666c --- /dev/null +++ b/packages/SystemUI/res/color/light_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/packages/SystemUI/res/color/light_foreground.xml b/packages/SystemUI/res/color/light_foreground.xml new file mode 100644 index 0000000000000..8143028795dfc --- /dev/null +++ b/packages/SystemUI/res/color/light_foreground.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/packages/SystemUI/res/color/lock_background.xml b/packages/SystemUI/res/color/lock_background.xml new file mode 100644 index 0000000000000..646fe5dfe712d --- /dev/null +++ b/packages/SystemUI/res/color/lock_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/packages/SystemUI/res/color/lock_foreground.xml b/packages/SystemUI/res/color/lock_foreground.xml new file mode 100644 index 0000000000000..3e05653bce928 --- /dev/null +++ b/packages/SystemUI/res/color/lock_foreground.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/packages/SystemUI/res/color/unknown_foreground.xml b/packages/SystemUI/res/color/unknown_foreground.xml new file mode 100644 index 0000000000000..bf028f18a7def --- /dev/null +++ b/packages/SystemUI/res/color/unknown_foreground.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/packages/SystemUI/res/drawable/control_background.xml b/packages/SystemUI/res/drawable/control_background.xml new file mode 100644 index 0000000000000..b246ea0a39352 --- /dev/null +++ b/packages/SystemUI/res/drawable/control_background.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/drawable/control_layer.xml b/packages/SystemUI/res/drawable/control_layer.xml new file mode 100644 index 0000000000000..fe8c4a4af1bb7 --- /dev/null +++ b/packages/SystemUI/res/drawable/control_layer.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/packages/SystemUI/res/drawable/control_no_favorites_background.xml b/packages/SystemUI/res/drawable/control_no_favorites_background.xml new file mode 100644 index 0000000000000..1e282ad0eec78 --- /dev/null +++ b/packages/SystemUI/res/drawable/control_no_favorites_background.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_device_thermostat_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_device_thermostat_gm2_24px.xml new file mode 100644 index 0000000000000..45a658fe07da6 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_device_thermostat_gm2_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_light_off_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_light_off_gm2_24px.xml new file mode 100644 index 0000000000000..78c3cc54f022b --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_light_off_gm2_24px.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_lock_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_lock_gm2_24px.xml new file mode 100644 index 0000000000000..f4299e6cda111 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_lock_gm2_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_lock_open_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_lock_open_gm2_24px.xml new file mode 100644 index 0000000000000..59fe0a9146562 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_lock_open_gm2_24px.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/res/drawable/ic_more_vert.xml b/packages/SystemUI/res/drawable/ic_more_vert.xml new file mode 100644 index 0000000000000..1309fa875b558 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_more_vert.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/SystemUI/res/drawable/ic_power_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_power_gm2_24px.xml new file mode 100644 index 0000000000000..cd957196e4dc3 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_power_gm2_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_power_off_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_power_off_gm2_24px.xml new file mode 100644 index 0000000000000..3eb7dd637abef --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_power_off_gm2_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_power_settings_new_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_power_settings_new_gm2_24px.xml new file mode 100644 index 0000000000000..f4edd875ddff5 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_power_settings_new_gm2_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_switches_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_switches_gm2_24px.xml new file mode 100644 index 0000000000000..bb535ceaed8cd --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_switches_gm2_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_vacuum_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_vacuum_gm2_24px.xml new file mode 100644 index 0000000000000..86b9591238f1d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_vacuum_gm2_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_videocam_gm2_24px.xml b/packages/SystemUI/res/drawable/ic_videocam_gm2_24px.xml new file mode 100644 index 0000000000000..687c9c417fa62 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_videocam_gm2_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/res/layout/controls_base_item.xml b/packages/SystemUI/res/layout/controls_base_item.xml new file mode 100644 index 0000000000000..3c4c61e30bc12 --- /dev/null +++ b/packages/SystemUI/res/layout/controls_base_item.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/controls_no_favorites.xml b/packages/SystemUI/res/layout/controls_no_favorites.xml new file mode 100644 index 0000000000000..79672caed61b1 --- /dev/null +++ b/packages/SystemUI/res/layout/controls_no_favorites.xml @@ -0,0 +1,18 @@ + + + diff --git a/packages/SystemUI/res/layout/controls_row.xml b/packages/SystemUI/res/layout/controls_row.xml new file mode 100644 index 0000000000000..13a6b36accd35 --- /dev/null +++ b/packages/SystemUI/res/layout/controls_row.xml @@ -0,0 +1,22 @@ + + + diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml new file mode 100644 index 0000000000000..7804fe6b6272b --- /dev/null +++ b/packages/SystemUI/res/layout/controls_with_favorites.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/global_actions_grid_v2.xml b/packages/SystemUI/res/layout/global_actions_grid_v2.xml index 4cfb47e3c6421..674148495478c 100644 --- a/packages/SystemUI/res/layout/global_actions_grid_v2.xml +++ b/packages/SystemUI/res/layout/global_actions_grid_v2.xml @@ -111,20 +111,7 @@ app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/global_actions_panel"> - - + diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 8dd2a8b9d07a2..09058f2460cb1 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -200,18 +200,28 @@ #3C4043 #202124 + #FCE8E6 #F28B82 #B71C1C #C5221F + #E8F0FE #AECBFA #8AB4F8 + #FF4285F4 #1A73E8 #1967D2 + #FEF7E0 #FFFBBC04 + #FF34A853 - #FF4285F4 + + #B06000 #FF9900 + + + ?android:attr/textColorPrimary + ?android:attr/colorBackgroundFloating diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index b40c5c0b02641..cc58b2014496a 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1181,5 +1181,12 @@ 45dp 40dp + + 5dp + 15dp + 100dp + 15dp + 12dp + 18dp 32dp diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 44a7fda6bce3d..1f13f8dc02fea 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2546,4 +2546,7 @@ Magnification Window Magnification Window Controls + + + Quick Controls diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt new file mode 100644 index 0000000000000..81b5f36985670 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt @@ -0,0 +1,400 @@ +/* + * 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>( + 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) + ) +) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlWithState.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlWithState.kt new file mode 100644 index 0000000000000..816f0b2cb1d17 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlWithState.kt @@ -0,0 +1,31 @@ +/* + * 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.service.controls.Control + +import com.android.systemui.controls.controller.ControlInfo + +/** + * A container for: + *
    + *
  • ControlInfo - Basic cached info about a Control + *
  • Control - Actual Control parcelable received directly from + * the participating application + *
+ */ +data class ControlWithState(val ci: ControlInfo, val control: Control?) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index 0270c2b6b1b33..b07a75d5e7575 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -19,12 +19,15 @@ package com.android.systemui.controls.ui import android.content.ComponentName import android.service.controls.Control import android.service.controls.actions.ControlAction +import android.view.ViewGroup interface ControlsUiController { + fun show(parent: ViewGroup) + fun hide() fun onRefreshState(componentName: ComponentName, controls: List) fun onActionResponse( componentName: ComponentName, controlId: String, @ControlAction.ResponseResult response: Int ) -} \ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 0ace1263b49b8..926fb6e75594c 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -16,19 +16,204 @@ package com.android.systemui.controls.ui +import android.accounts.Account +import android.accounts.AccountManager import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder import android.service.controls.Control +import android.service.controls.TokenProvider +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView + +import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.controls.controller.ControlInfo +import com.android.systemui.controls.management.ControlsProviderSelectorActivity +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.R + +import dagger.Lazy + +import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Singleton +private const val TAG = "ControlsUi" + +// TEMP CODE for MOCK +private const val TOKEN = "https://www.googleapis.com/auth/assistant" +private const val SCOPE = "oauth2:" + TOKEN +private var tokenProviderConnection: TokenProviderConnection? = null +class TokenProviderConnection(val cc: ControlsController, val context: Context) + : ServiceConnection { + private var mTokenProvider: TokenProvider? = null + + override fun onServiceConnected(cName: ComponentName, binder: IBinder) { + Thread({ + Log.i(TAG, "TokenProviderConnection connected") + mTokenProvider = TokenProvider.Stub.asInterface(binder) + + val mLastAccountName = mTokenProvider?.getAccountName() + + if (mLastAccountName == null || mLastAccountName.isEmpty()) { + Log.e(TAG, "NO ACCOUNT IS SET. Open HomeMock app") + } else { + mTokenProvider?.setAuthToken(getAuthToken(mLastAccountName)) + cc.subscribeToFavorites() + } + }, "TokenProviderThread").start() + } + + override fun onServiceDisconnected(cName: ComponentName) { + mTokenProvider = null + } + + fun getAuthToken(accountName: String): String? { + val am = AccountManager.get(context) + val accounts = am.getAccountsByType("com.google") + if (accounts == null || accounts.size == 0) { + Log.w(TAG, "No com.google accounts found") + return null + } + + var account: Account? = null + for (a in accounts) { + if (a.name.equals(accountName)) { + account = a + break + } + } + + if (account == null) { + account = accounts[0] + } + + try { + return am.blockingGetAuthToken(account!!, SCOPE, true) + } catch (e: Throwable) { + Log.e(TAG, "Error getting auth token", e) + return null + } + } +} + @Singleton -class ControlsUiControllerImpl @Inject constructor() : ControlsUiController { +class ControlsUiControllerImpl @Inject constructor ( + val controlsController: Lazy, + val context: Context, + @Main val uiExecutor: Executor +) : ControlsUiController { + + private lateinit var controlInfos: List + private val controlsById = mutableMapOf, ControlWithState>() + private val controlViewsById = mutableMapOf() + private lateinit var parent: ViewGroup + + override fun show(parent: ViewGroup) { + Log.d(TAG, "show()") + + this.parent = parent + + controlInfos = controlsController.get().getFavoriteControls() + + controlInfos.map { + ControlWithState(it, null) + }.associateByTo(controlsById) { Pair(it.ci.component, it.ci.controlId) } + + if (controlInfos.isEmpty()) { + showInitialSetupView() + } else { + showControlsView() + } + + // Temp code to pass auth + tokenProviderConnection = TokenProviderConnection(controlsController.get(), context) + val serviceIntent = Intent() + serviceIntent.setComponent(ComponentName("com.android.systemui.home.mock", + "com.android.systemui.home.mock.AuthService")) + context.bindService(serviceIntent, tokenProviderConnection!!, Context.BIND_AUTO_CREATE) + } + + private fun showInitialSetupView() { + val inflater = LayoutInflater.from(context) + inflater.inflate(R.layout.controls_no_favorites, parent, true) + + val textView = parent.requireViewById(R.id.controls_title) as TextView + textView.setOnClickListener { + val i = Intent() + i.setComponent(ComponentName(context, ControlsProviderSelectorActivity::class.java)) + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(i) + } + } + + private fun showControlsView() { + 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) + controlInfos.forEach { + Log.d(TAG, "favorited control id: " + it.controlId) + 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()) + cvh.bindData(controlsById.get(Pair(it.component, it.controlId))!!) + controlViewsById.put(it.controlId, cvh) + } + + val moreImageView = parent.requireViewById(R.id.controls_more) as View + moreImageView.setOnClickListener { + val i = Intent() + i.setComponent(ComponentName(context, ControlsProviderSelectorActivity::class.java)) + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(i) + } + } + + override fun hide() { + Log.d(TAG, "hide()") + controlsController.get().unsubscribe() + context.unbindService(tokenProviderConnection) + tokenProviderConnection = null + + parent.removeAllViews() + controlsById.clear() + controlViewsById.clear() + } override fun onRefreshState(componentName: ComponentName, controls: List) { - TODO("not implemented") + Log.d(TAG, "onRefreshState()") + controls.forEach { c -> + controlsById.get(Pair(componentName, c.getControlId()))?.let { + Log.d(TAG, "onRefreshState() for id: " + c.getControlId()) + val cws = ControlWithState(it.ci, c) + controlsById.put(Pair(componentName, c.getControlId()), cws) + + uiExecutor.execute { + controlViewsById.get(c.getControlId())?.bindData(cws) + } + } + } } override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) { + Log.d(TAG, "onActionResponse()") TODO("not implemented") } -} \ No newline at end of file + + private fun createRow(inflater: LayoutInflater, parent: ViewGroup): ViewGroup { + val row = inflater.inflate(R.layout.controls_row, parent, false) as ViewGroup + parent.addView(row) + return row + } +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index 83f6d45465b3f..80d776a59235c 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -93,12 +93,13 @@ import com.android.systemui.MultiListLayout; import com.android.systemui.MultiListLayout.MultiListAdapter; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.controls.ui.ControlsUiController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.GlobalActions.GlobalActionsManager; import com.android.systemui.plugins.GlobalActionsPanelPlugin; -import com.android.systemui.statusbar.phone.NotificationShadeWindowController; import com.android.systemui.statusbar.BlurUtils; +import com.android.systemui.statusbar.phone.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -183,6 +184,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private final IStatusBarService mStatusBarService; private final NotificationShadeWindowController mNotificationShadeWindowController; private GlobalActionsPanelPlugin mPanelPlugin; + private ControlsUiController mControlsUiController; /** * @param context everything needs a context :( @@ -200,7 +202,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, TelecomManager telecomManager, MetricsLogger metricsLogger, BlurUtils blurUtils, SysuiColorExtractor colorExtractor, IStatusBarService statusBarService, - NotificationShadeWindowController notificationShadeWindowController) { + NotificationShadeWindowController notificationShadeWindowController, + ControlsUiController controlsUiController) { mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme); mWindowManagerFuncs = windowManagerFuncs; mAudioManager = audioManager; @@ -220,6 +223,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mSysuiColorExtractor = colorExtractor; mStatusBarService = statusBarService; mNotificationShadeWindowController = notificationShadeWindowController; + mControlsUiController = controlsUiController; // receive broadcasts IntentFilter filter = new IntentFilter(); @@ -455,9 +459,12 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mKeyguardManager.isDeviceLocked()) : null; + boolean showControls = !mKeyguardManager.isDeviceLocked() && isControlsEnabled(mContext); + ActionsDialog dialog = new ActionsDialog(mContext, mAdapter, panelViewController, mBlurUtils, mSysuiColorExtractor, mStatusBarService, - mNotificationShadeWindowController, isControlsEnabled(mContext)); + mNotificationShadeWindowController, + showControls ? mControlsUiController : null); dialog.setCanceledOnTouchOutside(false); // Handled by the custom class. dialog.setKeyguardShowing(mKeyguardShowing); @@ -1543,13 +1550,15 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private boolean mHadTopUi; private final NotificationShadeWindowController mNotificationShadeWindowController; private final BlurUtils mBlurUtils; - private final boolean mControlsEnabled; + + private ControlsUiController mControlsUiController; + private ViewGroup mControlsView; ActionsDialog(Context context, MyAdapter adapter, GlobalActionsPanelPlugin.PanelViewController plugin, BlurUtils blurUtils, SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService, NotificationShadeWindowController notificationShadeWindowController, - boolean controlsEnabled) { + ControlsUiController controlsUiController) { super(context, com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions); mContext = context; mAdapter = adapter; @@ -1557,7 +1566,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mColorExtractor = sysuiColorExtractor; mStatusBarService = statusBarService; mNotificationShadeWindowController = notificationShadeWindowController; - mControlsEnabled = controlsEnabled; + mControlsUiController = controlsUiController; // Window initialization Window window = getWindow(); @@ -1639,6 +1648,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private void initializeLayout() { setContentView(getGlobalActionsLayoutId(mContext)); fixNavBarClipping(); + mControlsView = findViewById(com.android.systemui.R.id.global_actions_controls); mGlobalActionsLayout = findViewById(com.android.systemui.R.id.global_actions_view); mGlobalActionsLayout.setOutsideTouchListener(view -> dismiss()); ((View) mGlobalActionsLayout.getParent()).setOnClickListener(view -> dismiss()); @@ -1674,7 +1684,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } private int getGlobalActionsLayoutId(Context context) { - if (mControlsEnabled) { + if (mControlsUiController != null) { return com.android.systemui.R.layout.global_actions_grid_v2; } @@ -1758,6 +1768,9 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mBlurUtils.radiusForRatio(animatedValue)); }) .start(); + if (mControlsUiController != null) { + mControlsUiController.show(mControlsView); + } } @Override @@ -1766,6 +1779,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, return; } mShowing = false; + if (mControlsUiController != null) mControlsUiController.hide(); mGlobalActionsLayout.setTranslationX(0); mGlobalActionsLayout.setTranslationY(0); mGlobalActionsLayout.setAlpha(1);