diff options
| author | 2025-03-18 09:58:28 +0000 | |
|---|---|---|
| committer | 2025-03-19 23:12:31 +0000 | |
| commit | be38c2b22d5c807c833fe312076854321c5e56b3 (patch) | |
| tree | f214fe6dbdb5b8edb790d35ade6f53b62ed68b0c | |
| parent | 10f75d806dcbb4570198b6ee8a45d4fb7ea8e9a0 (diff) | |
Added Repository for emitting user installed apps:
1. This Repo is User Aware
2. This repo reacts to changes in installed applications(removing/adding
an app)
3. This repo only emits apps that are visible to the user, i.e. System
Apps, or apps not visible in Launcher's All Apps will not be shown.
Fix: 403243843
Flag: com.android.systemui.extended_apps_shortcut_category
Test: UserVisibleAppsRepositoryTest
Change-Id: I794d4b1d58b614dc95c87ee616f6ed3635d18a7e
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) } + } +} |