• 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.msdl.qs
18 
19 import android.service.quicksettings.Tile
20 import androidx.compose.runtime.Stable
21 import com.android.systemui.Flags
22 import com.android.systemui.animation.Expandable
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.lifecycle.ExclusiveActivatable
25 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
26 import com.android.systemui.util.kotlin.pairwise
27 import com.google.android.msdl.data.model.MSDLToken
28 import com.google.android.msdl.domain.MSDLPlayer
29 import dagger.assisted.Assisted
30 import dagger.assisted.AssistedFactory
31 import dagger.assisted.AssistedInject
32 import javax.inject.Inject
33 import kotlinx.coroutines.awaitCancellation
34 import kotlinx.coroutines.flow.Flow
35 import kotlinx.coroutines.flow.MutableStateFlow
36 import kotlinx.coroutines.flow.combine
37 import kotlinx.coroutines.flow.distinctUntilChanged
38 import kotlinx.coroutines.flow.mapLatest
39 import kotlinx.coroutines.flow.merge
40 import kotlinx.coroutines.flow.transform
41 
42 /** A view-model to trigger haptic feedback on Quick Settings tiles */
43 class TileHapticsViewModel
44 @AssistedInject
45 constructor(
46     private val msdlPlayer: MSDLPlayer,
47     @Assisted private val tileViewModel: TileViewModel,
48 ) : ExclusiveActivatable() {
49 
50     private val tileInteractionState = MutableStateFlow(TileInteractionState.IDLE)
51     private val tileAnimationState = MutableStateFlow(TileAnimationState.IDLE)
52     private val canPlayToggleHaptics: Boolean
53         get() =
54             tileAnimationState.value == TileAnimationState.IDLE &&
55                 tileInteractionState.value == TileInteractionState.CLICKED
56 
57     val isIdle: Boolean
58         get() =
59             tileAnimationState.value == TileAnimationState.IDLE &&
60                 tileInteractionState.value == TileInteractionState.IDLE
61 
62     private val toggleHapticsState: Flow<TileHapticsState> =
63         tileViewModel.state
64             .mapLatest { it.state }
65             .pairwise()
66             .transform { (previous, current) ->
67                 val toggleState =
68                     when {
69                         !canPlayToggleHaptics -> TileHapticsState.NO_HAPTICS
70                         previous == Tile.STATE_INACTIVE && current == Tile.STATE_ACTIVE ->
71                             TileHapticsState.TOGGLE_ON
72                         previous == Tile.STATE_ACTIVE && current == Tile.STATE_INACTIVE ->
73                             TileHapticsState.TOGGLE_OFF
74                         else -> TileHapticsState.NO_HAPTICS
75                     }
76                 emit(toggleState)
77             }
78             .distinctUntilChanged()
79 
80     private val interactionHapticsState: Flow<TileHapticsState> =
81         combine(tileInteractionState, tileAnimationState) { interactionState, animationState ->
82                 when {
83                     interactionState == TileInteractionState.LONG_CLICKED &&
84                         animationState == TileAnimationState.ACTIVITY_LAUNCH ->
85                         TileHapticsState.LONG_PRESS
86                     else -> TileHapticsState.NO_HAPTICS
87                 }
88             }
89             .distinctUntilChanged()
90 
91     private val hapticsState: Flow<TileHapticsState> =
92         merge(toggleHapticsState, interactionHapticsState)
93 
94     override suspend fun onActivated(): Nothing {
95         try {
96             hapticsState.collect { hapticsState ->
97                 val tokenToPlay: MSDLToken? =
98                     when (hapticsState) {
99                         TileHapticsState.TOGGLE_ON -> MSDLToken.SWITCH_ON
100                         TileHapticsState.TOGGLE_OFF -> MSDLToken.SWITCH_OFF
101                         TileHapticsState.LONG_PRESS -> MSDLToken.LONG_PRESS
102                         TileHapticsState.NO_HAPTICS -> null
103                     }
104                 tokenToPlay?.let {
105                     msdlPlayer.playToken(it)
106                     resetStates()
107                 }
108             }
109             awaitCancellation()
110         } finally {
111             resetStates()
112         }
113     }
114 
115     private fun resetStates() {
116         tileInteractionState.value = TileInteractionState.IDLE
117         tileAnimationState.value = TileAnimationState.IDLE
118     }
119 
120     fun onDialogDrawingStart() {
121         tileAnimationState.value = TileAnimationState.DIALOG_LAUNCH
122     }
123 
124     fun onDialogDrawingEnd() {
125         tileAnimationState.value = TileAnimationState.IDLE
126     }
127 
128     fun onActivityLaunchTransitionStart() {
129         tileAnimationState.value = TileAnimationState.ACTIVITY_LAUNCH
130     }
131 
132     fun onActivityLaunchTransitionEnd() {
133         tileAnimationState.value = TileAnimationState.IDLE
134     }
135 
136     fun setTileInteractionState(actionState: TileInteractionState) {
137         tileInteractionState.value = actionState
138     }
139 
140     fun createStateAwareExpandable(baseExpandable: Expandable): Expandable =
141         baseExpandable.withStateAwareness(
142             onDialogDrawingStart = ::onDialogDrawingStart,
143             onDialogDrawingEnd = ::onDialogDrawingEnd,
144             onActivityLaunchTransitionStart = ::onActivityLaunchTransitionStart,
145             onActivityLaunchTransitionEnd = ::onActivityLaunchTransitionEnd,
146         )
147 
148     /** Models the state of haptics to play */
149     enum class TileHapticsState {
150         TOGGLE_ON,
151         TOGGLE_OFF,
152         LONG_PRESS,
153         NO_HAPTICS,
154     }
155 
156     /** Models the interaction that took place on the tile */
157     enum class TileInteractionState {
158         IDLE,
159         CLICKED,
160         LONG_CLICKED,
161     }
162 
163     /** Models the animation state of dialogs and activity launches from a tile */
164     enum class TileAnimationState {
165         IDLE,
166         DIALOG_LAUNCH,
167         ACTIVITY_LAUNCH,
168     }
169 
170     @AssistedFactory
171     interface Factory {
172         fun create(tileViewModel: TileViewModel): TileHapticsViewModel
173     }
174 }
175 
176 @SysUISingleton
177 @Stable
178 class TileHapticsViewModelFactoryProvider
179 @Inject
180 constructor(private val tileHapticsViewModelFactory: TileHapticsViewModel.Factory) {
getHapticsViewModelFactorynull181     fun getHapticsViewModelFactory(): TileHapticsViewModel.Factory? =
182         if (Flags.msdlFeedback()) {
183             tileHapticsViewModelFactory
184         } else {
185             null
186         }
187 }
188