1 /* 2 * 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.SavedStateHandle 20 import androidx.lifecycle.testing.TestLifecycleOwner 21 import androidx.test.ext.junit.runners.AndroidJUnit4 22 import androidx.test.filters.SmallTest 23 import com.android.systemui.SysuiTestCase 24 import com.android.systemui.coroutines.collectLastValue 25 import com.android.systemui.coroutines.collectValues 26 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger 27 import com.android.systemui.inputdevice.tutorial.domain.interactor.KeyboardTouchpadConnectionInteractor 28 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_KEY 29 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_KEYBOARD 30 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_TOUCHPAD 31 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_TOUCHPAD_BACK 32 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_TOUCHPAD_HOME 33 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.ACTION_KEY 34 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.BACK_GESTURE 35 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.Screen.HOME_GESTURE 36 import com.android.systemui.keyboard.data.repository.keyboardRepository 37 import com.android.systemui.kosmos.testDispatcher 38 import com.android.systemui.kosmos.testScope 39 import com.android.systemui.model.sysUiState 40 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED 41 import com.android.systemui.testKosmos 42 import com.android.systemui.touchpad.data.repository.TouchpadRepository 43 import com.android.systemui.touchpad.tutorial.touchpadGesturesInteractor 44 import com.android.systemui.util.coroutines.MainDispatcherRule 45 import com.google.common.truth.Truth.assertThat 46 import java.util.Optional 47 import kotlinx.coroutines.ExperimentalCoroutinesApi 48 import kotlinx.coroutines.flow.Flow 49 import kotlinx.coroutines.flow.MutableStateFlow 50 import kotlinx.coroutines.test.TestScope 51 import kotlinx.coroutines.test.runCurrent 52 import kotlinx.coroutines.test.runTest 53 import org.junit.Rule 54 import org.junit.Test 55 import org.junit.runner.RunWith 56 import org.mockito.kotlin.mock 57 58 @OptIn(ExperimentalCoroutinesApi::class) 59 @SmallTest 60 @RunWith(AndroidJUnit4::class) 61 class KeyboardTouchpadTutorialViewModelTest : SysuiTestCase() { 62 63 private val kosmos = testKosmos() 64 private val testScope = kosmos.testScope 65 private val sysUiState = kosmos.sysUiState 66 private val touchpadRepo = PrettyFakeTouchpadRepository() 67 private val keyboardRepo = kosmos.keyboardRepository 68 private var tutorialScope = INTENT_TUTORIAL_SCOPE_TOUCHPAD <lambda>null69 private val viewModel by lazy { createViewModel(tutorialScope) } 70 71 @get:Rule val mainDispatcherRule = MainDispatcherRule(kosmos.testDispatcher) 72 createViewModelnull73 private fun createViewModel( 74 scope: String = INTENT_TUTORIAL_SCOPE_TOUCHPAD, 75 hasTouchpadTutorialScreens: Boolean = true, 76 ): KeyboardTouchpadTutorialViewModel { 77 val viewModel = 78 KeyboardTouchpadTutorialViewModel( 79 Optional.of(kosmos.touchpadGesturesInteractor), 80 KeyboardTouchpadConnectionInteractor(keyboardRepo, touchpadRepo), 81 hasTouchpadTutorialScreens, 82 mock<InputDeviceTutorialLogger>(), 83 SavedStateHandle(mapOf(INTENT_TUTORIAL_SCOPE_KEY to scope)), 84 ) 85 return viewModel 86 } 87 88 @Test screensOrder_whenTouchpadAndKeyboardConnectednull89 fun screensOrder_whenTouchpadAndKeyboardConnected() = 90 testScope.runTest { 91 val screens by collectValues(viewModel.screen) 92 val closeActivity by collectLastValue(viewModel.closeActivity) 93 peripheralsState(keyboardConnected = true, touchpadConnected = true) 94 95 goToNextScreen() 96 goToNextScreen() 97 // reached the last screen 98 99 assertThat(screens).containsExactly(BACK_GESTURE, HOME_GESTURE, ACTION_KEY).inOrder() 100 assertThat(closeActivity).isFalse() 101 } 102 103 @Test screensOrder_whenKeyboardDisconnectsDuringTutorialnull104 fun screensOrder_whenKeyboardDisconnectsDuringTutorial() = 105 testScope.runTest { 106 val screens by collectValues(viewModel.screen) 107 val closeActivity by collectLastValue(viewModel.closeActivity) 108 peripheralsState(keyboardConnected = true, touchpadConnected = true) 109 110 // back gesture screen 111 goToNextScreen() 112 // home gesture screen 113 peripheralsState(keyboardConnected = false, touchpadConnected = true) 114 goToNextScreen() 115 // no action key screen because keyboard disconnected 116 117 assertThat(screens).containsExactly(BACK_GESTURE, HOME_GESTURE).inOrder() 118 assertThat(closeActivity).isTrue() 119 } 120 121 @Test screensOrderUntilFinish_whenTouchpadAndKeyboardConnectednull122 fun screensOrderUntilFinish_whenTouchpadAndKeyboardConnected() = 123 testScope.runTest { 124 val screens by collectValues(viewModel.screen) 125 val closeActivity by collectLastValue(viewModel.closeActivity) 126 127 peripheralsState(keyboardConnected = true, touchpadConnected = true) 128 129 goToNextScreen() 130 goToNextScreen() 131 // we're at the last screen so "next screen" should be actually closing activity 132 goToNextScreen() 133 134 assertThat(screens).containsExactly(BACK_GESTURE, HOME_GESTURE, ACTION_KEY).inOrder() 135 assertThat(closeActivity).isTrue() 136 } 137 138 @Test screensOrder_whenGoingBackToPreviousScreensnull139 fun screensOrder_whenGoingBackToPreviousScreens() = 140 testScope.runTest { 141 val screens by collectValues(viewModel.screen) 142 val closeActivity by collectLastValue(viewModel.closeActivity) 143 peripheralsState(keyboardConnected = true, touchpadConnected = true) 144 145 // back gesture 146 goToNextScreen() 147 // home gesture 148 goToNextScreen() 149 // action key 150 151 goBack() 152 // home gesture 153 goBack() 154 // back gesture 155 goBack() 156 // finish activity 157 158 assertThat(screens) 159 .containsExactly(BACK_GESTURE, HOME_GESTURE, ACTION_KEY, HOME_GESTURE, BACK_GESTURE) 160 .inOrder() 161 assertThat(closeActivity).isTrue() 162 } 163 164 @Test screensOrder_whenGoingBackAndOnlyKeyboardConnectednull165 fun screensOrder_whenGoingBackAndOnlyKeyboardConnected() = 166 testScope.runTest { 167 tutorialScope = INTENT_TUTORIAL_SCOPE_KEYBOARD 168 val screens by collectValues(viewModel.screen) 169 val closeActivity by collectLastValue(viewModel.closeActivity) 170 peripheralsState(keyboardConnected = true, touchpadConnected = false) 171 172 // action key screen 173 goBack() 174 // activity finished 175 176 assertThat(screens).containsExactly(ACTION_KEY).inOrder() 177 assertThat(closeActivity).isTrue() 178 } 179 180 @Test screensOrder_whenTouchpadConnectednull181 fun screensOrder_whenTouchpadConnected() = 182 testScope.runTest { 183 tutorialScope = INTENT_TUTORIAL_SCOPE_TOUCHPAD 184 val screens by collectValues(viewModel.screen) 185 val closeActivity by collectLastValue(viewModel.closeActivity) 186 187 peripheralsState(keyboardConnected = false, touchpadConnected = true) 188 189 goToNextScreen() 190 goToNextScreen() 191 192 assertThat(screens).containsExactly(BACK_GESTURE, HOME_GESTURE).inOrder() 193 assertThat(closeActivity).isTrue() 194 } 195 196 @Test screensOrder_withBackGestureScopenull197 fun screensOrder_withBackGestureScope() = 198 testScope.runTest { 199 tutorialScope = INTENT_TUTORIAL_SCOPE_TOUCHPAD_BACK 200 val screens by collectValues(viewModel.screen) 201 val closeActivity by collectLastValue(viewModel.closeActivity) 202 peripheralsState(touchpadConnected = true) 203 204 goToNextScreen() 205 206 assertThat(screens).containsExactly(BACK_GESTURE).inOrder() 207 assertThat(closeActivity).isTrue() 208 } 209 210 @Test screensOrder_withHomeGestureScopenull211 fun screensOrder_withHomeGestureScope() = 212 testScope.runTest { 213 tutorialScope = INTENT_TUTORIAL_SCOPE_TOUCHPAD_HOME 214 val screens by collectValues(viewModel.screen) 215 val closeActivity by collectLastValue(viewModel.closeActivity) 216 peripheralsState(touchpadConnected = true) 217 218 goToNextScreen() 219 220 assertThat(screens).containsExactly(HOME_GESTURE).inOrder() 221 assertThat(closeActivity).isTrue() 222 } 223 224 @Test screensOrder_withKeyboardScopenull225 fun screensOrder_withKeyboardScope() = 226 testScope.runTest { 227 tutorialScope = INTENT_TUTORIAL_SCOPE_KEYBOARD 228 val screens by collectValues(viewModel.screen) 229 val closeActivity by collectLastValue(viewModel.closeActivity) 230 peripheralsState(keyboardConnected = true) 231 232 goToNextScreen() 233 234 assertThat(screens).containsExactly(ACTION_KEY).inOrder() 235 assertThat(closeActivity).isTrue() 236 } 237 238 @Test touchpadGesturesDisabled_onlyDuringTouchpadTutorialnull239 fun touchpadGesturesDisabled_onlyDuringTouchpadTutorial() = 240 testScope.runTest { 241 tutorialScope = INTENT_TUTORIAL_SCOPE_TOUCHPAD 242 collectValues(viewModel.screen) // just to initialize viewModel 243 peripheralsState(keyboardConnected = true, touchpadConnected = true) 244 245 assertGesturesDisabled() 246 goToNextScreen() 247 goToNextScreen() 248 // end of touchpad tutorial, keyboard tutorial starts 249 assertGesturesNotDisabled() 250 } 251 252 @Test activityFinishes_ifTouchpadModuleIsNotPresentnull253 fun activityFinishes_ifTouchpadModuleIsNotPresent() = 254 testScope.runTest { 255 val viewModel = 256 createViewModel( 257 scope = INTENT_TUTORIAL_SCOPE_TOUCHPAD, 258 hasTouchpadTutorialScreens = false, 259 ) 260 val screens by collectValues(viewModel.screen) 261 val closeActivity by collectLastValue(viewModel.closeActivity) 262 peripheralsState(touchpadConnected = true) 263 264 assertThat(screens).isEmpty() 265 assertThat(closeActivity).isTrue() 266 } 267 268 @Test touchpadGesturesDisabled_whenTutorialGoesToForegroundnull269 fun touchpadGesturesDisabled_whenTutorialGoesToForeground() = 270 testScope.runTest { 271 tutorialScope = INTENT_TUTORIAL_SCOPE_TOUCHPAD 272 collectValues(viewModel.screen) // just to initialize viewModel 273 peripheralsState(touchpadConnected = true) 274 275 viewModel.onStart(TestLifecycleOwner()) 276 277 assertGesturesDisabled() 278 } 279 280 @Test touchpadGesturesNotDisabled_whenTutorialGoesToBackgroundnull281 fun touchpadGesturesNotDisabled_whenTutorialGoesToBackground() = 282 testScope.runTest { 283 tutorialScope = INTENT_TUTORIAL_SCOPE_TOUCHPAD 284 collectValues(viewModel.screen) 285 peripheralsState(touchpadConnected = true) 286 287 viewModel.onStart(TestLifecycleOwner()) 288 viewModel.onStop(TestLifecycleOwner()) 289 290 assertGesturesNotDisabled() 291 } 292 293 @Test keyboardShortcutsDisabled_onlyDuringKeyboardTutorialnull294 fun keyboardShortcutsDisabled_onlyDuringKeyboardTutorial() = 295 testScope.runTest { 296 // TODO(b/358587037) 297 } 298 TestScopenull299 private fun TestScope.goToNextScreen() { 300 viewModel.onDoneButtonClicked() 301 runCurrent() 302 } 303 goBacknull304 private fun TestScope.goBack() { 305 viewModel.onBack() 306 runCurrent() 307 } 308 peripheralsStatenull309 private fun TestScope.peripheralsState( 310 keyboardConnected: Boolean = false, 311 touchpadConnected: Boolean = false, 312 ) { 313 keyboardRepo.setIsAnyKeyboardConnected(keyboardConnected) 314 touchpadRepo.setIsAnyTouchpadConnected(touchpadConnected) 315 runCurrent() 316 } 317 TestScopenull318 private fun TestScope.assertGesturesNotDisabled() = assertFlagEnabled(enabled = false) 319 320 private fun TestScope.assertGesturesDisabled() = assertFlagEnabled(enabled = true) 321 322 private fun TestScope.assertFlagEnabled(enabled: Boolean) { 323 // sysui state is changed on background scope so let's make sure it's executed 324 runCurrent() 325 assertThat(sysUiState.isFlagEnabled(SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED)) 326 .isEqualTo(enabled) 327 } 328 329 // replace below when we have better fake 330 internal class PrettyFakeTouchpadRepository : TouchpadRepository { 331 332 private val _isAnyTouchpadConnected = MutableStateFlow(false) 333 override val isAnyTouchpadConnected: Flow<Boolean> = _isAnyTouchpadConnected 334 setIsAnyTouchpadConnectednull335 fun setIsAnyTouchpadConnected(connected: Boolean) { 336 _isAnyTouchpadConnected.value = connected 337 } 338 } 339 } 340