diff options
| author | 2024-08-16 09:23:08 +0000 | |
|---|---|---|
| committer | 2024-08-16 09:23:08 +0000 | |
| commit | 6dd0ed5a671b78f81c5be6fa951934fa94d683d4 (patch) | |
| tree | 98ddce50faf6fa9a17305bd4a9a2a9fb7d8d74ce | |
| parent | 2e761f5782a8b3e1a9445d04b8d4683bd2e7803a (diff) | |
| parent | 114796ce92524adb6298a6da0f570baa31cc6a92 (diff) | |
Merge "Adding proper OOBE flow for keyboard and touchpad tutorial" into main
14 files changed, 738 insertions, 136 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/view/KeyboardTouchpadTutorialActivity.kt deleted file mode 100644 index 3e382d669e5d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/view/KeyboardTouchpadTutorialActivity.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.inputdevice.tutorial.ui.view - -import android.os.Bundle -import android.view.WindowManager -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels -import androidx.compose.runtime.Composable -import com.android.compose.theme.PlatformTheme -import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider -import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel -import java.util.Optional -import javax.inject.Inject - -/** - * Activity for out of the box experience for keyboard and touchpad. Note that it's possible that - * either of them are actually not connected when this is launched - */ -class KeyboardTouchpadTutorialActivity -@Inject -constructor( - private val viewModelFactory: KeyboardTouchpadTutorialViewModel.Factory, - private val touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>, -) : ComponentActivity() { - - private val vm by - viewModels<KeyboardTouchpadTutorialViewModel>(factoryProducer = { viewModelFactory }) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - PlatformTheme { - KeyboardTouchpadTutorialContainer(vm, touchpadTutorialScreensProvider) { finish() } - } - } - // required to handle 3+ fingers on touchpad - window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) - } - - override fun onResume() { - super.onResume() - vm.onOpened() - } - - override fun onPause() { - super.onPause() - vm.onClosed() - } -} - -@Composable -fun KeyboardTouchpadTutorialContainer( - vm: KeyboardTouchpadTutorialViewModel, - touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>, - closeTutorial: () -> Unit -) {} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt deleted file mode 100644 index 39b1ec0f0390..000000000000 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.inputdevice.tutorial.ui.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor -import java.util.Optional -import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -class KeyboardTouchpadTutorialViewModel( - private val gesturesInteractor: Optional<TouchpadGesturesInteractor> -) : ViewModel() { - - private val _screen = MutableStateFlow(Screen.BACK_GESTURE) - val screen: StateFlow<Screen> = _screen - - fun goTo(screen: Screen) { - _screen.value = screen - } - - fun onOpened() { - gesturesInteractor.ifPresent { it.disableGestures() } - } - - fun onClosed() { - gesturesInteractor.ifPresent { it.enableGestures() } - } - - class Factory - @Inject - constructor(private val gesturesInteractor: Optional<TouchpadGesturesInteractor>) : - ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun <T : ViewModel> create(modelClass: Class<T>): T { - return KeyboardTouchpadTutorialViewModel(gesturesInteractor) as T - } - } -} - -enum class Screen { - BACK_GESTURE, - HOME_GESTURE, - ACTION_KEY -} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialModule.kt index 8e6cb077a25e..8e6cb077a25e 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadTutorialModule.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialModule.kt diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/TouchpadTutorialScreensProvider.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/TouchpadTutorialScreensProvider.kt index bd3e771f40bc..bd3e771f40bc 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/TouchpadTutorialScreensProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/TouchpadTutorialScreensProvider.kt diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/KeyboardTouchpadConnectionInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/KeyboardTouchpadConnectionInteractor.kt new file mode 100644 index 000000000000..3f1f68ad2897 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/KeyboardTouchpadConnectionInteractor.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.inputdevice.tutorial.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.data.repository.KeyboardRepository +import com.android.systemui.touchpad.data.repository.TouchpadRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +@SysUISingleton +class KeyboardTouchpadConnectionInteractor +@Inject +constructor( + keyboardRepository: KeyboardRepository, + touchpadRepository: TouchpadRepository, +) { + + val connectionState: Flow<ConnectionState> = + combine( + keyboardRepository.isAnyKeyboardConnected, + touchpadRepository.isAnyTouchpadConnected + ) { keyboardConnected, touchpadConnected -> + ConnectionState(keyboardConnected, touchpadConnected) + } +} + +data class ConnectionState(val keyboardConnected: Boolean, val touchpadConnected: Boolean) diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt index c5b0ca78d65a..c5b0ca78d65a 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt index c50b7dc06265..c50b7dc06265 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialComponents.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt index 01ad585019d2..01ad585019d2 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialComponents.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt index 0406bb9e6fef..0406bb9e6fef 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialScreenConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt new file mode 100644 index 000000000000..34ecc9518e83 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.inputdevice.tutorial.ui.view + +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.Lifecycle.State.STARTED +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.android.compose.theme.PlatformTheme +import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider +import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel.Factory.ViewModelFactoryAssistedProvider +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.ACTION_KEY +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.BACK_GESTURE +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.HOME_GESTURE +import java.util.Optional +import javax.inject.Inject +import kotlinx.coroutines.launch + +/** + * Activity for out of the box experience for keyboard and touchpad. Note that it's possible that + * either of them are actually not connected when this is launched + */ +class KeyboardTouchpadTutorialActivity +@Inject +constructor( + private val viewModelFactoryAssistedProvider: ViewModelFactoryAssistedProvider, + private val touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>, +) : ComponentActivity() { + + companion object { + const val INTENT_TUTORIAL_TYPE_KEY = "tutorial_type" + const val INTENT_TUTORIAL_TYPE_TOUCHPAD = "touchpad" + const val INTENT_TUTORIAL_TYPE_KEYBOARD = "keyboard" + } + + private val vm by + viewModels<KeyboardTouchpadTutorialViewModel>( + factoryProducer = { + viewModelFactoryAssistedProvider.create(touchpadTutorialScreensProvider.isPresent) + } + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + // required to handle 3+ fingers on touchpad + window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + lifecycle.addObserver(vm) + lifecycleScope.launch { + vm.closeActivity.collect { finish -> + if (finish) { + finish() + } + } + } + setContent { + PlatformTheme { KeyboardTouchpadTutorialContainer(vm, touchpadTutorialScreensProvider) } + } + } +} + +@Composable +fun KeyboardTouchpadTutorialContainer( + vm: KeyboardTouchpadTutorialViewModel, + touchpadScreens: Optional<TouchpadTutorialScreensProvider>, +) { + val activeScreen by vm.screen.collectAsStateWithLifecycle(STARTED) + when (activeScreen) { + BACK_GESTURE -> + touchpadScreens + .get() + .BackGesture(onDoneButtonClicked = vm::onDoneButtonClicked, onBack = vm::onBack) + HOME_GESTURE -> + touchpadScreens + .get() + .HomeGesture(onDoneButtonClicked = vm::onDoneButtonClicked, onBack = vm::onBack) + ACTION_KEY -> + ActionKeyTutorialScreen( + onDoneButtonClicked = vm::onDoneButtonClicked, + onBack = vm::onBack + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt new file mode 100644 index 000000000000..315c102e94d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.inputdevice.tutorial.ui.viewmodel + +import androidx.lifecycle.AbstractSavedStateViewModelFactory +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.android.systemui.inputdevice.tutorial.domain.interactor.ConnectionState +import com.android.systemui.inputdevice.tutorial.domain.interactor.KeyboardTouchpadConnectionInteractor +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.RequiredHardware.KEYBOARD +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.RequiredHardware.TOUCHPAD +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.ACTION_KEY +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.BACK_GESTURE +import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.Optional +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.launch + +class KeyboardTouchpadTutorialViewModel( + private val gesturesInteractor: Optional<TouchpadGesturesInteractor>, + private val keyboardTouchpadConnectionInteractor: KeyboardTouchpadConnectionInteractor, + private val hasTouchpadTutorialScreens: Boolean, + handle: SavedStateHandle +) : ViewModel(), DefaultLifecycleObserver { + + private fun startingScreen(handle: SavedStateHandle): Screen { + val tutorialType: String? = handle[INTENT_TUTORIAL_TYPE_KEY] + return if (tutorialType == INTENT_TUTORIAL_TYPE_KEYBOARD) ACTION_KEY else BACK_GESTURE + } + + private val _screen = MutableStateFlow(startingScreen(handle)) + val screen: Flow<Screen> = _screen.filter { it.canBeShown() } + + private val _closeActivity: MutableStateFlow<Boolean> = MutableStateFlow(false) + val closeActivity: StateFlow<Boolean> = _closeActivity + + private val screensBackStack = ArrayDeque(listOf(_screen.value)) + + private var connectionState: ConnectionState = + ConnectionState(keyboardConnected = false, touchpadConnected = false) + + init { + viewModelScope.launch { + keyboardTouchpadConnectionInteractor.connectionState.collect { connectionState = it } + } + + viewModelScope.launch { + screen + .runningFold<Screen, Pair<Screen?, Screen?>>(null to null) { + previousScreensPair, + currentScreen -> + previousScreensPair.second to currentScreen + } + .collect { (previousScreen, currentScreen) -> + // ignore first empty emission + if (currentScreen != null) { + setupDeviceState(previousScreen, currentScreen) + } + } + } + + viewModelScope.launch { + // close activity if screen requires touchpad but we don't have it. This can only happen + // when current sysui build doesn't contain touchpad module dependency + _screen.filterNot { it.canBeShown() }.collect { _closeActivity.value = true } + } + } + + override fun onCleared() { + // this shouldn't be needed as onTutorialInvisible should already clear device state but + // it'd be really bad if we'd block gestures/shortcuts after leaving tutorial so just to be + // extra sure... + clearDeviceStateForScreen(_screen.value) + } + + override fun onStart(owner: LifecycleOwner) { + setupDeviceState(previousScreen = null, currentScreen = _screen.value) + } + + override fun onStop(owner: LifecycleOwner) { + clearDeviceStateForScreen(_screen.value) + } + + fun onDoneButtonClicked() { + var nextScreen = _screen.value.next() + while (nextScreen != null) { + if (requiredHardwarePresent(nextScreen)) { + break + } + nextScreen = nextScreen.next() + } + if (nextScreen == null) { + _closeActivity.value = true + } else { + _screen.value = nextScreen + screensBackStack.add(nextScreen) + } + } + + private fun Screen.canBeShown() = requiredHardware != TOUCHPAD || hasTouchpadTutorialScreens + + private fun setupDeviceState(previousScreen: Screen?, currentScreen: Screen) { + if (previousScreen?.requiredHardware == currentScreen.requiredHardware) return + previousScreen?.let { clearDeviceStateForScreen(it) } + when (currentScreen.requiredHardware) { + TOUCHPAD -> gesturesInteractor.get().disableGestures() + KEYBOARD -> {} // TODO(b/358587037) disabled keyboard shortcuts + } + } + + private fun clearDeviceStateForScreen(screen: Screen) { + when (screen.requiredHardware) { + TOUCHPAD -> gesturesInteractor.get().enableGestures() + KEYBOARD -> {} // TODO(b/358587037) enable keyboard shortcuts + } + } + + private fun requiredHardwarePresent(screen: Screen): Boolean = + when (screen.requiredHardware) { + KEYBOARD -> connectionState.keyboardConnected + TOUCHPAD -> connectionState.touchpadConnected + } + + fun onBack() { + if (screensBackStack.size <= 1) { + _closeActivity.value = true + } else { + screensBackStack.removeLast() + _screen.value = screensBackStack.last() + } + } + + class Factory + @AssistedInject + constructor( + private val gesturesInteractor: Optional<TouchpadGesturesInteractor>, + private val keyboardTouchpadConnected: KeyboardTouchpadConnectionInteractor, + @Assisted private val hasTouchpadTutorialScreens: Boolean, + ) : AbstractSavedStateViewModelFactory() { + + @AssistedFactory + fun interface ViewModelFactoryAssistedProvider { + fun create(@Assisted hasTouchpadTutorialScreens: Boolean): Factory + } + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + key: String, + modelClass: Class<T>, + handle: SavedStateHandle + ): T = + KeyboardTouchpadTutorialViewModel( + gesturesInteractor, + keyboardTouchpadConnected, + hasTouchpadTutorialScreens, + handle + ) + as T + } +} + +enum class RequiredHardware { + TOUCHPAD, + KEYBOARD +} + +enum class Screen(val requiredHardware: RequiredHardware) { + BACK_GESTURE(requiredHardware = TOUCHPAD), + HOME_GESTURE(requiredHardware = TOUCHPAD), + ACTION_KEY(requiredHardware = KEYBOARD); + + fun next(): Screen? = + when (this) { + BACK_GESTURE -> HOME_GESTURE + HOME_GESTURE -> ACTION_KEY + ACTION_KEY -> null + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt new file mode 100644 index 000000000000..0c716137f434 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/ui/viewmodel/KeyboardTouchpadTutorialViewModelTest.kt @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.inputdevice.tutorial.ui.viewmodel + +import androidx.lifecycle.Lifecycle.Event +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.SavedStateHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.inputdevice.tutorial.domain.interactor.KeyboardTouchpadConnectionInteractor +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_TOUCHPAD +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.ACTION_KEY +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.BACK_GESTURE +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.HOME_GESTURE +import com.android.systemui.keyboard.data.repository.keyboardRepository +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.model.sysUiState +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED +import com.android.systemui.testKosmos +import com.android.systemui.touchpad.data.repository.TouchpadRepository +import com.android.systemui.touchpad.tutorial.touchpadGesturesInteractor +import com.android.systemui.util.coroutines.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyboardTouchpadTutorialViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sysUiState = kosmos.sysUiState + private val touchpadRepo = PrettyFakeTouchpadRepository() + private val keyboardRepo = kosmos.keyboardRepository + private var startingPeripheral = INTENT_TUTORIAL_TYPE_TOUCHPAD + private val viewModel by lazy { createViewModel(startingPeripheral) } + + // createUnsafe so its methods don't have to be called on Main thread + private val lifecycle = LifecycleRegistry.createUnsafe(mock(LifecycleOwner::class.java)) + + @get:Rule val mainDispatcherRule = MainDispatcherRule(kosmos.testDispatcher) + + private fun createViewModel( + startingPeripheral: String = INTENT_TUTORIAL_TYPE_TOUCHPAD, + hasTouchpadTutorialScreens: Boolean = true, + ): KeyboardTouchpadTutorialViewModel { + val viewModel = + KeyboardTouchpadTutorialViewModel( + Optional.of(kosmos.touchpadGesturesInteractor), + KeyboardTouchpadConnectionInteractor(keyboardRepo, touchpadRepo), + hasTouchpadTutorialScreens, + SavedStateHandle(mapOf(INTENT_TUTORIAL_TYPE_KEY to startingPeripheral)) + ) + lifecycle.addObserver(viewModel) + return viewModel + } + + @Test + fun screensOrder_whenTouchpadAndKeyboardConnected() = + testScope.runTest { + val screens by collectValues(viewModel.screen) + val closeActivity by collectLastValue(viewModel.closeActivity) + peripheralsState(keyboardConnected = true, touchpadConnected = true) + + goToNextScreen() + goToNextScreen() + // reached the last screen + + assertThat(screens).containsExactly(BACK_GESTURE, HOME_GESTURE, ACTION_KEY).inOrder() + assertThat(closeActivity).isFalse() + } + + @Test + fun screensOrder_whenKeyboardDisconnectsDuringTutorial() = + testScope.runTest { + val screens by collectValues(viewModel.screen) + val closeActivity by collectLastValue(viewModel.closeActivity) + peripheralsState(keyboardConnected = true, touchpadConnected = true) + + // back gesture screen + goToNextScreen() + // home gesture screen + peripheralsState(keyboardConnected = false, touchpadConnected = true) + goToNextScreen() + // no action key screen because keyboard disconnected + + assertThat(screens).containsExactly(BACK_GESTURE, HOME_GESTURE).inOrder() + assertThat(closeActivity).isTrue() + } + + @Test + fun screensOrderUntilFinish_whenTouchpadAndKeyboardConnected() = + testScope.runTest { + val screens by collectValues(viewModel.screen) + val closeActivity by collectLastValue(viewModel.closeActivity) + + peripheralsState(keyboardConnected = true, touchpadConnected = true) + + goToNextScreen() + goToNextScreen() + // we're at the last screen so "next screen" should be actually closing activity + goToNextScreen() + + assertThat(screens).containsExactly(BACK_GESTURE, HOME_GESTURE, ACTION_KEY).inOrder() + assertThat(closeActivity).isTrue() + } + + @Test + fun screensOrder_whenGoingBackToPreviousScreens() = + testScope.runTest { + val screens by collectValues(viewModel.screen) + val closeActivity by collectLastValue(viewModel.closeActivity) + peripheralsState(keyboardConnected = true, touchpadConnected = true) + + // back gesture + goToNextScreen() + // home gesture + goToNextScreen() + // action key + + goBack() + // home gesture + goBack() + // back gesture + goBack() + // finish activity + + assertThat(screens) + .containsExactly(BACK_GESTURE, HOME_GESTURE, ACTION_KEY, HOME_GESTURE, BACK_GESTURE) + .inOrder() + assertThat(closeActivity).isTrue() + } + + @Test + fun screensOrder_whenGoingBackAndOnlyKeyboardConnected() = + testScope.runTest { + startingPeripheral = INTENT_TUTORIAL_TYPE_KEYBOARD + val screens by collectValues(viewModel.screen) + val closeActivity by collectLastValue(viewModel.closeActivity) + peripheralsState(keyboardConnected = true, touchpadConnected = false) + + // action key screen + goBack() + // activity finished + + assertThat(screens).containsExactly(ACTION_KEY).inOrder() + assertThat(closeActivity).isTrue() + } + + @Test + fun screensOrder_whenTouchpadConnected() = + testScope.runTest { + startingPeripheral = INTENT_TUTORIAL_TYPE_TOUCHPAD + val screens by collectValues(viewModel.screen) + val closeActivity by collectLastValue(viewModel.closeActivity) + + peripheralsState(keyboardConnected = false, touchpadConnected = true) + + goToNextScreen() + goToNextScreen() + goToNextScreen() + + assertThat(screens).containsExactly(BACK_GESTURE, HOME_GESTURE).inOrder() + assertThat(closeActivity).isTrue() + } + + @Test + fun screensOrder_whenKeyboardConnected() = + testScope.runTest { + startingPeripheral = INTENT_TUTORIAL_TYPE_KEYBOARD + val screens by collectValues(viewModel.screen) + val closeActivity by collectLastValue(viewModel.closeActivity) + + peripheralsState(keyboardConnected = true) + + goToNextScreen() + goToNextScreen() + + assertThat(screens).containsExactly(ACTION_KEY).inOrder() + assertThat(closeActivity).isTrue() + } + + @Test + fun touchpadGesturesDisabled_onlyDuringTouchpadTutorial() = + testScope.runTest { + startingPeripheral = INTENT_TUTORIAL_TYPE_TOUCHPAD + collectValues(viewModel.screen) // just to initialize viewModel + peripheralsState(keyboardConnected = true, touchpadConnected = true) + + assertGesturesDisabled() + goToNextScreen() + goToNextScreen() + // end of touchpad tutorial, keyboard tutorial starts + assertGesturesNotDisabled() + } + + @Test + fun activityFinishes_ifTouchpadModuleIsNotPresent() = + testScope.runTest { + val viewModel = + createViewModel( + startingPeripheral = INTENT_TUTORIAL_TYPE_TOUCHPAD, + hasTouchpadTutorialScreens = false + ) + val screens by collectValues(viewModel.screen) + val closeActivity by collectLastValue(viewModel.closeActivity) + peripheralsState(touchpadConnected = true) + + assertThat(screens).isEmpty() + assertThat(closeActivity).isTrue() + } + + @Test + fun touchpadGesturesDisabled_whenTutorialGoesToForeground() = + testScope.runTest { + startingPeripheral = INTENT_TUTORIAL_TYPE_TOUCHPAD + collectValues(viewModel.screen) // just to initialize viewModel + peripheralsState(touchpadConnected = true) + + lifecycle.handleLifecycleEvent(Event.ON_START) + + assertGesturesDisabled() + } + + @Test + fun touchpadGesturesNotDisabled_whenTutorialGoesToBackground() = + testScope.runTest { + startingPeripheral = INTENT_TUTORIAL_TYPE_TOUCHPAD + collectValues(viewModel.screen) + peripheralsState(touchpadConnected = true) + + lifecycle.handleLifecycleEvent(Event.ON_START) + lifecycle.handleLifecycleEvent(Event.ON_STOP) + + assertGesturesNotDisabled() + } + + @Test + fun keyboardShortcutsDisabled_onlyDuringKeyboardTutorial() = + testScope.runTest { + // TODO(b/358587037) + } + + private fun TestScope.goToNextScreen() { + viewModel.onDoneButtonClicked() + runCurrent() + } + + private fun TestScope.goBack() { + viewModel.onBack() + runCurrent() + } + + private fun TestScope.peripheralsState( + keyboardConnected: Boolean = false, + touchpadConnected: Boolean = false + ) { + keyboardRepo.setIsAnyKeyboardConnected(keyboardConnected) + touchpadRepo.setIsAnyTouchpadConnected(touchpadConnected) + runCurrent() + } + + private fun TestScope.assertGesturesNotDisabled() = assertFlagEnabled(enabled = false) + + private fun TestScope.assertGesturesDisabled() = assertFlagEnabled(enabled = true) + + private fun TestScope.assertFlagEnabled(enabled: Boolean) { + // sysui state is changed on background scope so let's make sure it's executed + runCurrent() + assertThat(sysUiState.isFlagEnabled(SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED)) + .isEqualTo(enabled) + } + + // replace below when we have better fake + internal class PrettyFakeTouchpadRepository : TouchpadRepository { + + private val _isAnyTouchpadConnected = MutableStateFlow(false) + override val isAnyTouchpadConnected: Flow<Boolean> = _isAnyTouchpadConnected + + fun setIsAnyTouchpadConnected(connected: Boolean) { + _isAnyTouchpadConnected.value = connected + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialKosmos.kt new file mode 100644 index 000000000000..f502df0b4075 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.touchpad.tutorial + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.model.sysUiState +import com.android.systemui.settings.displayTracker +import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor + +var Kosmos.touchpadGesturesInteractor: TouchpadGesturesInteractor by + Kosmos.Fixture { + TouchpadGesturesInteractor(sysUiState, displayTracker, testScope.backgroundScope) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/coroutines/MainDispatcherRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/coroutines/MainDispatcherRule.kt new file mode 100644 index 000000000000..577620347991 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/coroutines/MainDispatcherRule.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.coroutines + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +/** + * Overrides main dispatcher to passed testDispatcher. You probably want to use it when using + * viewModelScope which has hardcoded main dispatcher. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class MainDispatcherRule(val testDispatcher: TestDispatcher) : TestWatcher() { + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description) { + Dispatchers.resetMain() + } +} |