summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepositoryTest.kt158
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/UserVisibleAppsRepository.kt137
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt16
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/fakes/FakeLauncherApps.kt76
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) }
+ }
+}