• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.haptics.slider.compose.ui
18 
19 import androidx.compose.foundation.gestures.Orientation
20 import androidx.compose.foundation.interaction.DragInteraction
21 import androidx.compose.foundation.interaction.InteractionSource
22 import androidx.compose.ui.geometry.Offset
23 import androidx.compose.ui.input.pointer.util.VelocityTracker
24 import androidx.compose.ui.unit.Velocity
25 import com.android.app.tracing.coroutines.launchTraced as launch
26 import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig
27 import com.android.systemui.haptics.slider.SliderDragVelocityProvider
28 import com.android.systemui.haptics.slider.SliderEventType
29 import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig
30 import com.android.systemui.haptics.slider.SliderHapticFeedbackProvider
31 import com.android.systemui.haptics.slider.SliderStateProducer
32 import com.android.systemui.haptics.slider.SliderStateTracker
33 import com.android.systemui.lifecycle.ExclusiveActivatable
34 import com.android.systemui.statusbar.VibratorHelper
35 import com.android.systemui.util.time.SystemClock
36 import com.google.android.msdl.domain.MSDLPlayer
37 import dagger.assisted.Assisted
38 import dagger.assisted.AssistedFactory
39 import dagger.assisted.AssistedInject
40 import kotlin.math.abs
41 import kotlinx.coroutines.Job
42 import kotlinx.coroutines.awaitCancellation
43 import kotlinx.coroutines.coroutineScope
44 
45 class SliderHapticsViewModel
46 @AssistedInject
47 constructor(
48     @Assisted private val interactionSource: InteractionSource,
49     @Assisted private val sliderRange: ClosedFloatingPointRange<Float>,
50     @Assisted private val orientation: Orientation,
51     @Assisted private val sliderHapticFeedbackConfig: SliderHapticFeedbackConfig,
52     @Assisted private val sliderTrackerConfig: SeekableSliderTrackerConfig,
53     vibratorHelper: VibratorHelper,
54     msdlPlayer: MSDLPlayer,
55     systemClock: SystemClock,
56 ) : ExclusiveActivatable() {
57 
58     var currentSliderEventType = SliderEventType.NOTHING
59         private set
60 
61     private val velocityTracker = VelocityTracker()
62     private val maxVelocity =
63         Velocity(
64             sliderHapticFeedbackConfig.maxVelocityToScale,
65             sliderHapticFeedbackConfig.maxVelocityToScale,
66         )
67     private val dragVelocityProvider = SliderDragVelocityProvider {
68         val velocity =
69             when (orientation) {
70                 Orientation.Horizontal -> velocityTracker.calculateVelocity(maxVelocity).x
71                 Orientation.Vertical -> velocityTracker.calculateVelocity(maxVelocity).y
72             }
73         abs(velocity)
74     }
75 
76     private var startingProgress = 0f
77 
78     // Haptic slider stack of components
79     private val sliderStateProducer = SliderStateProducer()
80     private val sliderHapticFeedbackProvider =
81         SliderHapticFeedbackProvider(
82             vibratorHelper,
83             msdlPlayer,
84             dragVelocityProvider,
85             sliderHapticFeedbackConfig,
86             systemClock,
87         )
88     private var sliderTracker: SliderStateTracker? = null
89 
90     private var trackerJob: Job? = null
91 
92     val isRunning: Boolean
93         get() = trackerJob?.isActive == true && sliderTracker?.isTracking == true
94 
95     override suspend fun onActivated(): Nothing {
96         coroutineScope {
97             trackerJob =
98                 launch("SliderHapticsViewModel#SliderStateTracker") {
99                     try {
100                         sliderTracker =
101                             SliderStateTracker(
102                                 sliderHapticFeedbackProvider,
103                                 sliderStateProducer,
104                                 this,
105                                 sliderTrackerConfig,
106                             )
107                         sliderTracker?.startTracking()
108                         awaitCancellation()
109                     } finally {
110                         sliderTracker?.stopTracking()
111                         sliderTracker = null
112                         velocityTracker.resetTracking()
113                     }
114                 }
115 
116             launch("SliderHapticsViewModel#InteractionSource") {
117                 interactionSource.interactions.collect { interaction ->
118                     if (interaction is DragInteraction.Start) {
119                         currentSliderEventType = SliderEventType.STARTED_TRACKING_TOUCH
120                         sliderStateProducer.onStartTracking(true)
121                     }
122                 }
123             }
124             awaitCancellation()
125         }
126     }
127 
128     /**
129      * React to a value change in the slider.
130      *
131      * @param[value] latest value of the slider inside the [sliderRange] provided to the class
132      *   constructor.
133      */
134     fun onValueChange(value: Float) {
135         val normalized = value.normalize()
136         when (currentSliderEventType) {
137             SliderEventType.NOTHING -> {
138                 currentSliderEventType = SliderEventType.STARTED_TRACKING_PROGRAM
139                 startingProgress = normalized
140                 sliderStateProducer.resetWithProgress(normalized)
141                 sliderStateProducer.onStartTracking(false)
142             }
143             SliderEventType.STARTED_TRACKING_TOUCH -> {
144                 startingProgress = normalized
145                 currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_USER
146                 sliderStateProducer.onProgressChanged(true, normalized)
147             }
148             SliderEventType.PROGRESS_CHANGE_BY_USER -> {
149                 addVelocityDataPoint(value)
150                 currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_USER
151                 sliderStateProducer.onProgressChanged(true, normalized)
152             }
153             SliderEventType.STARTED_TRACKING_PROGRAM -> {
154                 startingProgress = normalized
155                 currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_PROGRAM
156                 sliderStateProducer.onProgressChanged(false, normalized)
157             }
158             SliderEventType.PROGRESS_CHANGE_BY_PROGRAM -> {
159                 addVelocityDataPoint(value)
160                 currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_PROGRAM
161                 sliderStateProducer.onProgressChanged(false, normalized)
162             }
163             else -> {}
164         }
165     }
166 
167     fun addVelocityDataPoint(value: Float) {
168         val normalized = value.normalize()
169         velocityTracker.addPosition(System.currentTimeMillis(), normalized.toOffset())
170     }
171 
172     fun onValueChangeEnded() {
173         when (currentSliderEventType) {
174             SliderEventType.STARTED_TRACKING_PROGRAM,
175             SliderEventType.PROGRESS_CHANGE_BY_PROGRAM -> sliderStateProducer.onStopTracking(false)
176             SliderEventType.STARTED_TRACKING_TOUCH,
177             SliderEventType.PROGRESS_CHANGE_BY_USER -> sliderStateProducer.onStopTracking(true)
178             else -> {}
179         }
180         currentSliderEventType = SliderEventType.NOTHING
181         velocityTracker.resetTracking()
182     }
183 
184     private fun ClosedFloatingPointRange<Float>.length(): Float = endInclusive - start
185 
186     private fun Float.normalize(): Float =
187         ((this - sliderRange.start) / sliderRange.length()).coerceIn(0f, 1f)
188 
189     private fun Float.toOffset(): Offset =
190         when (orientation) {
191             Orientation.Horizontal -> Offset(x = this - startingProgress, y = 0f)
192             Orientation.Vertical -> Offset(x = 0f, y = this - startingProgress)
193         }
194 
195     @AssistedFactory
196     interface Factory {
197         fun create(
198             interactionSource: InteractionSource,
199             sliderRange: ClosedFloatingPointRange<Float>,
200             orientation: Orientation,
201             sliderHapticFeedbackConfig: SliderHapticFeedbackConfig,
202             sliderTrackerConfig: SeekableSliderTrackerConfig,
203         ): SliderHapticsViewModel
204     }
205 }
206