• 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.scene.ui.viewmodel
18 
19 import android.view.HapticFeedbackConstants
20 import android.view.View
21 import androidx.compose.ui.hapticfeedback.HapticFeedback
22 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
23 import com.android.compose.animation.scene.ObservableTransitionState
24 import com.android.compose.animation.scene.reveal.ContainerRevealHaptics
25 import com.android.systemui.Flags
26 import com.android.systemui.lifecycle.ExclusiveActivatable
27 import com.android.systemui.scene.domain.interactor.SceneInteractor
28 import com.android.systemui.scene.shared.model.Overlays
29 import com.android.systemui.scene.shared.model.Scenes
30 import com.android.systemui.scene.ui.composable.SceneContainer
31 import com.android.systemui.shade.domain.interactor.ShadeInteractor
32 import com.google.android.msdl.data.model.MSDLToken
33 import com.google.android.msdl.domain.MSDLPlayer
34 import dagger.assisted.Assisted
35 import dagger.assisted.AssistedFactory
36 import dagger.assisted.AssistedInject
37 import kotlinx.coroutines.awaitCancellation
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.combine
40 import kotlinx.coroutines.flow.distinctUntilChanged
41 import kotlinx.coroutines.flow.filter
42 
43 /**
44  * Models haptics UI state for the scene container.
45  *
46  * This model gets a [View] to play haptics using the [View.performHapticFeedback] API. This should
47  * be the only purpose of this reference.
48  */
49 class SceneContainerHapticsViewModel
50 @AssistedInject
51 constructor(
52     @Assisted private val view: View,
53     sceneInteractor: SceneInteractor,
54     shadeInteractor: ShadeInteractor,
55     private val msdlPlayer: MSDLPlayer,
56 ) : ExclusiveActivatable() {
57 
58     /** Should haptics be played by pulling down the shade */
59     private val isShadePullHapticsRequired: Flow<Boolean> =
60         combine(shadeInteractor.isUserInteracting, sceneInteractor.transitionState) {
61                 interacting,
62                 transitionState ->
63                 interacting && transitionState.isValidForShadePullHaptics()
64             }
65             .distinctUntilChanged()
66 
67     override suspend fun onActivated(): Nothing {
68         playShadePullHaptics()
69         awaitCancellation()
70     }
71 
72     /**
73      * Returns a handler for reveal transition haptics in [SceneContainer] for the given
74      * [hapticFeedback] target.
75      */
76     fun getRevealHaptics(hapticFeedback: HapticFeedback): ContainerRevealHaptics {
77         return object : ContainerRevealHaptics {
78             override fun onRevealThresholdCrossed(revealed: Boolean) {
79                 if (revealed) {
80                     hapticFeedback.performHapticFeedback(
81                         HapticFeedbackType.GestureThresholdActivate
82                     )
83                 }
84             }
85         }
86     }
87 
88     private suspend fun playShadePullHaptics() {
89         isShadePullHapticsRequired
90             .filter { it }
91             .collect {
92                 if (Flags.msdlFeedback()) {
93                     msdlPlayer.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
94                 } else {
95                     view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START)
96                 }
97             }
98     }
99 
100     private fun ObservableTransitionState.isValidForShadePullHaptics(): Boolean {
101         val validOrigin =
102             isTransitioning(from = Scenes.Gone) || isTransitioning(from = Scenes.Lockscreen)
103         val validDestination =
104             isTransitioning(to = Scenes.Shade) ||
105                 isTransitioning(to = Scenes.QuickSettings) ||
106                 isTransitioning(to = Overlays.QuickSettingsShade) ||
107                 isTransitioning(to = Overlays.NotificationsShade)
108         return validOrigin && validDestination
109     }
110 
111     @AssistedFactory
112     interface Factory {
113         fun create(view: View): SceneContainerHapticsViewModel
114     }
115 }
116