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.inputdevice.tutorial.ui.viewmodel 18 19 import androidx.lifecycle.AbstractSavedStateViewModelFactory 20 import androidx.lifecycle.DefaultLifecycleObserver 21 import androidx.lifecycle.LifecycleOwner 22 import androidx.lifecycle.SavedStateHandle 23 import androidx.lifecycle.ViewModel 24 import androidx.lifecycle.viewModelScope 25 import com.android.app.tracing.coroutines.launchTraced as launch 26 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger 27 import com.android.systemui.inputdevice.tutorial.domain.interactor.ConnectionState 28 import com.android.systemui.inputdevice.tutorial.domain.interactor.KeyboardTouchpadConnectionInteractor 29 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_ALL 30 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_KEY 31 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_KEYBOARD 32 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_TOUCHPAD 33 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_TOUCHPAD_BACK 34 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_TOUCHPAD_HOME 35 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.RequiredHardware.KEYBOARD 36 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.RequiredHardware.TOUCHPAD 37 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.ACTION_KEY 38 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.BACK_GESTURE 39 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.HOME_GESTURE 40 import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor 41 import dagger.assisted.Assisted 42 import dagger.assisted.AssistedFactory 43 import dagger.assisted.AssistedInject 44 import java.util.Optional 45 import kotlin.time.Duration.Companion.seconds 46 import kotlinx.coroutines.delay 47 import kotlinx.coroutines.flow.Flow 48 import kotlinx.coroutines.flow.MutableStateFlow 49 import kotlinx.coroutines.flow.StateFlow 50 import kotlinx.coroutines.flow.filter 51 import kotlinx.coroutines.flow.filterNot 52 import kotlinx.coroutines.flow.runningFold 53 54 @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) 55 class KeyboardTouchpadTutorialViewModel( 56 private val gesturesInteractor: Optional<TouchpadGesturesInteractor>, 57 private val keyboardTouchpadConnectionInteractor: KeyboardTouchpadConnectionInteractor, 58 private val hasTouchpadTutorialScreens: Boolean, 59 private val logger: InputDeviceTutorialLogger, 60 handle: SavedStateHandle, 61 ) : ViewModel(), DefaultLifecycleObserver { 62 63 private val _screen = MutableStateFlow(startingScreen(handle)) 64 val screen: Flow<Screen> = _screen.filter { it.canBeShown() } 65 66 private val _closeActivity: MutableStateFlow<Boolean> = MutableStateFlow(false) 67 val closeActivity: StateFlow<Boolean> = _closeActivity 68 69 private val screenSequence: ScreenSequence = chooseScreenSequence(handle) 70 71 private val screensBackStack = ArrayDeque(listOf(_screen.value)) 72 73 private var connectionState: ConnectionState = 74 ConnectionState(keyboardConnected = false, touchpadConnected = false) 75 76 init { 77 viewModelScope.launch { 78 keyboardTouchpadConnectionInteractor.connectionState.collect { 79 logger.logNewConnectionState(connectionState) 80 connectionState = it 81 } 82 } 83 84 viewModelScope.launch { 85 screen 86 .runningFold<Screen, Pair<Screen?, Screen?>>(null to null) { 87 previousScreensPair, 88 currentScreen -> 89 previousScreensPair.second to currentScreen 90 } 91 .collect { (previousScreen, currentScreen) -> 92 // ignore first empty emission 93 if (currentScreen != null) { 94 setupDeviceState(previousScreen, currentScreen) 95 } 96 } 97 } 98 99 viewModelScope.launch { 100 // close activity if screen requires touchpad but we don't have it. This can only happen 101 // when current sysui build doesn't contain touchpad module dependency 102 _screen 103 .filterNot { it.canBeShown() } 104 .collect { 105 logger.e( 106 "Touchpad is connected but touchpad module is missing, something went wrong" 107 ) 108 _closeActivity.value = true 109 } 110 } 111 } 112 113 private fun startingScreen(handle: SavedStateHandle): Screen { 114 val scope: String? = handle[INTENT_TUTORIAL_SCOPE_KEY] 115 return when (scope) { 116 INTENT_TUTORIAL_SCOPE_KEYBOARD -> ACTION_KEY 117 INTENT_TUTORIAL_SCOPE_TOUCHPAD_HOME -> HOME_GESTURE 118 INTENT_TUTORIAL_SCOPE_TOUCHPAD, 119 INTENT_TUTORIAL_SCOPE_ALL, 120 INTENT_TUTORIAL_SCOPE_TOUCHPAD_BACK -> BACK_GESTURE 121 else -> { 122 logger.w("Intent didn't specify tutorial scope, starting with default") 123 BACK_GESTURE 124 } 125 } 126 } 127 128 private fun chooseScreenSequence(handle: SavedStateHandle): ScreenSequence { 129 val scope: String? = handle[INTENT_TUTORIAL_SCOPE_KEY] 130 return if ( 131 scope == INTENT_TUTORIAL_SCOPE_TOUCHPAD_HOME || 132 scope == INTENT_TUTORIAL_SCOPE_TOUCHPAD_BACK 133 ) { 134 SingleScreenOnly 135 } else { 136 AllSupportedScreens 137 } 138 } 139 140 override fun onCleared() { 141 // this shouldn't be needed as onTutorialInvisible should already clear device state but 142 // it'd be really bad if we'd block gestures/shortcuts after leaving tutorial so just to be 143 // extra sure... 144 clearDeviceStateForScreen(_screen.value) 145 } 146 147 override fun onStart(owner: LifecycleOwner) { 148 setupDeviceState(previousScreen = null, currentScreen = _screen.value) 149 } 150 151 override fun onStop(owner: LifecycleOwner) { 152 clearDeviceStateForScreen(_screen.value) 153 } 154 155 suspend fun onAutoProceed() { 156 delay(AUTO_PROCEED_DELAY) 157 progressToNextScreen() 158 } 159 160 fun onDoneButtonClicked() { 161 progressToNextScreen() 162 } 163 164 private fun progressToNextScreen() { 165 var nextScreen = screenSequence.nextScreen(_screen.value) 166 while (nextScreen != null) { 167 if (requiredHardwarePresent(nextScreen)) { 168 break 169 } 170 logger.logNextScreenMissingHardware(nextScreen) 171 nextScreen = screenSequence.nextScreen(nextScreen) 172 } 173 if (nextScreen == null) { 174 logger.d("Final screen reached, closing tutorial") 175 _closeActivity.value = true 176 } else { 177 logger.logNextScreen(nextScreen) 178 _screen.value = nextScreen 179 screensBackStack.add(nextScreen) 180 } 181 } 182 183 private fun Screen.canBeShown() = requiredHardware != TOUCHPAD || hasTouchpadTutorialScreens 184 185 private fun setupDeviceState(previousScreen: Screen?, currentScreen: Screen) { 186 logger.logMovingBetweenScreens(previousScreen, currentScreen) 187 if (previousScreen?.requiredHardware == currentScreen.requiredHardware) return 188 previousScreen?.let { clearDeviceStateForScreen(it) } 189 when (currentScreen.requiredHardware) { 190 TOUCHPAD -> gesturesInteractor.get().disableGestures() 191 KEYBOARD -> {} // TODO(b/358587037) disabled keyboard shortcuts 192 } 193 } 194 195 private fun clearDeviceStateForScreen(screen: Screen) { 196 when (screen.requiredHardware) { 197 TOUCHPAD -> gesturesInteractor.get().enableGestures() 198 KEYBOARD -> {} // TODO(b/358587037) enable keyboard shortcuts 199 } 200 } 201 202 private fun requiredHardwarePresent(screen: Screen): Boolean = 203 when (screen.requiredHardware) { 204 KEYBOARD -> connectionState.keyboardConnected 205 TOUCHPAD -> connectionState.touchpadConnected 206 } 207 208 fun onBack() { 209 if (screensBackStack.size <= 1) { 210 _closeActivity.value = true 211 } else { 212 screensBackStack.removeLast() 213 logger.logGoingBack(screensBackStack.last()) 214 _screen.value = screensBackStack.last() 215 } 216 } 217 218 class Factory 219 @AssistedInject 220 constructor( 221 private val gesturesInteractor: Optional<TouchpadGesturesInteractor>, 222 private val keyboardTouchpadConnected: KeyboardTouchpadConnectionInteractor, 223 private val logger: InputDeviceTutorialLogger, 224 @Assisted private val hasTouchpadTutorialScreens: Boolean, 225 ) : AbstractSavedStateViewModelFactory() { 226 227 @AssistedFactory 228 fun interface ViewModelFactoryAssistedProvider { 229 fun create(@Assisted hasTouchpadTutorialScreens: Boolean): Factory 230 } 231 232 @Suppress("UNCHECKED_CAST") 233 override fun <T : ViewModel> create( 234 key: String, 235 modelClass: Class<T>, 236 handle: SavedStateHandle, 237 ): T = 238 KeyboardTouchpadTutorialViewModel( 239 gesturesInteractor, 240 keyboardTouchpadConnected, 241 hasTouchpadTutorialScreens, 242 logger, 243 handle, 244 ) 245 as T 246 } 247 248 private interface ScreenSequence { 249 fun nextScreen(current: Screen): Screen? 250 } 251 252 private object AllSupportedScreens : ScreenSequence { 253 override fun nextScreen(current: Screen): Screen? { 254 return when (current) { 255 BACK_GESTURE -> HOME_GESTURE 256 HOME_GESTURE -> ACTION_KEY 257 ACTION_KEY -> null 258 } 259 } 260 } 261 262 private object SingleScreenOnly : ScreenSequence { 263 override fun nextScreen(current: Screen): Screen? = null 264 } 265 266 companion object { 267 private val AUTO_PROCEED_DELAY = 3.seconds 268 } 269 } 270 271 enum class RequiredHardware { 272 TOUCHPAD, 273 KEYBOARD, 274 } 275 276 enum class Screen(val requiredHardware: RequiredHardware) { 277 BACK_GESTURE(requiredHardware = TOUCHPAD), 278 HOME_GESTURE(requiredHardware = TOUCHPAD), 279 ACTION_KEY(requiredHardware = KEYBOARD), 280 } 281