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

Commit d6b7f549 authored by Chaohui Wang's avatar Chaohui Wang
Browse files

Display cloned app under person tab in App List

This is for the new SPA App List.

Before this change, cloned app is displayed in separated tab, after this
change, cloned app will displayed under person tab in App List.

Fix: 266040895
Test: Manually with Settings
Test: Unit test
Change-Id: I0dd448c3a33815b9b63348381d97b2520e05ab27
parent f104ba94
Loading
Loading
Loading
Loading
+0 −2
Original line number Diff line number Diff line
@@ -33,7 +33,6 @@ import com.android.settingslib.spa.framework.compose.LifecycleEffect
fun DisposableBroadcastReceiverAsUser(
    intentFilter: IntentFilter,
    userHandle: UserHandle,
    onStart: () -> Unit = {},
    onReceive: (Intent) -> Unit,
) {
    val context = LocalContext.current
@@ -49,7 +48,6 @@ fun DisposableBroadcastReceiverAsUser(
            context.registerReceiverAsUser(
                broadcastReceiver, userHandle, intentFilter, null, null
            )
            onStart()
        },
        onStop = {
            context.unregisterReceiver(broadcastReceiver)
+24 −27
Original line number Diff line number Diff line
@@ -28,20 +28,12 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.runBlocking

/**
 * The config used to load the App List.
 */
data class AppListConfig(
    val userId: Int,
    val showInstantApps: Boolean,
)

/**
 * The repository to load the App List data.
 */
internal interface AppListRepository {
    /** Loads the list of [ApplicationInfo]. */
    suspend fun loadApps(config: AppListConfig): List<ApplicationInfo>
    suspend fun loadApps(userId: Int, showInstantApps: Boolean): List<ApplicationInfo>

    /** Gets the flow of predicate that could used to filter system app. */
    fun showSystemPredicate(
@@ -50,7 +42,7 @@ internal interface AppListRepository {
    ): Flow<(app: ApplicationInfo) -> Boolean>

    /** Gets the system app package names. */
    fun getSystemPackageNamesBlocking(config: AppListConfig): Set<String>
    fun getSystemPackageNamesBlocking(userId: Int, showInstantApps: Boolean): Set<String>
}

/**
@@ -59,15 +51,21 @@ internal interface AppListRepository {
object AppListRepositoryUtil {
    /** Gets the system app package names. */
    @JvmStatic
    fun getSystemPackageNames(context: Context, config: AppListConfig): Set<String> {
        return AppListRepositoryImpl(context).getSystemPackageNamesBlocking(config)
    }
    fun getSystemPackageNames(
        context: Context,
        userId: Int,
        showInstantApps: Boolean,
    ): Set<String> =
        AppListRepositoryImpl(context).getSystemPackageNamesBlocking(userId, showInstantApps)
}

internal class AppListRepositoryImpl(private val context: Context) : AppListRepository {
    private val packageManager = context.packageManager

    override suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> = coroutineScope {
    override suspend fun loadApps(
        userId: Int,
        showInstantApps: Boolean,
    ): List<ApplicationInfo> = coroutineScope {
        val hiddenSystemModulesDeferred = async {
            packageManager.getInstalledModules(0)
                .filter { it.isHidden }
@@ -82,12 +80,12 @@ internal class AppListRepositoryImpl(private val context: Context) : AppListRepo
                PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
        )
        val installedApplicationsAsUser =
            packageManager.getInstalledApplicationsAsUser(flags, config.userId)
            packageManager.getInstalledApplicationsAsUser(flags, userId)

        val hiddenSystemModules = hiddenSystemModulesDeferred.await()
        val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await()
        installedApplicationsAsUser.filter { app ->
            app.isInAppList(config.showInstantApps, hiddenSystemModules, hideWhenDisabledPackages)
            app.isInAppList(showInstantApps, hiddenSystemModules, hideWhenDisabledPackages)
        }
    }

@@ -97,14 +95,13 @@ internal class AppListRepositoryImpl(private val context: Context) : AppListRepo
    ): Flow<(app: ApplicationInfo) -> Boolean> =
        userIdFlow.combine(showSystemFlow, ::showSystemPredicate)

    override fun getSystemPackageNamesBlocking(config: AppListConfig) = runBlocking {
        getSystemPackageNames(config)
    }
    override fun getSystemPackageNamesBlocking(userId: Int, showInstantApps: Boolean) =
        runBlocking { getSystemPackageNames(userId, showInstantApps) }

    private suspend fun getSystemPackageNames(config: AppListConfig): Set<String> =
    private suspend fun getSystemPackageNames(userId: Int, showInstantApps: Boolean): Set<String> =
        coroutineScope {
                val loadAppsDeferred = async { loadApps(config) }
                val homeOrLauncherPackages = loadHomeOrLauncherPackages(config.userId)
            val loadAppsDeferred = async { loadApps(userId, showInstantApps) }
            val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
            val showSystemPredicate =
                { app: ApplicationInfo -> isSystemApp(app, homeOrLauncherPackages) }
            loadAppsDeferred.await().filter(showSystemPredicate).map { it.packageName }.toSet()
+68 −36
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import com.android.settingslib.spa.framework.util.StateFlowBridge
import com.android.settingslib.spa.framework.util.asyncMapItem
import com.android.settingslib.spa.framework.util.waitFirst
import com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.template.app.AppListConfig
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -34,8 +35,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
@@ -78,11 +79,19 @@ internal open class AppListViewModelImpl<T : AppRecord>(
    private val labelMap = ConcurrentHashMap<String, String>()
    private val scope = viewModelScope + Dispatchers.IO

    private val userIdFlow = appListConfig.flow.map { it.userId }
    private val userSubGraphsFlow = appListConfig.flow.map { config ->
        config.userIds.map { userId -> UserSubGraph(userId, config.showInstantApps) }
    }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)

    private inner class UserSubGraph(
        private val userId: Int,
        private val showInstantApps: Boolean,
    ) {
        private val userIdFlow = flowOf(userId)

        private val appsStateFlow = MutableStateFlow<List<ApplicationInfo>?>(null)

    private val recordListFlow = listModel.flow
        val recordListFlow = listModel.flow
            .flatMapLatest { it.transform(userIdFlow, appsStateFlow.filterNotNull()) }
            .shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)

@@ -91,33 +100,33 @@ internal open class AppListViewModelImpl<T : AppRecord>(
                .combine(recordListFlow) { showAppPredicate, recordList ->
                    recordList.filter { showAppPredicate(it.app) }
                }

    override val spinnerOptionsFlow =
        recordListFlow.combine(listModel.flow) { recordList, listModel ->
            listModel.getSpinnerOptions(recordList)
        }

    override val appListDataFlow = optionFlow.filterNotNull().flatMapLatest(::filterAndSort)
        .combine(searchQuery.flow) { appListData, searchQuery ->
            appListData.filter {
                it.label.contains(other = searchQuery, ignoreCase = true)
            }
        }
                .shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)

    init {
        scheduleOnFirstLoaded()
        val listModelFilteredFlow = optionFlow.filterNotNull().flatMapLatest { option ->
            listModel.flow.flatMapLatest { listModel ->
                listModel.filter(this.userIdFlow, option, this.systemFilteredFlow)
            }
        }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)

        fun reloadApps() {
            scope.launch {
            appsStateFlow.value = appListRepository.loadApps(appListConfig.flow.first())
                appsStateFlow.value = appListRepository.loadApps(userId, showInstantApps)
            }
        }
    }

    private fun filterAndSort(option: Int) = listModel.flow.flatMapLatest { listModel ->
        listModel.filter(userIdFlow, option, systemFilteredFlow)
            .asyncMapItem { record ->
    private val combinedRecordListFlow = userSubGraphsFlow.flatMapLatest { userSubGraphList ->
        combine(userSubGraphList.map { it.recordListFlow }) { it.toList().flatten() }
    }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)

    override val spinnerOptionsFlow =
        combinedRecordListFlow.combine(listModel.flow) { recordList, listModel ->
            listModel.getSpinnerOptions(recordList)
        }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)

    private val appEntryListFlow = userSubGraphsFlow.flatMapLatest { userSubGraphList ->
        combine(userSubGraphList.map { it.listModelFilteredFlow }) { it.toList().flatten() }
    }.asyncMapItem { record ->
        val label = getLabel(record.app)
        AppEntry(
            record = record,
@@ -125,16 +134,39 @@ internal open class AppListViewModelImpl<T : AppRecord>(
            labelCollationKey = collator.getCollationKey(label),
        )
    }
            .map { appEntries ->

    override val appListDataFlow =
        combine(
            appEntryListFlow,
            listModel.flow,
            optionFlow.filterNotNull(),
        ) { appEntries, listModel, option ->
            AppListData(
                appEntries = appEntries.sortedWith(listModel.getComparator(option)),
                option = option,
            )
        }.combine(searchQuery.flow) { appListData, searchQuery ->
            appListData.filter {
                it.label.contains(other = searchQuery, ignoreCase = true)
            }
        }.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)

    init {
        scheduleOnFirstLoaded()
    }

    fun reloadApps() {
        scope.launch {
            userSubGraphsFlow.collect { userSubGraphList ->
                for (userSubGraph in userSubGraphList) {
                    userSubGraph.reloadApps()
                }
            }
        }
    }

    private fun scheduleOnFirstLoaded() {
        recordListFlow
        combinedRecordListFlow
            .waitFirst(appListDataFlow)
            .combine(listModel.flow) { recordList, listModel ->
                if (listModel.onFirstLoaded(recordList)) {
+24 −14
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.settingslib.spa.framework.compose.LifecycleEffect
import com.android.settingslib.spa.framework.compose.LogCompositions
import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer
import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll
@@ -43,7 +44,6 @@ import com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.R
import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
import com.android.settingslib.spaprivileged.model.app.AppEntry
import com.android.settingslib.spaprivileged.model.app.AppListConfig
import com.android.settingslib.spaprivileged.model.app.AppListData
import com.android.settingslib.spaprivileged.model.app.AppListModel
import com.android.settingslib.spaprivileged.model.app.AppListViewModel
@@ -56,6 +56,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
private const val TAG = "AppList"
private const val CONTENT_TYPE_HEADER = "header"

/**
 * The config used to load the App List.
 */
data class AppListConfig(
    val userIds: List<Int>,
    val showInstantApps: Boolean,
)

data class AppListState(
    val showSystem: State<Boolean>,
    val searchQuery: State<String>,
@@ -84,7 +92,7 @@ fun <T : AppRecord> AppListInput<T>.AppList() {
internal fun <T : AppRecord> AppListInput<T>.AppListImpl(
    viewModelSupplier: @Composable () -> IAppListViewModel<T>,
) {
    LogCompositions(TAG, config.userId.toString())
    LogCompositions(TAG, config.userIds.toString())
    val viewModel = viewModelSupplier()
    Column(Modifier.fillMaxSize()) {
        val optionsState = viewModel.spinnerOptionsFlow.collectAsState(null, Dispatchers.IO)
@@ -168,21 +176,23 @@ private fun <T : AppRecord> rememberViewModel(
    listModel: AppListModel<T>,
    state: AppListState,
): AppListViewModel<T> {
    val viewModel: AppListViewModel<T> = viewModel(key = config.userId.toString())
    val viewModel: AppListViewModel<T> = viewModel(key = config.userIds.toString())
    viewModel.appListConfig.setIfAbsent(config)
    viewModel.listModel.setIfAbsent(listModel)
    viewModel.showSystem.Sync(state.showSystem)
    viewModel.searchQuery.Sync(state.searchQuery)

    DisposableBroadcastReceiverAsUser(
        intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
    LifecycleEffect(onStart = { viewModel.reloadApps() })
    val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
        addAction(Intent.ACTION_PACKAGE_REMOVED)
        addAction(Intent.ACTION_PACKAGE_CHANGED)
        addDataScheme("package")
        },
        userHandle = UserHandle.of(config.userId),
        onStart = { viewModel.reloadApps() },
    }
    for (userId in config.userIds) {
        DisposableBroadcastReceiverAsUser(
            intentFilter = intentFilter,
            userHandle = UserHandle.of(userId),
        ) { viewModel.reloadApps() }

    }
    return viewModel
}
+3 −4
Original line number Diff line number Diff line
@@ -24,10 +24,9 @@ import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction
import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope
import com.android.settingslib.spa.widget.scaffold.SearchScaffold
import com.android.settingslib.spaprivileged.R
import com.android.settingslib.spaprivileged.model.app.AppListConfig
import com.android.settingslib.spaprivileged.model.app.AppListModel
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.template.common.WorkProfilePager
import com.android.settingslib.spaprivileged.template.common.UserProfilePager

/**
 * The full screen template for an App List page.
@@ -55,10 +54,10 @@ fun <T : AppRecord> AppListPage(
            }
        },
    ) { bottomPadding, searchQuery ->
        WorkProfilePager(primaryUserOnly) { userInfo ->
        UserProfilePager(primaryUserOnly) { userGroup ->
            val appListInput = AppListInput(
                config = AppListConfig(
                    userId = userInfo.id,
                    userIds = userGroup.userInfos.map { it.id },
                    showInstantApps = showInstantApps,
                ),
                listModel = listModel,
Loading