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

Commit 8dfd0a8d authored by Joshua Mokut's avatar Joshua Mokut Committed by Android (Google) Code Review
Browse files

Merge "Added Repository for emitting user installed apps:" into main

parents 5f3220f6 be38c2b2
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) }
    }
}