From 307b1ef70caf1ad78a44b4d489fe1cd986443921 Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Mon, 10 Feb 2020 07:27:01 -0500 Subject: [PATCH] Controls UI - Update drag behavior for ranges Consolidate toggle() fns. Update range handling. It was previously acting like a slider but now detects the diff between how far you swiped and updates the current value appropriately. Bug: 148207527 Test: manual testing of drag behavior Change-Id: I85896527bd3ff225cd08d11fbe2e4b34190a6b5a --- .../controls/ui/ControlActionCoordinator.kt | 50 ++++++ .../systemui/controls/ui/ControlViewHolder.kt | 34 ++-- .../controls/ui/ControlsUiController.kt | 4 + .../controls/ui/ControlsUiControllerImpl.kt | 20 ++- .../controls/ui/TemperatureControlBehavior.kt | 2 + .../systemui/controls/ui/ToggleBehavior.kt | 18 +-- .../controls/ui/ToggleRangeBehavior.kt | 147 ++++++++++-------- 7 files changed, 171 insertions(+), 104 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt new file mode 100644 index 0000000000000..59eb00dd3f5a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt @@ -0,0 +1,50 @@ +/* + * 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.app.PendingIntent +import android.content.Intent +import android.service.controls.actions.BooleanAction +import android.util.Log +import android.view.HapticFeedbackConstants + +object ControlActionCoordinator { + public const val MIN_LEVEL = 0 + public const val MAX_LEVEL = 10000 + + fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) { + cvh.action(BooleanAction(templateId, !isChecked)) + + val nextLevel = if (isChecked) MIN_LEVEL else MAX_LEVEL + cvh.clipLayer.setLevel(nextLevel) + } + + fun longPress(cvh: ControlViewHolder) { + // Long press snould only be called when there is valid control state, otherwise ignore + cvh.cws.control?.let { + try { + cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + it.getAppIntent().send() + val closeDialog = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) + cvh.context.sendBroadcast(closeDialog) + } catch (e: PendingIntent.CanceledException) { + Log.e(ControlsUiController.TAG, "Error sending pending intent", e) + cvh.setTransientStatus("Error opening application") + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt index 78e0e8b81b44f..55c1b6a1e369a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt @@ -17,7 +17,6 @@ package com.android.systemui.controls.ui import android.content.Context -import android.content.Intent import android.graphics.drawable.ClipDrawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.Icon @@ -37,8 +36,6 @@ import com.android.systemui.controls.controller.ControlsController import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.R -const val MIN_LEVEL = 0 -const val MAX_LEVEL = 10000 private const val UPDATE_DELAY_IN_MILLIS = 3000L class ControlViewHolder( @@ -79,12 +76,9 @@ class ControlViewHolder( Pair(Control.STATUS_UNKNOWN, ControlTemplate.NO_TEMPLATE) } - cws.control?.let { c -> + cws.control?.let { layout.setOnLongClickListener(View.OnLongClickListener() { - val closeDialog = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) - context.sendBroadcast(closeDialog) - - c.getAppIntent().send() + ControlActionCoordinator.longPress(this@ControlViewHolder) true }) } @@ -100,19 +94,23 @@ class ControlViewHolder( } if (!text.isEmpty()) { - val previousText = status.getText() - val previousTextExtra = statusExtra.getText() - - cancelUpdate = uiExecutor.executeDelayed({ - status.setText(previousText) - statusExtra.setText(previousTextExtra) - }, UPDATE_DELAY_IN_MILLIS) - - status.setText(text) - statusExtra.setText("") + setTransientStatus(text) } } + fun setTransientStatus(tempStatus: String) { + val previousText = status.getText() + val previousTextExtra = statusExtra.getText() + + cancelUpdate = uiExecutor.executeDelayed({ + status.setText(previousText) + statusExtra.setText(previousTextExtra) + }, UPDATE_DELAY_IN_MILLIS) + + status.setText(tempStatus) + statusExtra.setText("") + } + fun action(action: ControlAction) { controlsController.action(cws.ci, action) } 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 d70c86fc3266b..0f105376847f5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -24,6 +24,10 @@ import android.view.ViewGroup interface ControlsUiController { val available: Boolean + companion object { + public const val TAG = "ControlsUiController" + } + fun show(parent: ViewGroup) fun hide() fun onRefreshState(componentName: ComponentName, controls: List) 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 f029dfbe1bb2b..9e6636fca97d3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -51,8 +51,6 @@ import java.text.Collator 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 @@ -63,13 +61,13 @@ class TokenProviderConnection(val cc: ControlsController, val context: Context) override fun onServiceConnected(cName: ComponentName, binder: IBinder) { Thread({ - Log.i(TAG, "TokenProviderConnection connected") + Log.i(ControlsUiController.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") + Log.e(ControlsUiController.TAG, "NO ACCOUNT IS SET. Open HomeMock app") } else { mTokenProvider?.setAuthToken(getAuthToken(mLastAccountName)) cc.subscribeToFavorites() @@ -85,7 +83,7 @@ class TokenProviderConnection(val cc: ControlsController, val context: Context) 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") + Log.w(ControlsUiController.TAG, "No com.google accounts found") return null } @@ -104,7 +102,7 @@ class TokenProviderConnection(val cc: ControlsController, val context: Context) try { return am.blockingGetAuthToken(account!!, SCOPE, true) } catch (e: Throwable) { - Log.e(TAG, "Error getting auth token", e) + Log.e(ControlsUiController.TAG, "Error getting auth token", e) return null } } @@ -146,7 +144,7 @@ class ControlsUiControllerImpl @Inject constructor ( } override fun show(parent: ViewGroup) { - Log.d(TAG, "show()") + Log.d(ControlsUiController.TAG, "show()") this.parent = parent @@ -218,7 +216,7 @@ class ControlsUiControllerImpl @Inject constructor ( 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) + Log.d(ControlsUiController.TAG, "favorited control id: " + it.controlId) if (lastRow.getChildCount() == 2) { lastRow = createRow(inflater, listView) } @@ -240,7 +238,7 @@ class ControlsUiControllerImpl @Inject constructor ( } override fun hide() { - Log.d(TAG, "hide()") + Log.d(ControlsUiController.TAG, "hide()") controlsController.get().unsubscribe() context.unbindService(tokenProviderConnection) tokenProviderConnection = null @@ -252,10 +250,10 @@ class ControlsUiControllerImpl @Inject constructor ( } override fun onRefreshState(componentName: ComponentName, controls: List) { - Log.d(TAG, "onRefreshState()") + Log.d(ControlsUiController.TAG, "onRefreshState()") controls.forEach { c -> controlsById.get(ControlKey(componentName, c.getControlId()))?.let { - Log.d(TAG, "onRefreshState() for id: " + c.getControlId()) + Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId()) val cws = ControlWithState(it.ci, c) val key = ControlKey(componentName, c.getControlId()) controlsById.put(key, cws) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt index ae0ebbb9e1bb5..7661c6fb0bec5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt @@ -24,6 +24,8 @@ import android.service.controls.templates.TemperatureControlTemplate import android.widget.TextView import com.android.systemui.R +import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL +import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL class TemperatureControlBehavior : Behavior { lateinit var clipLayer: Drawable diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt index 7cd3ab795678b..4c35d26c6281b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt @@ -22,10 +22,11 @@ import android.graphics.drawable.LayerDrawable import android.view.View import android.widget.TextView import android.service.controls.Control -import android.service.controls.actions.BooleanAction import android.service.controls.templates.ToggleTemplate import com.android.systemui.R +import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL +import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL class ToggleBehavior : Behavior { lateinit var clipLayer: Drawable @@ -42,13 +43,15 @@ class ToggleBehavior : Behavior { status.setText(control.getStatusText()) - cvh.layout.setOnClickListener(View.OnClickListener() { toggle() }) + template = control.getControlTemplate() as ToggleTemplate + + cvh.layout.setOnClickListener(View.OnClickListener() { + ControlActionCoordinator.toggle(cvh, template.getTemplateId(), template.isChecked()) + }) 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() @@ -56,11 +59,4 @@ class ToggleBehavior : Behavior { cvh.setEnabled(checked) cvh.applyRenderInfo(RenderInfo.lookup(deviceType, checked)) } - - fun toggle() { - cvh.action(BooleanAction(template.getTemplateId(), !template.isChecked())) - - val nextLevel = if (template.isChecked()) MIN_LEVEL else MAX_LEVEL - clipLayer.setLevel(nextLevel) - } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt index a6918f50a977b..8d65ca344d171 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt @@ -19,17 +19,23 @@ package com.android.systemui.controls.ui import android.content.Context import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable +import android.util.Log +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent import android.view.View import android.widget.TextView import android.service.controls.Control -import android.service.controls.actions.BooleanAction import android.service.controls.actions.FloatAction import android.service.controls.templates.RangeTemplate import android.service.controls.templates.ToggleRangeTemplate import android.util.TypedValue import com.android.systemui.R +import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL +import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL + +import java.util.IllegalFormatException class ToggleRangeBehavior : Behavior { lateinit var clipLayer: Drawable @@ -41,6 +47,10 @@ class ToggleRangeBehavior : Behavior { lateinit var status: TextView lateinit var context: Context + companion object { + private const val DEFAULT_FORMAT = "%.1f" + } + override fun apply(cvh: ControlViewHolder, cws: ControlWithState) { this.control = cws.control!! this.cvh = cvh @@ -52,10 +62,25 @@ class ToggleRangeBehavior : Behavior { context = status.getContext() - cvh.layout.setOnTouchListener(ToggleRangeTouchListener()) + val gestureListener = ToggleRangeGestureListener(cvh.layout) + val gestureDetector = GestureDetector(context, gestureListener) + cvh.layout.setOnTouchListener({ v: View, e: MotionEvent -> + if (gestureDetector.onTouchEvent(e)) { + return@setOnTouchListener true + } + + if (e.getAction() == MotionEvent.ACTION_UP && gestureListener.isDragging) { + gestureListener.isDragging = false + endUpdateRange() + return@setOnTouchListener true + } + + return@setOnTouchListener false + }) val ld = cvh.layout.getBackground() as LayerDrawable clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) + clipLayer.setLevel(MIN_LEVEL) template = control.getControlTemplate() as ToggleRangeTemplate rangeTemplate = template.getRange() @@ -63,30 +88,31 @@ class ToggleRangeBehavior : Behavior { val checked = template.isChecked() val deviceType = control.getDeviceType() - updateRange((rangeTemplate.getCurrentValue() / 100.0f), checked) + val currentRatio = rangeTemplate.getCurrentValue() / + (rangeTemplate.getMaxValue() - rangeTemplate.getMinValue()) + updateRange(currentRatio, checked) cvh.setEnabled(checked) cvh.applyRenderInfo(RenderInfo.lookup(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) + fun updateRange(ratioDiff: Float, checked: Boolean) { + val changeAmount = if (checked) (MAX_LEVEL * ratioDiff).toInt() else MIN_LEVEL + val newLevel = Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, clipLayer.getLevel() + changeAmount)) + clipLayer.setLevel(newLevel) - if (checked && f < 100.0f && f > 0.0f) { - statusExtra.setText("" + (f * 100.0).toInt() + "%") + if (checked) { + val newValue = levelToRangeValue() + val formattedNewValue = format(rangeTemplate.getFormatString().toString(), + DEFAULT_FORMAT, newValue) + + statusExtra.setText(formattedNewValue) statusExtra.setVisibility(View.VISIBLE) } else { statusExtra.setText("") @@ -94,17 +120,30 @@ class ToggleRangeBehavior : Behavior { } } - fun endUpdateRange(f: Float) { - statusExtra.setText(" - " + (f * 100.0).toInt() + "%") + private fun format(primaryFormat: String, backupFormat: String, value: Float): String { + return try { + String.format(primaryFormat, value) + } catch (e: IllegalFormatException) { + Log.w(ControlsUiController.TAG, "Illegal format in range template", e) + if (backupFormat == "") { + "" + } else { + format(backupFormat, "", value) + } + } + } - val newValue = rangeTemplate.getMinValue() + - (f * (rangeTemplate.getMaxValue() - rangeTemplate.getMinValue())) + private fun levelToRangeValue(): Float { + val ratio = clipLayer.getLevel().toFloat() / MAX_LEVEL + return rangeTemplate.getMinValue() + + (ratio * (rangeTemplate.getMaxValue() - rangeTemplate.getMinValue())) + } + fun endUpdateRange() { 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))) + cvh.action(FloatAction(rangeTemplate.getTemplateId(), findNearestStep(levelToRangeValue()))) } fun findNearestStep(value: Float): Float { @@ -125,59 +164,39 @@ class ToggleRangeBehavior : Behavior { 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) - } + inner class ToggleRangeGestureListener( + val v: View + ) : SimpleOnGestureListener() { + var isDragging: Boolean = false + override fun onDown(e: MotionEvent): Boolean { return true } - private fun setupTouch(e: MotionEvent) { - initialTouchX = e.getX() - initialTouchY = e.getY() + override fun onLongPress(e: MotionEvent) { + ControlActionCoordinator.longPress(this@ToggleRangeBehavior.cvh) } - 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@ToggleRangeBehavior.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@ToggleRangeBehavior.updateRange(percent, true) - } - } - } - - private fun endTouch(v: View, e: MotionEvent) { + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + xDiff: Float, + yDiff: Float + ): Boolean { if (!isDragging) { - this@ToggleRangeBehavior.toggle() - } else { - val percent = Math.max(0.0f, Math.min(1.0f, e.getX() / v.getWidth())) - this@ToggleRangeBehavior.endUpdateRange(percent) + this@ToggleRangeBehavior.beginUpdateRange() + isDragging = true } - initialTouchX = 0.0f - initialTouchY = 0.0f - isDragging = false + this@ToggleRangeBehavior.updateRange(-xDiff / v.getWidth(), true) + return true + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { + val th = this@ToggleRangeBehavior + ControlActionCoordinator.toggle(th.cvh, th.template.getTemplateId(), + th.template.isChecked()) + return true } } }