Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit be38c2b2 authored by Josh's avatar Josh
Browse files

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
parent 10f75d80
Loading
Loading
Loading
Loading
+158 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+137 −0
Original line number Diff line number Diff line
/*
 * 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"
    }
}
+16 −0
Original line number Diff line number Diff line
@@ -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,
        )
    }
+76 −0
Original line number Diff line number Diff line
/*
 * 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) }
    }
}