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);