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