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.keyboard.shortcut.ui.viewmodel
18 
19 import android.app.role.RoleManager
20 import android.app.role.mockRoleManager
21 import android.content.Context
22 import android.content.Context.INPUT_SERVICE
23 import android.content.pm.ApplicationInfo
24 import android.content.pm.PackageManager
25 import android.content.pm.PackageManager.NameNotFoundException
26 import android.hardware.input.InputGestureData
27 import android.hardware.input.fakeInputManager
28 import android.view.KeyEvent
29 import android.view.KeyboardShortcutGroup
30 import android.view.KeyboardShortcutInfo
31 import androidx.compose.material.icons.Icons
32 import androidx.compose.material.icons.filled.Tv
33 import androidx.compose.material.icons.filled.VerticalSplit
34 import androidx.test.ext.junit.runners.AndroidJUnit4
35 import androidx.test.filters.SmallTest
36 import com.android.systemui.SysuiTestCase
37 import com.android.systemui.coroutines.collectLastValue
38 import com.android.systemui.coroutines.collectValues
39 import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
40 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
41 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData
42 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
43 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.CurrentApp
44 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
45 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
46 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
47 import com.android.systemui.keyboard.shortcut.shared.model.shortcut
48 import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
49 import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
50 import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
51 import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
52 import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
53 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
54 import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel
55 import com.android.systemui.keyboard.shortcut.ui.model.IconSource
56 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi
57 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
58 import com.android.systemui.kosmos.Kosmos
59 import com.android.systemui.kosmos.testCase
60 import com.android.systemui.kosmos.testDispatcher
61 import com.android.systemui.kosmos.testScope
62 import com.android.systemui.model.sysUiState
63 import com.android.systemui.settings.FakeUserTracker
64 import com.android.systemui.settings.fakeUserTracker
65 import com.android.systemui.settings.userTracker
66 import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SHORTCUT_HELPER_SHOWING
67 import com.google.common.truth.Truth.assertThat
68 import kotlinx.coroutines.ExperimentalCoroutinesApi
69 import kotlinx.coroutines.test.UnconfinedTestDispatcher
70 import kotlinx.coroutines.test.runTest
71 import org.junit.Before
72 import org.junit.Test
73 import org.junit.runner.RunWith
74 import org.mockito.ArgumentMatchers.anyString
75 import org.mockito.kotlin.eq
76 import org.mockito.kotlin.mock
77 import org.mockito.kotlin.whenever
78 
79 @OptIn(ExperimentalCoroutinesApi::class)
80 @SmallTest
81 @RunWith(AndroidJUnit4::class)
82 class ShortcutHelperViewModelTest : SysuiTestCase() {
83 
84     private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
85     private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
86     private val fakeCurrentAppsSource = FakeKeyboardShortcutGroupsSource()
87     private val mockPackageManager: PackageManager = mock()
88     private val mockUserContext: Context = mock()
89     private val mockApplicationInfo: ApplicationInfo = mock()
90 
91     private val kosmos =
<lambda>null92         Kosmos().also {
93             it.testCase = this
94             it.testDispatcher = UnconfinedTestDispatcher()
95             it.shortcutHelperSystemShortcutsSource = fakeSystemSource
96             it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
97             it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
98             it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
99             it.shortcutHelperCurrentAppShortcutsSource = fakeCurrentAppsSource
100             it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
101         }
102 
103     private val testScope = kosmos.testScope
104     private val testHelper = kosmos.shortcutHelperTestHelper
105     private val sysUiState = kosmos.sysUiState
106     private val fakeUserTracker = kosmos.fakeUserTracker
107     private val mockRoleManager = kosmos.mockRoleManager
108     private val inputManager = kosmos.fakeInputManager.inputManager
109     private val viewModel = kosmos.shortcutHelperViewModel
110 
111 
112     @Before
setUpnull113     fun setUp() {
114         fakeSystemSource.setGroups(TestShortcuts.systemGroups)
115         fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
116         fakeCurrentAppsSource.setGroups(TestShortcuts.currentAppGroups)
117         whenever(mockPackageManager.getApplicationInfo(anyString(), eq(0))).thenReturn(mockApplicationInfo)
118         whenever(mockPackageManager.getApplicationLabel(mockApplicationInfo)).thenReturn("Current App")
119         whenever(mockPackageManager.getApplicationIcon(anyString())).thenThrow(NameNotFoundException())
120         whenever(mockUserContext.packageManager).thenReturn(mockPackageManager)
121         whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
122     }
123 
124     @Test
shouldShow_falseByDefaultnull125     fun shouldShow_falseByDefault() =
126         testScope.runTest {
127             val shouldShow by collectLastValue(viewModel.shouldShow)
128 
129             assertThat(shouldShow).isFalse()
130         }
131 
132     @Test
shouldShow_trueAfterShowRequestednull133     fun shouldShow_trueAfterShowRequested() =
134         testScope.runTest {
135             val shouldShow by collectLastValue(viewModel.shouldShow)
136 
137             testHelper.showFromActivity()
138 
139             assertThat(shouldShow).isTrue()
140         }
141 
142     @Test
shouldShow_trueAfterToggleRequestednull143     fun shouldShow_trueAfterToggleRequested() =
144         testScope.runTest {
145             val shouldShow by collectLastValue(viewModel.shouldShow)
146 
147             testHelper.toggle(deviceId = 123)
148 
149             assertThat(shouldShow).isTrue()
150         }
151 
152     @Test
shouldShow_falseAfterToggleTwicenull153     fun shouldShow_falseAfterToggleTwice() =
154         testScope.runTest {
155             val shouldShow by collectLastValue(viewModel.shouldShow)
156 
157             testHelper.toggle(deviceId = 123)
158             testHelper.toggle(deviceId = 123)
159 
160             assertThat(shouldShow).isFalse()
161         }
162 
163     @Test
shouldShow_falseAfterViewClosednull164     fun shouldShow_falseAfterViewClosed() =
165         testScope.runTest {
166             val shouldShow by collectLastValue(viewModel.shouldShow)
167 
168             testHelper.toggle(deviceId = 567)
169             viewModel.onViewClosed()
170 
171             assertThat(shouldShow).isFalse()
172         }
173 
174     @Test
shouldShow_falseAfterCloseSystemDialogsnull175     fun shouldShow_falseAfterCloseSystemDialogs() =
176         testScope.runTest {
177             val shouldShow by collectLastValue(viewModel.shouldShow)
178 
179             testHelper.showFromActivity()
180             testHelper.hideThroughCloseSystemDialogs()
181 
182             assertThat(shouldShow).isFalse()
183         }
184 
185     @Test
shouldShow_doesNotEmitDuplicateValuesnull186     fun shouldShow_doesNotEmitDuplicateValues() =
187         testScope.runTest {
188             val shouldShowValues by collectValues(viewModel.shouldShow)
189 
190             testHelper.hideForSystem()
191             testHelper.toggle(deviceId = 987)
192             testHelper.showFromActivity()
193             viewModel.onViewClosed()
194             testHelper.hideFromActivity()
195             testHelper.hideForSystem()
196             testHelper.toggle(deviceId = 456)
197             testHelper.showFromActivity()
198 
199             assertThat(shouldShowValues).containsExactly(false, true, false, true).inOrder()
200         }
201 
202     @Test
shouldShow_emitsLatestValueToNewSubscribersnull203     fun shouldShow_emitsLatestValueToNewSubscribers() =
204         testScope.runTest {
205             val shouldShow by collectLastValue(viewModel.shouldShow)
206 
207             testHelper.showFromActivity()
208 
209             val shouldShowNew by collectLastValue(viewModel.shouldShow)
210             assertThat(shouldShowNew).isEqualTo(shouldShow)
211         }
212 
213     @Test
sysUiStateFlag_disabledByDefaultnull214     fun sysUiStateFlag_disabledByDefault() =
215         testScope.runTest {
216             assertThat(sysUiState.isFlagEnabled(SYSUI_STATE_SHORTCUT_HELPER_SHOWING)).isFalse()
217         }
218 
219     @Test
sysUiStateFlag_trueAfterViewOpenednull220     fun sysUiStateFlag_trueAfterViewOpened() =
221         testScope.runTest {
222             viewModel.onViewOpened()
223 
224             assertThat(sysUiState.isFlagEnabled(SYSUI_STATE_SHORTCUT_HELPER_SHOWING)).isTrue()
225         }
226 
227     @Test
sysUiStateFlag_falseAfterViewClosednull228     fun sysUiStateFlag_falseAfterViewClosed() =
229         testScope.runTest {
230             viewModel.onViewOpened()
231             viewModel.onViewClosed()
232 
233             assertThat(sysUiState.isFlagEnabled(SYSUI_STATE_SHORTCUT_HELPER_SHOWING)).isFalse()
234         }
235 
236     @Test
shortcutsUiState_inactiveByDefaultnull237     fun shortcutsUiState_inactiveByDefault() =
238         testScope.runTest {
239             val uiState by collectLastValue(viewModel.shortcutsUiState)
240 
241             assertThat(uiState).isEqualTo(ShortcutsUiState.Inactive)
242         }
243 
244     @Test
shortcutsUiState_featureActive_emitsActivenull245     fun shortcutsUiState_featureActive_emitsActive() =
246         testScope.runTest {
247             val uiState by collectLastValue(viewModel.shortcutsUiState)
248 
249             testHelper.showFromActivity()
250 
251             assertThat(uiState).isInstanceOf(ShortcutsUiState.Active::class.java)
252         }
253 
254     @Test
shortcutsUiState_noCurrentAppCategory_defaultSelectedCategoryIsSystemnull255     fun shortcutsUiState_noCurrentAppCategory_defaultSelectedCategoryIsSystem() =
256         testScope.runTest {
257             fakeCurrentAppsSource.setGroups(emptyList())
258 
259             val uiState by collectLastValue(viewModel.shortcutsUiState)
260 
261             testHelper.showFromActivity()
262 
263             val activeUiState = uiState as ShortcutsUiState.Active
264             assertThat(activeUiState.defaultSelectedCategory).isEqualTo(System)
265         }
266 
267     @Test
shortcutsUiState_currentAppCategoryPresent_currentAppIsDefaultSelectednull268     fun shortcutsUiState_currentAppCategoryPresent_currentAppIsDefaultSelected() =
269         testScope.runTest {
270             val uiState by collectLastValue(viewModel.shortcutsUiState)
271 
272             testHelper.showFromActivity()
273 
274             val activeUiState = uiState as ShortcutsUiState.Active
275             assertThat(activeUiState.defaultSelectedCategory)
276                 .isEqualTo(CurrentApp(TestShortcuts.currentAppPackageName))
277         }
278 
279     @Test
shortcutsUiState_currentAppIsLauncher_defaultSelectedCategoryIsSystemnull280     fun shortcutsUiState_currentAppIsLauncher_defaultSelectedCategoryIsSystem() =
281         testScope.runTest {
282             whenever(
283                 mockRoleManager.getRoleHoldersAsUser(
284                     RoleManager.ROLE_HOME,
285                     fakeUserTracker.userHandle,
286                 )
287             )
288                 .thenReturn(listOf(TestShortcuts.currentAppPackageName))
289             val uiState by collectLastValue(viewModel.shortcutsUiState)
290 
291             testHelper.showFromActivity()
292 
293             val activeUiState = uiState as ShortcutsUiState.Active
294             assertThat(activeUiState.defaultSelectedCategory).isEqualTo(System)
295         }
296 
297     @Test
shortcutsUiState_userTypedQuery_filtersMatchingShortcutLabelsnull298     fun shortcutsUiState_userTypedQuery_filtersMatchingShortcutLabels() =
299         testScope.runTest {
300             fakeSystemSource.setGroups(
301                 groupWithShortcutLabels("first Foo shortcut1", "first bar shortcut1"),
302                 groupWithShortcutLabels(
303                     "second foO shortcut2",
304                     "second bar shortcut2",
305                     groupLabel = SECOND_SIMPLE_GROUP_LABEL,
306                 ),
307             )
308             fakeMultiTaskingSource.setGroups(
309                 groupWithShortcutLabels("third FoO shortcut1", "third bar shortcut1")
310             )
311             val uiState by collectLastValue(viewModel.shortcutsUiState)
312 
313             testHelper.showFromActivity()
314             viewModel.onSearchQueryChanged("foo")
315 
316             val activeUiState = uiState as ShortcutsUiState.Active
317             assertThat(activeUiState.shortcutCategories)
318                 .containsExactly(
319                     ShortcutCategoryUi(
320                         label = "System",
321                         iconSource = IconSource(imageVector = Icons.Default.Tv),
322                         shortcutCategory =
323                         ShortcutCategory(
324                             System,
325                             subCategoryWithShortcutLabels("first Foo shortcut1"),
326                             subCategoryWithShortcutLabels(
327                                 "second foO shortcut2",
328                                 subCategoryLabel = SECOND_SIMPLE_GROUP_LABEL,
329                             ),
330                         ),
331                     ),
332                     ShortcutCategoryUi(
333                         label = "Multitasking",
334                         iconSource = IconSource(imageVector = Icons.Default.VerticalSplit),
335                         shortcutCategory =
336                         ShortcutCategory(
337                             MultiTasking,
338                             subCategoryWithShortcutLabels("third FoO shortcut1"),
339                         ),
340                     ),
341                 )
342         }
343 
344     @Test
shortcutsUiState_userTypedQuery_noMatch_returnsEmptyListnull345     fun shortcutsUiState_userTypedQuery_noMatch_returnsEmptyList() =
346         testScope.runTest {
347             fakeSystemSource.setGroups(
348                 groupWithShortcutLabels("first Foo shortcut1", "first bar shortcut1"),
349                 groupWithShortcutLabels("second foO shortcut2", "second bar shortcut2"),
350             )
351             fakeMultiTaskingSource.setGroups(
352                 groupWithShortcutLabels("third FoO shortcut1", "third bar shortcut1")
353             )
354             val uiState by collectLastValue(viewModel.shortcutsUiState)
355 
356             testHelper.showFromActivity()
357             viewModel.onSearchQueryChanged("unmatched query")
358 
359             val activeUiState = uiState as ShortcutsUiState.Active
360             assertThat(activeUiState.shortcutCategories).isEmpty()
361         }
362 
363     @Test
shortcutsUiState_userTypedQuery_noMatch_returnsNullDefaultSelectedCategorynull364     fun shortcutsUiState_userTypedQuery_noMatch_returnsNullDefaultSelectedCategory() =
365         testScope.runTest {
366             fakeSystemSource.setGroups(
367                 groupWithShortcutLabels("first Foo shortcut1", "first bar shortcut1"),
368                 groupWithShortcutLabels("second foO shortcut2", "second bar shortcut2"),
369             )
370             fakeMultiTaskingSource.setGroups(
371                 groupWithShortcutLabels("third FoO shortcut1", "third bar shortcut1")
372             )
373             val uiState by collectLastValue(viewModel.shortcutsUiState)
374 
375             testHelper.showFromActivity()
376             viewModel.onSearchQueryChanged("unmatched query")
377 
378             val activeUiState = uiState as ShortcutsUiState.Active
379             assertThat(activeUiState.defaultSelectedCategory).isNull()
380         }
381 
382     @Test
shortcutsUiState_userTypedQuery_changesDefaultSelectedCategoryToFirstMatchingCategorynull383     fun shortcutsUiState_userTypedQuery_changesDefaultSelectedCategoryToFirstMatchingCategory() =
384         testScope.runTest {
385             fakeSystemSource.setGroups(groupWithShortcutLabels("first shortcut"))
386             fakeMultiTaskingSource.setGroups(groupWithShortcutLabels("second shortcut"))
387             val uiState by collectLastValue(viewModel.shortcutsUiState)
388 
389             testHelper.showFromActivity()
390             viewModel.onSearchQueryChanged("second")
391 
392             val activeUiState = uiState as ShortcutsUiState.Active
393             assertThat(activeUiState.defaultSelectedCategory).isEqualTo(MultiTasking)
394         }
395 
396     @Test
shortcutsUiState_userTypedQuery_multipleCategoriesMatch_currentAppIsDefaultSelectednull397     fun shortcutsUiState_userTypedQuery_multipleCategoriesMatch_currentAppIsDefaultSelected() =
398         testScope.runTest {
399             fakeSystemSource.setGroups(groupWithShortcutLabels("first shortcut"))
400             fakeMultiTaskingSource.setGroups(groupWithShortcutLabels("second shortcut"))
401             fakeCurrentAppsSource.setGroups(groupWithShortcutLabels("third shortcut"))
402             val uiState by collectLastValue(viewModel.shortcutsUiState)
403 
404             testHelper.showFromActivity()
405             viewModel.onSearchQueryChanged("shortcut")
406 
407             val activeUiState = uiState as ShortcutsUiState.Active
408             assertThat(activeUiState.defaultSelectedCategory).isInstanceOf(CurrentApp::class.java)
409         }
410 
411     @Test
shortcutsUiState_shouldShowResetButton_isFalseWhenThereAreNoCustomShortcutsnull412     fun shortcutsUiState_shouldShowResetButton_isFalseWhenThereAreNoCustomShortcuts() =
413         testScope.runTest {
414             val uiState by collectLastValue(viewModel.shortcutsUiState)
415 
416             testHelper.showFromActivity()
417 
418             val activeUiState = uiState as ShortcutsUiState.Active
419             assertThat(activeUiState.shouldShowResetButton).isFalse()
420         }
421 
422     @Test
shortcutsUiState_shouldShowResetButton_isTrueWhenThereAreCustomShortcutsnull423     fun shortcutsUiState_shouldShowResetButton_isTrueWhenThereAreCustomShortcuts() =
424         testScope.runTest {
425             whenever(
426                 inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
427             ).thenReturn(listOf(allAppsInputGestureData))
428             val uiState by collectLastValue(viewModel.shortcutsUiState)
429 
430             testHelper.showFromActivity()
431 
432             val activeUiState = uiState as ShortcutsUiState.Active
433             assertThat(activeUiState.shouldShowResetButton).isTrue()
434         }
435 
groupWithShortcutLabelsnull436     private fun groupWithShortcutLabels(
437         vararg shortcutLabels: String,
438         groupLabel: String = FIRST_SIMPLE_GROUP_LABEL,
439     ) =
440         KeyboardShortcutGroup(groupLabel, shortcutLabels.map { simpleShortcutInfo(it) }).apply {
441             packageName = "test.package.name"
442         }
443 
simpleShortcutInfonull444     private fun simpleShortcutInfo(label: String) =
445         KeyboardShortcutInfo(label, KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON)
446 
447     private fun subCategoryWithShortcutLabels(
448         vararg shortcutLabels: String,
449         subCategoryLabel: String = FIRST_SIMPLE_GROUP_LABEL,
450     ) =
451         ShortcutSubCategory(
452             label = subCategoryLabel,
453             shortcuts = shortcutLabels.map { simpleShortcut(it) },
454         )
455 
simpleShortcutnull456     private fun simpleShortcut(label: String) =
457         shortcut(label) {
458             command {
459                 key("Ctrl")
460                 key("A")
461             }
462             contentDescription { "$label, Press key Ctrl plus A" }
463         }
464 
465     companion object {
466         private const val FIRST_SIMPLE_GROUP_LABEL = "simple group 1"
467         private const val SECOND_SIMPLE_GROUP_LABEL = "simple group 2"
468     }
469 }
470