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

Commit 3aed8e72 authored by Anton Vayvod's avatar Anton Vayvod
Browse files

Refactor AppListRepositoryImpl to add caching.

This change introduces `AppListRepositoryImplHelper` to contain the
original logic. `AppListRepositoryImpl` now wraps this helper and adds a
caching layer using `ConcurrentHashMap` and `Deferred` for `loadApps`
and `loadHomeOrLauncherPackages`, controlled by a `useCaching` flag.

Bug: 439793151
Flag: EXEMPT bug fix
Test: manual + presubmit
Change-Id: Ib92b9d60f596c1032549746c37fb29429eb03af8
parent 6d3bf369
Loading
Loading
Loading
Loading
+192 −84
Original line number Diff line number Diff line
@@ -28,6 +28,8 @@ import android.content.pm.ResolveInfo
import android.util.Log
import com.android.internal.R
import com.android.settingslib.spaprivileged.framework.common.userManager
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@@ -35,9 +37,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.runBlocking

/**
 * The repository to load the App List data.
 */
/** The repository to load the App List data. */
interface AppListRepository {
    /** Loads the list of [ApplicationInfo]. */
    suspend fun loadApps(
@@ -65,9 +65,7 @@ interface AppListRepository {
    ): List<ApplicationInfo> = listOf()
}

/**
 * Util for app list repository.
 */
/** Util for app list repository. */
object AppListRepositoryUtil {
    /** Gets the system app package names. */
    @JvmStatic
@@ -75,23 +73,21 @@ object AppListRepositoryUtil {
        AppListRepositoryImpl(context).getSystemPackageNamesBlocking(userId)
}

/**
 * This constructor is visible for tests only in order to override `featureFlags`.
 */
class AppListRepositoryImpl(
internal class AppListRepositoryImplHelper(
    private val context: Context,
    private val featureFlags: FeatureFlags
) : AppListRepository {
    private val featureFlags: FeatureFlags,
) {
    private val packageManager = context.packageManager
    private val userManager = context.userManager

    constructor(context: Context) : this(context, FeatureFlagsImpl())

    override suspend fun loadApps(
    suspend fun loadApps(
        userId: Int,
        loadInstantApps: Boolean,
        matchAnyUserForAdmin: Boolean,
    ): List<ApplicationInfo> = try {
    ): List<ApplicationInfo> =
        try {
            coroutineScope {
                // TODO(b/382016780): to be removed after flag cleanup.
                val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() }
@@ -117,36 +113,36 @@ class AppListRepositoryImpl(
        userId: Int,
        matchAnyUserForAdmin: Boolean,
    ): List<ApplicationInfo> {
        val disabledComponentsFlag = (PackageManager.MATCH_DISABLED_COMPONENTS or
            PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
        val archivedPackagesFlag: Long = if (isArchivingEnabled(featureFlags))
            PackageManager.MATCH_ARCHIVED_PACKAGES else 0L
        val regularFlags = ApplicationInfoFlags.of(
            disabledComponentsFlag or
                archivedPackagesFlag
        )
        val disabledComponentsFlag =
            (PackageManager.MATCH_DISABLED_COMPONENTS or
                    PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS)
                .toLong()
        val archivedPackagesFlag: Long =
            if (isArchivingEnabled(featureFlags)) PackageManager.MATCH_ARCHIVED_PACKAGES else 0L
        val regularFlags = ApplicationInfoFlags.of(disabledComponentsFlag or archivedPackagesFlag)
        return if (!matchAnyUserForAdmin || !userManager.getUserInfo(userId).isAdmin) {
            packageManager.getInstalledApplicationsAsUser(regularFlags, userId)
        } else {
            coroutineScope {
                val deferredPackageNamesInChildProfiles =
                    userManager.getProfileIdsWithDisabled(userId)
                    userManager
                        .getProfileIdsWithDisabled(userId)
                        .filter { it != userId }
                        .map {
                            async {
                                packageManager.getInstalledApplicationsAsUser(regularFlags, it)
                                packageManager
                                    .getInstalledApplicationsAsUser(regularFlags, it)
                                    .map { it.packageName }
                            }
                        }
                val adminFlags = ApplicationInfoFlags.of(
                val adminFlags =
                    ApplicationInfoFlags.of(
                        PackageManager.MATCH_ANY_USER.toLong() or regularFlags.value
                    )
                val allInstalledApplications =
                    packageManager.getInstalledApplicationsAsUser(adminFlags, userId)
                val packageNamesInChildProfiles = deferredPackageNamesInChildProfiles
                    .awaitAll()
                    .flatten()
                    .toSet()
                val packageNamesInChildProfiles =
                    deferredPackageNamesInChildProfiles.awaitAll().flatten().toSet()
                // If an app is for a child profile and not installed on the owner, not display as
                // 'not installed for this user' in the owner. This will prevent duplicates of work
                // only apps showing up in the personal profile.
@@ -157,30 +153,23 @@ class AppListRepositoryImpl(
        }
    }

    private fun isArchivingEnabled(featureFlags: FeatureFlags) =
            featureFlags.archiving()

    override fun showSystemPredicate(
        userIdFlow: Flow<Int>,
        showSystemFlow: Flow<Boolean>,
    ): Flow<(app: ApplicationInfo) -> Boolean> =
        userIdFlow.combine(showSystemFlow, ::showSystemPredicate)
    private fun isArchivingEnabled(featureFlags: FeatureFlags) = featureFlags.archiving()

    override fun getSystemPackageNamesBlocking(userId: Int) = runBlocking {
    fun getSystemPackageNamesBlocking(userId: Int) = runBlocking {
        loadAndFilterApps(userId = userId, isSystemApp = true).map { it.packageName }.toSet()
    }

    override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) = coroutineScope {
        val loadAppsDeferred = async { loadApps(userId) }
    suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) = coroutineScope {
        val loadAppsDeferred = async { loadApps(userId, false, false) }
        val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
        loadAppsDeferred.await().filter { app ->
            isSystemApp(app, homeOrLauncherPackages) == isSystemApp
        }
    }

    override suspend fun loadAndMaybeExcludeSystemApps(userId: Int, excludeSystemApp: Boolean) =
    suspend fun loadAndMaybeExcludeSystemApps(userId: Int, excludeSystemApp: Boolean) =
        coroutineScope {
            val loadAppsDeferred = async { loadApps(userId) }
            val loadAppsDeferred = async { loadApps(userId, false, false) }
            if (excludeSystemApp) {
                val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
                loadAppsDeferred.await().filter { app -> !isSystemApp(app, homeOrLauncherPackages) }
@@ -189,25 +178,18 @@ class AppListRepositoryImpl(
            }
        }

    private suspend fun showSystemPredicate(
        userId: Int,
        showSystem: Boolean,
    ): (app: ApplicationInfo) -> Boolean {
        if (showSystem) return { true }
        val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
        return { app -> !isSystemApp(app, homeOrLauncherPackages) }
    }

    private suspend fun loadHomeOrLauncherPackages(userId: Int): Set<String> {
    internal suspend fun loadHomeOrLauncherPackages(userId: Int): Set<String> {
        val launchIntent = Intent(Intent.ACTION_MAIN, null).addCategory(Intent.CATEGORY_LAUNCHER)
        // If we do not specify MATCH_DIRECT_BOOT_AWARE or MATCH_DIRECT_BOOT_UNAWARE, system will
        // derive and update the flags according to the user's lock state. When the user is locked,
        // components with ComponentInfo#directBootAware == false will be filtered. We should
        // explicitly include both direct boot aware and unaware component here.
        val flags = PackageManager.ResolveInfoFlags.of(
        val flags =
            PackageManager.ResolveInfoFlags.of(
                (PackageManager.MATCH_DISABLED_COMPONENTS or
                        PackageManager.MATCH_DIRECT_BOOT_AWARE or
                PackageManager.MATCH_DIRECT_BOOT_UNAWARE).toLong()
                        PackageManager.MATCH_DIRECT_BOOT_UNAWARE)
                    .toLong()
            )
        return coroutineScope {
            val launcherActivities = async {
@@ -235,14 +217,15 @@ class AppListRepositoryImpl(
    }

    companion object {
        private const val TAG = "AppListRepository"
        private const val TAG = "AppListRepositoryImplHelper"

        // TODO(b/382016780): to be removed after flag cleanup.
        private fun ApplicationInfo.isInAppList(
            showInstantApps: Boolean,
            hiddenSystemModules: Set<String>,
            hideWhenDisabledPackages: Array<String>,
        ) = when {
        ) =
            when {
                !showInstantApps && isInstantApp -> false
                !Flags.removeHiddenModuleUsage() && (packageName in hiddenSystemModules) -> false
                packageName in hideWhenDisabledPackages -> enabled && !isDisabledUntilUsed
@@ -251,3 +234,128 @@ class AppListRepositoryImpl(
            }
    }
}

/** This constructor is visible for tests only in order to override `featureFlags`. */
class AppListRepositoryImpl(context: Context, private val featureFlags: FeatureFlags) :
    AppListRepository {
    private val helper = AppListRepositoryImplHelper(context, featureFlags)

    constructor(context: Context) : this(context, FeatureFlagsImpl())

    companion object {
        @JvmStatic
        @Volatile
        var useCaching = false
            set(value) {
                if (!value) {
                    appsCache.clear()
                    homeOrLauncherPackagesCache.clear()
                }
                field = value
            }

        private val appsCache = ConcurrentHashMap<AppsCacheKey, Deferred<List<ApplicationInfo>>>()

        private data class AppsCacheKey(
            val userId: Int,
            val loadInstantApps: Boolean,
            val matchAnyUserForAdmin: Boolean,
        )

        private val homeOrLauncherPackagesCache = ConcurrentHashMap<Int, Deferred<Set<String>>>()
    }

    private suspend fun <K, V> getOrAsync(
        cache: ConcurrentHashMap<K, Deferred<V>>,
        key: K,
        block: suspend () -> V,
    ): Deferred<V> = coroutineScope {
        cache.computeIfAbsent(key) { this@coroutineScope.async { block() } }
    }

    override suspend fun loadApps(
        userId: Int,
        loadInstantApps: Boolean,
        matchAnyUserForAdmin: Boolean,
    ): List<ApplicationInfo> {
        if (!useCaching) {
            return helper.loadApps(userId, loadInstantApps, matchAnyUserForAdmin)
        }
        val key = AppsCacheKey(userId, loadInstantApps, matchAnyUserForAdmin)
        return getOrAsync(appsCache, key) {
                helper.loadApps(userId, loadInstantApps, matchAnyUserForAdmin)
            }
            .await()
    }

    override fun showSystemPredicate(
        userIdFlow: Flow<Int>,
        showSystemFlow: Flow<Boolean>,
    ): Flow<(app: ApplicationInfo) -> Boolean> =
        userIdFlow.combine(showSystemFlow, ::showSystemPredicate)

    override fun getSystemPackageNamesBlocking(userId: Int): Set<String> {
        if (!useCaching) {
            return helper.getSystemPackageNamesBlocking(userId)
        }
        return runBlocking {
            loadAndFilterApps(userId = userId, isSystemApp = true).map { it.packageName }.toSet()
        }
    }

    override suspend fun loadAndFilterApps(
        userId: Int,
        isSystemApp: Boolean,
    ): List<ApplicationInfo> {
        if (!useCaching) {
            return helper.loadAndFilterApps(userId, isSystemApp)
        }
        return coroutineScope {
            val loadAppsDeferred = async { loadApps(userId) }
            val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
            loadAppsDeferred.await().filter { app ->
                isSystemApp(app, homeOrLauncherPackages) == isSystemApp
            }
        }
    }

    override suspend fun loadAndMaybeExcludeSystemApps(
        userId: Int,
        excludeSystemApp: Boolean,
    ): List<ApplicationInfo> {
        if (!useCaching) {
            return helper.loadAndMaybeExcludeSystemApps(userId, excludeSystemApp)
        }
        return coroutineScope {
            val loadAppsDeferred = async { loadApps(userId) }
            if (excludeSystemApp) {
                val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
                loadAppsDeferred.await().filter { app -> !isSystemApp(app, homeOrLauncherPackages) }
            } else {
                loadAppsDeferred.await()
            }
        }
    }

    private suspend fun showSystemPredicate(
        userId: Int,
        showSystem: Boolean,
    ): (app: ApplicationInfo) -> Boolean {
        if (showSystem) return { true }
        val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
        return { app -> !isSystemApp(app, homeOrLauncherPackages) }
    }

    private suspend fun loadHomeOrLauncherPackages(userId: Int): Set<String> {
        if (!useCaching) {
            return helper.loadHomeOrLauncherPackages(userId)
        }
        return getOrAsync(homeOrLauncherPackagesCache, userId) {
                helper.loadHomeOrLauncherPackages(userId)
            }
            .await()
    }

    private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean =
        app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages
}
+253 −176

File changed.

Preview size limit exceeded, changes collapsed.