1 /*
<lambda>null2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.controls.ui
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.graphics.drawable.Drawable
24 import android.graphics.drawable.LayerDrawable
25 import android.os.Bundle
26 import android.service.controls.Control
27 import android.service.controls.templates.ControlTemplate
28 import android.service.controls.templates.RangeTemplate
29 import android.service.controls.templates.TemperatureControlTemplate
30 import android.service.controls.templates.ToggleRangeTemplate
31 import android.util.Log
32 import android.util.MathUtils
33 import android.view.GestureDetector
34 import android.view.GestureDetector.SimpleOnGestureListener
35 import android.view.MotionEvent
36 import android.view.View
37 import android.view.ViewGroup
38 import android.view.accessibility.AccessibilityEvent
39 import android.view.accessibility.AccessibilityNodeInfo
40 import com.android.systemui.res.R
41 import com.android.app.animation.Interpolators
42 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL
43 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL
44 import java.util.IllegalFormatException
45 
46 /**
47  * Supports [ToggleRangeTemplate] and [RangeTemplate], as well as when one of those templates is
48  * defined as the subtemplate in [TemperatureControlTemplate].
49  */
50 class ToggleRangeBehavior : Behavior {
51     private var rangeAnimator: ValueAnimator? = null
52     lateinit var clipLayer: Drawable
53     lateinit var templateId: String
54     lateinit var control: Control
55     lateinit var cvh: ControlViewHolder
56     lateinit var rangeTemplate: RangeTemplate
57     lateinit var context: Context
58     var currentStatusText: CharSequence = ""
59     var currentRangeValue: String = ""
60     var isChecked: Boolean = false
61     var isToggleable: Boolean = false
62     var colorOffset: Int = 0
63 
64     companion object {
65         private const val DEFAULT_FORMAT = "%.1f"
66     }
67 
68     override fun initialize(cvh: ControlViewHolder) {
69         this.cvh = cvh
70         context = cvh.context
71 
72         val gestureListener = ToggleRangeGestureListener(cvh.layout)
73         val gestureDetector = GestureDetector(context, gestureListener)
74         cvh.layout.setOnTouchListener { v: View, e: MotionEvent ->
75             if (gestureDetector.onTouchEvent(e)) {
76                 // Don't return true to let the state list change to "pressed"
77                 return@setOnTouchListener false
78             }
79 
80             if (e.getAction() == MotionEvent.ACTION_UP && gestureListener.isDragging) {
81                 v.getParent().requestDisallowInterceptTouchEvent(false)
82                 gestureListener.isDragging = false
83                 endUpdateRange()
84                 return@setOnTouchListener false
85             }
86 
87             return@setOnTouchListener false
88         }
89     }
90 
91     private fun setup(template: ToggleRangeTemplate) {
92         rangeTemplate = template.getRange()
93         isToggleable = true
94         isChecked = template.isChecked()
95     }
96 
97     private fun setup(template: RangeTemplate) {
98         rangeTemplate = template
99 
100         // only show disabled state when value is at the minimum
101         isChecked = rangeTemplate.currentValue != rangeTemplate.minValue
102     }
103 
104     private fun setupTemplate(template: ControlTemplate): Boolean {
105         return when (template) {
106             is ToggleRangeTemplate -> {
107                 setup(template)
108                 true
109             }
110             is RangeTemplate -> {
111                 setup(template)
112                 true
113             }
114             is TemperatureControlTemplate -> setupTemplate(template.getTemplate())
115             else -> {
116                 Log.e(ControlsUiController.TAG, "Unsupported template type: $template")
117                 false
118             }
119         }
120     }
121 
122     override fun bind(cws: ControlWithState, colorOffset: Int) {
123         this.control = cws.control!!
124         this.colorOffset = colorOffset
125 
126         currentStatusText = control.getStatusText()
127 
128         // ControlViewHolder sets a long click listener, but we want to handle touch in
129         // here instead, otherwise we'll have state conflicts.
130         cvh.layout.setOnLongClickListener(null)
131 
132         val ld = cvh.layout.getBackground() as LayerDrawable
133         clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
134 
135         val template = control.getControlTemplate()
136         if (!setupTemplate(template)) return
137         templateId = template.getTemplateId()
138 
139         updateRange(rangeToLevelValue(rangeTemplate.currentValue), isChecked,
140             /* isDragging */ false)
141 
142         cvh.applyRenderInfo(isChecked, colorOffset)
143 
144         /*
145          * This is custom widget behavior, so add a new accessibility delegate to
146          * handle clicks and range events. Present as a seek bar control.
147          */
148         cvh.layout.setAccessibilityDelegate(object : View.AccessibilityDelegate() {
149             override fun onInitializeAccessibilityNodeInfo(
150                 host: View,
151                 info: AccessibilityNodeInfo
152             ) {
153                 super.onInitializeAccessibilityNodeInfo(host, info)
154 
155                 val min = levelToRangeValue(MIN_LEVEL)
156                 val current = levelToRangeValue(clipLayer.getLevel())
157                 val max = levelToRangeValue(MAX_LEVEL)
158 
159                 val step = rangeTemplate.getStepValue().toDouble()
160                 val type = if (step == Math.floor(step)) {
161                     AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT
162                 } else {
163                     AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_FLOAT
164                 }
165 
166                 if (isChecked) {
167                     val rangeInfo = AccessibilityNodeInfo.RangeInfo.obtain(type, min, max, current)
168                     info.setRangeInfo(rangeInfo)
169                 }
170                 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS)
171             }
172 
173             override fun performAccessibilityAction(
174                 host: View,
175                 action: Int,
176                 arguments: Bundle?
177             ): Boolean {
178                 val handled = when (action) {
179                     AccessibilityNodeInfo.ACTION_CLICK -> {
180                         if (!isToggleable) {
181                             false
182                         } else {
183                             cvh.controlActionCoordinator.toggle(cvh, templateId, isChecked)
184                             true
185                         }
186                     }
187                     AccessibilityNodeInfo.ACTION_LONG_CLICK -> {
188                         cvh.controlActionCoordinator.longPress(cvh)
189                         true
190                     }
191                     AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.getId() -> {
192                         if (arguments == null || !arguments.containsKey(
193                                 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) {
194                             false
195                         } else {
196                             val value = arguments.getFloat(
197                                 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)
198                             val level = rangeToLevelValue(value)
199                             updateRange(level, isChecked, /* isDragging */ true)
200                             endUpdateRange()
201                             true
202                         }
203                     }
204                     else -> false
205                 }
206 
207                 return handled || super.performAccessibilityAction(host, action, arguments)
208             }
209 
210             override fun onRequestSendAccessibilityEvent(
211                 host: ViewGroup,
212                 child: View,
213                 event: AccessibilityEvent
214             ): Boolean = true
215         })
216     }
217 
218     fun beginUpdateRange() {
219         cvh.userInteractionInProgress = true
220         cvh.setStatusTextSize(context.getResources()
221                 .getDimensionPixelSize(R.dimen.control_status_expanded).toFloat())
222     }
223 
224     fun updateRange(level: Int, checked: Boolean, isDragging: Boolean) {
225         val newLevel = Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, level))
226 
227         // If the current level is at the minimum and the user is dragging, set the control to
228         // the enabled state to indicate their intention to enable the device. This will update
229         // control colors to support dragging.
230         if (clipLayer.level == MIN_LEVEL && newLevel > MIN_LEVEL) {
231             cvh.applyRenderInfo(checked, colorOffset, false /* animated */)
232         }
233 
234         rangeAnimator?.cancel()
235         if (isDragging) {
236             val isEdge = newLevel == MIN_LEVEL || newLevel == MAX_LEVEL
237             if (clipLayer.level != newLevel) {
238                 cvh.controlActionCoordinator.drag(cvh, isEdge)
239                 clipLayer.level = newLevel
240             }
241         } else if (newLevel != clipLayer.level) {
242             rangeAnimator = ValueAnimator.ofInt(cvh.clipLayer.level, newLevel).apply {
243                 addUpdateListener {
244                     cvh.clipLayer.level = it.animatedValue as Int
245                 }
246                 addListener(object : AnimatorListenerAdapter() {
247                     override fun onAnimationEnd(animation: Animator) {
248                         rangeAnimator = null
249                     }
250                 })
251                 duration = ControlViewHolder.STATE_ANIMATION_DURATION
252                 interpolator = Interpolators.CONTROL_STATE
253                 start()
254             }
255         }
256 
257         if (checked) {
258             val newValue = levelToRangeValue(newLevel)
259             currentRangeValue = format(rangeTemplate.getFormatString().toString(),
260                     DEFAULT_FORMAT, newValue)
261             if (isDragging) {
262                 cvh.setStatusText(currentRangeValue, /* immediately */ true)
263             } else {
264                 cvh.setStatusText("$currentStatusText $currentRangeValue")
265             }
266         } else {
267             cvh.setStatusText(currentStatusText)
268         }
269     }
270 
271     private fun format(primaryFormat: String, backupFormat: String, value: Float): String {
272         return try {
273             String.format(primaryFormat, findNearestStep(value))
274         } catch (e: IllegalFormatException) {
275             Log.w(ControlsUiController.TAG, "Illegal format in range template", e)
276             if (backupFormat == "") {
277                 ""
278             } else {
279                 format(backupFormat, "", value)
280             }
281         }
282     }
283 
284     private fun levelToRangeValue(i: Int): Float {
285         return MathUtils.constrainedMap(rangeTemplate.minValue, rangeTemplate.maxValue,
286                 MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(), i.toFloat())
287     }
288 
289     private fun rangeToLevelValue(i: Float): Int {
290         return MathUtils.constrainedMap(MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(),
291                 rangeTemplate.minValue, rangeTemplate.maxValue, i).toInt()
292     }
293 
294     fun endUpdateRange() {
295         cvh.setStatusTextSize(context.getResources()
296                 .getDimensionPixelSize(R.dimen.control_status_normal).toFloat())
297         cvh.setStatusText("$currentStatusText $currentRangeValue", /* immediately */ true)
298         cvh.controlActionCoordinator.setValue(cvh, rangeTemplate.getTemplateId(),
299             findNearestStep(levelToRangeValue(clipLayer.getLevel())))
300         cvh.userInteractionInProgress = false
301     }
302 
303     fun findNearestStep(value: Float): Float {
304         var minDiff = Float.MAX_VALUE
305 
306         var f = rangeTemplate.getMinValue()
307         while (f <= rangeTemplate.getMaxValue()) {
308             val currentDiff = Math.abs(value - f)
309             if (currentDiff < minDiff) {
310                 minDiff = currentDiff
311             } else {
312                 return f - rangeTemplate.getStepValue()
313             }
314 
315             f += rangeTemplate.getStepValue()
316         }
317 
318         return rangeTemplate.getMaxValue()
319     }
320 
321     inner class ToggleRangeGestureListener(
322         val v: View
323     ) : SimpleOnGestureListener() {
324         var isDragging: Boolean = false
325 
326         override fun onDown(e: MotionEvent): Boolean {
327             return true
328         }
329 
330         override fun onLongPress(e: MotionEvent) {
331             if (isDragging) {
332                 return
333             }
334             cvh.controlActionCoordinator.longPress(cvh)
335         }
336 
337         override fun onScroll(
338             e1: MotionEvent?,
339             e2: MotionEvent,
340             xDiff: Float,
341             yDiff: Float
342         ): Boolean {
343             if (!isDragging) {
344                 v.getParent().requestDisallowInterceptTouchEvent(true)
345                 beginUpdateRange()
346                 isDragging = true
347             }
348 
349             val ratioDiff = -xDiff / v.width
350             val changeAmount = ((MAX_LEVEL - MIN_LEVEL) * ratioDiff).toInt()
351             updateRange(clipLayer.level + changeAmount, checked = true, isDragging = true)
352             return true
353         }
354 
355         override fun onSingleTapUp(e: MotionEvent): Boolean {
356             if (!isToggleable) return false
357             cvh.controlActionCoordinator.toggle(cvh, templateId, isChecked)
358             return true
359         }
360     }
361 }
362