diff options
4 files changed, 387 insertions, 0 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt new file mode 100644 index 000000000000..a2e42976f413 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 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.keyboard.shortcut.data.repository + +import android.content.pm.UserInfo +import android.os.UserHandle +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.keyboard.shortcut.fakeLauncherApps +import com.android.systemui.keyboard.shortcut.userVisibleAppsRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class UserVisibleAppsRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val fakeLauncherApps = kosmos.fakeLauncherApps + private val repo = kosmos.userVisibleAppsRepository + private val userTracker = kosmos.fakeUserTracker + private val testScope = kosmos.testScope + private val userVisibleAppsContainsApplication: + (pkgName: String, clsName: String) -> Flow<Boolean> = + { pkgName, clsName -> + repo.userVisibleApps.map { userVisibleApps -> + userVisibleApps.any { + it.componentName.packageName == pkgName && it.componentName.className == clsName + } + } + } + + @Before + fun setup() { + switchUser(index = PRIMARY_USER_INDEX) + } + + @Test + fun userVisibleApps_emitsUpdatedAppsList_onNewAppInstalled() { + testScope.runTest { + val containsPackageOne by + collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_1, TEST_CLASS_1)) + + installPackageOneForUserOne() + + assertThat(containsPackageOne).isTrue() + } + } + + @Test + fun userVisibleApps_emitsUpdatedAppsList_onAppUserChanged() { + testScope.runTest { + val containsPackageOne by + collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_1, TEST_CLASS_1)) + val containsPackageTwo by + collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_2, TEST_CLASS_2)) + + installPackageOneForUserOne() + installPackageTwoForUserTwo() + + assertThat(containsPackageOne).isTrue() + assertThat(containsPackageTwo).isFalse() + + switchUser(index = SECONDARY_USER_INDEX) + + assertThat(containsPackageOne).isFalse() + assertThat(containsPackageTwo).isTrue() + } + } + + @Test + fun userVisibleApps_emitsUpdatedAppsList_onAppUninstalled() { + testScope.runTest { + val containsPackageOne by + collectLastValue(userVisibleAppsContainsApplication(TEST_PACKAGE_1, TEST_CLASS_1)) + + installPackageOneForUserOne() + uninstallPackageOneForUserOne() + + assertThat(containsPackageOne).isFalse() + } + } + + private fun switchUser(index: Int) { + userTracker.set( + userInfos = + listOf( + UserInfo(/* id= */ PRIMARY_USER_ID, /* name= */ "Primary User", /* flags= */ 0), + UserInfo( + /* id= */ SECONDARY_USER_ID, + /* name= */ "Secondary User", + /* flags= */ 0, + ), + ), + selectedUserIndex = index, + ) + } + + private fun installPackageOneForUserOne() { + fakeLauncherApps.installPackageForUser( + TEST_PACKAGE_1, + TEST_CLASS_1, + UserHandle(/* userId= */ PRIMARY_USER_ID), + ) + } + + private fun uninstallPackageOneForUserOne() { + fakeLauncherApps.uninstallPackageForUser( + TEST_PACKAGE_1, + TEST_CLASS_1, + UserHandle(/* userId= */ PRIMARY_USER_ID), + ) + } + + private fun installPackageTwoForUserTwo() { + fakeLauncherApps.installPackageForUser( + TEST_PACKAGE_2, + TEST_CLASS_2, + UserHandle(/* userId= */ SECONDARY_USER_ID), + ) + } + + companion object { + const val TEST_PACKAGE_1 = "test.package.one" + const val TEST_PACKAGE_2 = "test.package.two" + const val TEST_CLASS_1 = "TestClassOne" + const val TEST_CLASS_2 = "TestClassTwo" + const val PRIMARY_USER_ID = 10 + const val PRIMARY_USER_INDEX = 0 + const val SECONDARY_USER_ID = 11 + const val SECONDARY_USER_INDEX = 1 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt new file mode 100644 index 000000000000..5a4ee16e0e64 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025 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.keyboard.shortcut.data.repository + +import android.content.Context +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.os.Handler +import android.os.UserHandle +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserTracker +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +@SysUISingleton +class UserVisibleAppsRepository +@Inject +constructor( + private val userTracker: UserTracker, + @Background private val bgExecutor: Executor, + @Background private val bgHandler: Handler, + private val launcherApps: LauncherApps, +) { + + val userVisibleApps: Flow<List<LauncherActivityInfo>> + get() = conflatedCallbackFlow { + val packageChangeCallback: LauncherApps.Callback = + object : LauncherApps.Callback() { + override fun onPackageAdded(packageName: String, userHandle: UserHandle) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGE_ADDED, + ) + } + + override fun onPackageChanged(packageName: String, userHandle: UserHandle) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGE_CHANGED, + ) + } + + override fun onPackageRemoved(packageName: String, userHandle: UserHandle) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGE_REMOVED, + ) + } + + override fun onPackagesAvailable( + packages: Array<out String>, + userHandle: UserHandle, + replacing: Boolean, + ) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGES_AVAILABLE, + ) + } + + override fun onPackagesUnavailable( + packages: Array<out String>, + userHandle: UserHandle, + replacing: Boolean, + ) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_PACKAGES_UNAVAILABLE, + ) + } + } + + val userChangeCallback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = ON_USER_CHANGED, + ) + } + } + + userTracker.addCallback(userChangeCallback, bgExecutor) + launcherApps.registerCallback(packageChangeCallback, bgHandler) + + trySendWithFailureLogging( + element = retrieveLauncherApps(), + loggingTag = TAG, + elementDescription = INITIAL_VALUE, + ) + + awaitClose { + userTracker.removeCallback(userChangeCallback) + launcherApps.unregisterCallback(packageChangeCallback) + } + } + + private fun retrieveLauncherApps(): List<LauncherActivityInfo> { + return launcherApps.getActivityList(/* packageName= */ null, userTracker.userHandle) + } + + private companion object { + const val TAG = "UserVisibleAppsRepository" + const val ON_PACKAGE_ADDED = "onPackageAdded" + const val ON_PACKAGE_CHANGED = "onPackageChanged" + const val ON_PACKAGE_REMOVED = "onPackageRemoved" + const val ON_PACKAGES_AVAILABLE = "onPackagesAvailable" + const val ON_PACKAGES_UNAVAILABLE = "onPackagesUnavailable" + const val ON_USER_CHANGED = "onUserChanged" + const val INITIAL_VALUE = "InitialValue" + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index 318e5c716ca7..8465345a0bdd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -20,8 +20,10 @@ import android.app.role.mockRoleManager import android.content.applicationContext import android.content.res.mainResources import android.hardware.input.fakeInputManager +import android.os.fakeExecutorHandler import android.view.windowManager import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.keyboard.shortcut.data.repository.AppLaunchDataRepository import com.android.systemui.keyboard.shortcut.data.repository.CustomInputGesturesRepository import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository @@ -33,6 +35,7 @@ import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCust import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperInputDeviceRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper +import com.android.systemui.keyboard.shortcut.data.repository.UserVisibleAppsRepository import com.android.systemui.keyboard.shortcut.data.source.AccessibilityShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.CurrentAppShortcutsSource @@ -44,6 +47,7 @@ import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomiz import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCustomizationModeInteractor import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor +import com.android.systemui.keyboard.shortcut.fakes.FakeLauncherApps import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperExclusions import com.android.systemui.keyboard.shortcut.ui.ShortcutCustomizationDialogStarter import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutCustomizationViewModel @@ -247,3 +251,15 @@ val Kosmos.shortcutCustomizationViewModelFactory by } } } + +val Kosmos.fakeLauncherApps by Kosmos.Fixture { FakeLauncherApps() } + +val Kosmos.userVisibleAppsRepository by + Kosmos.Fixture { + UserVisibleAppsRepository( + userTracker, + fakeExecutor, + fakeExecutorHandler, + fakeLauncherApps.launcherApps, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt new file mode 100644 index 000000000000..f0c4a357b974 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 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.keyboard.shortcut.fakes + +import android.content.ComponentName +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.os.UserHandle +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock + +class FakeLauncherApps { + + private val activityListPerUser: MutableMap<Int, MutableList<LauncherActivityInfo>> = + mutableMapOf() + private val callbacks: MutableList<LauncherApps.Callback> = mutableListOf() + + val launcherApps: LauncherApps = mock { + on { getActivityList(anyOrNull(), any()) } + .then { + val userHandle = it.getArgument<UserHandle>(1) + + activityListPerUser.getOrDefault(userHandle.identifier, emptyList()) + } + on { registerCallback(any(), any()) } + .then { + val callback = it.getArgument<LauncherApps.Callback>(0) + + callbacks.add(callback) + } + on { unregisterCallback(any()) } + .then { + val callback = it.getArgument<LauncherApps.Callback>(0) + + callbacks.remove(callback) + } + } + + fun installPackageForUser(packageName: String, className: String, userHandle: UserHandle) { + val launcherActivityInfo: LauncherActivityInfo = mock { + on { componentName } + .thenReturn(ComponentName(/* pkg= */ packageName, /* cls= */ className)) + } + + if (!activityListPerUser.containsKey(userHandle.identifier)) { + activityListPerUser[userHandle.identifier] = mutableListOf() + } + + activityListPerUser[userHandle.identifier]!!.add(launcherActivityInfo) + + callbacks.forEach { it.onPackageAdded(/* pkg= */ packageName, userHandle) } + } + + fun uninstallPackageForUser(packageName: String, className: String, userHandle: UserHandle) { + activityListPerUser[userHandle.identifier]?.removeIf { + it.componentName.packageName == packageName && it.componentName.className == className + } + + callbacks.forEach { it.onPackageRemoved(/* pkg= */ packageName, userHandle) } + } +} |