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