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