Create AppList for SpaPrivilegedLib
Bug: 235727273
Test: Manual with Test App
Change-Id: Ia5b179d311fe9172c705ad036588e172a5e2a5ca
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
index 5351ea6..8e2c3d6 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
@@ -17,11 +17,7 @@
package com.android.settingslib.spa.gallery.page
import android.os.Bundle
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.api.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
@@ -29,7 +25,7 @@
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.SettingsPager
-import com.android.settingslib.spa.widget.ui.SettingsTitle
+import com.android.settingslib.spa.widget.ui.PlaceholderTitle
object SettingsPagerPageProvider : SettingsPageProvider {
override val name = "SettingsPager"
@@ -51,9 +47,7 @@
@Composable
private fun SettingsPagerPage() {
SettingsPager(listOf("Personal", "Work")) {
- Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- SettingsTitle(title = "Page $it")
- }
+ PlaceholderTitle("Page $it")
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LogCompositions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LogCompositions.kt
new file mode 100644
index 0000000..4eef2a8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LogCompositions.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 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.settingslib.spa.framework.compose
+
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.remember
+
+const val ENABLE_LOG_COMPOSITIONS = false
+
+data class LogCompositionsRef(var count: Int)
+
+// Note the inline function below which ensures that this function is essentially
+// copied at the call site to ensure that its logging only recompositions from the
+// original call site.
+@Suppress("NOTHING_TO_INLINE")
+@Composable
+inline fun LogCompositions(tag: String, msg: String) {
+ if (ENABLE_LOG_COMPOSITIONS) {
+ val ref = remember { LogCompositionsRef(0) }
+ SideEffect { ref.count++ }
+ Log.d(tag, "Compositions $msg: ${ref.count}")
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
index 7d3e107..9654368 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
@@ -32,4 +32,10 @@
bottom = itemPaddingVertical,
)
val itemPaddingAround = 8.dp
+
+ /** The size when app icon is displayed in list. */
+ val appIconItemSize = 32.dp
+
+ /** The size when app icon is displayed in App info page. */
+ val appIconInfoSize = 48.dp
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Collections.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Collections.kt
new file mode 100644
index 0000000..ba25336
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Collections.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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.settingslib.spa.framework.util
+
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+
+suspend inline fun <R, T> Iterable<T>.asyncMap(crossinline transform: (T) -> R): List<R> =
+ coroutineScope {
+ map { item ->
+ async { transform(item) }
+ }.awaitAll()
+ }
+
+suspend inline fun <T> Iterable<T>.asyncFilter(crossinline predicate: (T) -> Boolean): List<T> =
+ asyncMap { item -> item to predicate(item) }
+ .filter { it.second }
+ .map { it.first }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt
new file mode 100644
index 0000000..999d8d7
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/Flows.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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.settingslib.spa.framework.util
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.snapshotFlow
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+
+inline fun <T, R> Flow<List<T>>.asyncMapItem(crossinline transform: (T) -> R): Flow<List<R>> =
+ map { list -> list.asyncMap(transform) }
+
+@OptIn(ExperimentalCoroutinesApi::class)
+inline fun <T, R> Flow<T>.mapState(crossinline block: (T) -> State<R>): Flow<R> =
+ flatMapLatest { snapshotFlow { block(it).value } }
+
+fun <T1, T2> Flow<T1>.waitFirst(flow: Flow<T2>): Flow<T1> =
+ combine(flow.distinctUntilChangedBy {}) { value, _ -> value }
+
+class StateFlowBridge<T> {
+ private val stateFlow = MutableStateFlow<T?>(null)
+ val flow = stateFlow.filterNotNull()
+
+ fun setIfAbsent(value: T) {
+ if (stateFlow.value == null) {
+ stateFlow.value = value
+ }
+ }
+
+ @Composable
+ fun Sync(state: State<T>) {
+ LaunchedEffect(state.value) {
+ stateFlow.value = state.value
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
index a414c89..59b413c 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
@@ -16,10 +16,14 @@
package com.android.settingslib.spa.widget.ui
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
@Composable
fun SettingsTitle(title: State<String>) {
@@ -50,3 +54,17 @@
)
}
}
+
+@Composable
+fun PlaceholderTitle(title: String) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = title,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp
index ecbb219..a6469b5 100644
--- a/packages/SettingsLib/SpaPrivileged/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/Android.bp
@@ -28,5 +28,8 @@
"SettingsLib",
"androidx.compose.runtime_runtime",
],
- kotlincflags: ["-Xjvm-default=all"],
+ kotlincflags: [
+ "-Xjvm-default=all",
+ "-Xopt-in=kotlin.RequiresOptIn",
+ ],
}
diff --git a/packages/SettingsLib/SpaPrivileged/res/values/strings.xml b/packages/SettingsLib/SpaPrivileged/res/values/strings.xml
new file mode 100644
index 0000000..8f8dd2b
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 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.
+-->
+<resources>
+ <!-- [CHAR LIMIT=25] Text shown when there are no applications to display. -->
+ <string name="no_applications">No apps.</string>
+</resources>
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt
new file mode 100644
index 0000000..2fa869c
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt
@@ -0,0 +1,26 @@
+package com.android.settingslib.spaprivileged.model.app
+
+import android.content.pm.ApplicationInfo
+import android.icu.text.CollationKey
+import kotlinx.coroutines.flow.Flow
+
+data class AppEntry<T : AppRecord>(
+ val record: T,
+ val label: String,
+ val labelCollationKey: CollationKey,
+)
+
+interface AppListModel<T : AppRecord> {
+ fun getSpinnerOptions(): List<String> = emptyList()
+ fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>): Flow<List<T>>
+ fun filter(userIdFlow: Flow<Int>, option: Int, recordListFlow: Flow<List<T>>): Flow<List<T>>
+
+ suspend fun onFirstLoaded(recordList: List<T>) {}
+ fun getComparator(option: Int): Comparator<AppEntry<T>> = compareBy(
+ { it.labelCollationKey },
+ { it.record.app.packageName },
+ { it.record.app.uid },
+ )
+
+ fun getSummary(option: Int, record: T): Flow<String>?
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt
new file mode 100644
index 0000000..9265158
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 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.settingslib.spaprivileged.model.app
+
+import android.app.Application
+import android.content.pm.ApplicationInfo
+import android.content.pm.UserInfo
+import android.icu.text.Collator
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+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 java.util.concurrent.ConcurrentHashMap
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.plus
+
+internal data class AppListData<T : AppRecord>(
+ val appEntries: List<AppEntry<T>>,
+ val option: Int,
+) {
+ fun filter(predicate: (AppEntry<T>) -> Boolean) =
+ AppListData(appEntries.filter(predicate), option)
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class AppListViewModel<T : AppRecord>(
+ application: Application,
+) : AndroidViewModel(application) {
+ val userInfo = StateFlowBridge<UserInfo>()
+ val listModel = StateFlowBridge<AppListModel<T>>()
+ val showSystem = StateFlowBridge<Boolean>()
+ val option = StateFlowBridge<Int>()
+ val searchQuery = StateFlowBridge<String>()
+
+ private val appsRepository = AppsRepository(application)
+ private val appRepository = AppRepositoryImpl(application)
+ private val collator = Collator.getInstance().freeze()
+ private val labelMap = ConcurrentHashMap<String, String>()
+ private val scope = viewModelScope + Dispatchers.Default
+
+ private val userIdFlow = userInfo.flow.map { it.id }
+
+ private val recordListFlow = listModel.flow
+ .flatMapLatest { it.transform(userIdFlow, appsRepository.loadApps(userInfo.flow)) }
+ .shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
+
+ private val systemFilteredFlow = appsRepository.showSystemPredicate(userIdFlow, showSystem.flow)
+ .combine(recordListFlow) { showAppPredicate, recordList ->
+ recordList.filter { showAppPredicate(it.app) }
+ }
+
+ val appListDataFlow = option.flow.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()
+ }
+
+ private fun filterAndSort(option: Int) = listModel.flow.flatMapLatest { listModel ->
+ listModel.filter(userIdFlow, option, systemFilteredFlow)
+ .asyncMapItem { record ->
+ val label = getLabel(record.app)
+ AppEntry(
+ record = record,
+ label = label,
+ labelCollationKey = collator.getCollationKey(label),
+ )
+ }
+ .map { appEntries ->
+ AppListData(
+ appEntries = appEntries.sortedWith(listModel.getComparator(option)),
+ option = option,
+ )
+ }
+ }
+
+ private fun scheduleOnFirstLoaded() {
+ recordListFlow
+ .waitFirst(appListDataFlow)
+ .combine(listModel.flow) { recordList, listModel ->
+ listModel.maybePreFetchLabels(recordList)
+ listModel.onFirstLoaded(recordList)
+ }
+ .launchIn(scope)
+ }
+
+ private fun AppListModel<T>.maybePreFetchLabels(recordList: List<T>) {
+ if (getSpinnerOptions().isNotEmpty()) {
+ for (record in recordList) {
+ getLabel(record.app)
+ }
+ }
+ }
+
+ private fun getLabel(app: ApplicationInfo) = labelMap.computeIfAbsent(app.packageName) {
+ appRepository.loadLabel(app)
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
index 7ffa938..34f12af 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
@@ -31,6 +31,8 @@
fun rememberAppRepository(): AppRepository = rememberContext(::AppRepositoryImpl)
interface AppRepository {
+ fun loadLabel(app: ApplicationInfo): String
+
@Composable
fun produceLabel(app: ApplicationInfo): State<String>
@@ -38,9 +40,11 @@
fun produceIcon(app: ApplicationInfo): State<Drawable?>
}
-private class AppRepositoryImpl(private val context: Context) : AppRepository {
+internal class AppRepositoryImpl(private val context: Context) : AppRepository {
private val packageManager = context.packageManager
+ override fun loadLabel(app: ApplicationInfo): String = app.loadLabel(packageManager).toString()
+
@Composable
override fun produceLabel(app: ApplicationInfo) = produceState(initialValue = "", app) {
withContext(Dispatchers.Default) {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppsRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppsRepository.kt
new file mode 100644
index 0000000..6a64620
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppsRepository.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 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.settingslib.spaprivileged.model.app
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.UserInfo
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+class AppsRepository(context: Context) {
+ private val packageManager = context.packageManager
+
+ fun loadApps(userInfoFlow: Flow<UserInfo>): Flow<List<ApplicationInfo>> = userInfoFlow
+ .map { loadApps(it) }
+ .flowOn(Dispatchers.Default)
+
+ private suspend fun loadApps(userInfo: UserInfo): List<ApplicationInfo> {
+ return coroutineScope {
+ val hiddenSystemModulesDeferred = async {
+ packageManager.getInstalledModules(0)
+ .filter { it.isHidden }
+ .map { it.packageName }
+ .toSet()
+ }
+ val flags = PackageManager.ApplicationInfoFlags.of(
+ ((if (userInfo.isAdmin) PackageManager.MATCH_ANY_USER else 0) or
+ PackageManager.MATCH_DISABLED_COMPONENTS or
+ PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
+ )
+ val installedApplicationsAsUser =
+ packageManager.getInstalledApplicationsAsUser(flags, userInfo.id)
+
+ val hiddenSystemModules = hiddenSystemModulesDeferred.await()
+ installedApplicationsAsUser.filter { app ->
+ app.isInAppList(hiddenSystemModules)
+ }
+ }
+ }
+
+ fun showSystemPredicate(
+ userIdFlow: Flow<Int>,
+ showSystemFlow: Flow<Boolean>,
+ ): Flow<(app: ApplicationInfo) -> Boolean> =
+ userIdFlow.combine(showSystemFlow) { userId, showSystem ->
+ showSystemPredicate(userId, showSystem)
+ }
+
+ private suspend fun showSystemPredicate(
+ userId: Int,
+ showSystem: Boolean,
+ ): (app: ApplicationInfo) -> Boolean {
+ if (showSystem) return { true }
+ val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
+ return { app ->
+ app.isUpdatedSystemApp || !app.isSystemApp || app.packageName in homeOrLauncherPackages
+ }
+ }
+
+ private 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(
+ (PackageManager.MATCH_DISABLED_COMPONENTS or
+ PackageManager.MATCH_DIRECT_BOOT_AWARE or
+ PackageManager.MATCH_DIRECT_BOOT_UNAWARE).toLong()
+ )
+ return coroutineScope {
+ val launcherActivities = async {
+ packageManager.queryIntentActivitiesAsUser(launchIntent, flags, userId)
+ }
+ val homeActivities = ArrayList<ResolveInfo>()
+ packageManager.getHomeActivities(homeActivities)
+ (launcherActivities.await() + homeActivities)
+ .map { it.activityInfo.packageName }
+ .toSet()
+ }
+ }
+
+ companion object {
+ private fun ApplicationInfo.isInAppList(hiddenSystemModules: Set<String>) =
+ when {
+ packageName in hiddenSystemModules -> false
+ enabled -> true
+ enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true
+ else -> false
+ }
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
index d802b04..58d0f8d 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -29,8 +29,10 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
+import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.ui.SettingsBody
import com.android.settingslib.spa.widget.ui.SettingsTitle
import com.android.settingslib.spaprivileged.model.app.PackageManagers
@@ -45,7 +47,7 @@
horizontalAlignment = Alignment.CenterHorizontally) {
val packageInfo = remember { PackageManagers.getPackageInfoAsUser(packageName, userId) }
Box(modifier = Modifier.padding(8.dp)) {
- AppIcon(app = packageInfo.applicationInfo, size = 48)
+ AppIcon(app = packageInfo.applicationInfo, size = SettingsDimension.appIconInfoSize)
}
AppLabel(packageInfo.applicationInfo)
Spacer(modifier = Modifier.height(4.dp))
@@ -54,12 +56,12 @@
}
@Composable
-fun AppIcon(app: ApplicationInfo, size: Int) {
+fun AppIcon(app: ApplicationInfo, size: Dp) {
val appRepository = rememberAppRepository()
Image(
painter = rememberDrawablePainter(appRepository.produceIcon(app).value),
contentDescription = null,
- modifier = Modifier.size(size.dp)
+ modifier = Modifier.size(size)
)
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
new file mode 100644
index 0000000..c60976d
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 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.settingslib.spaprivileged.template.app
+
+import android.content.pm.UserInfo
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.settingslib.spa.framework.compose.LogCompositions
+import com.android.settingslib.spa.framework.compose.toState
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.ui.PlaceholderTitle
+import com.android.settingslib.spaprivileged.R
+import com.android.settingslib.spaprivileged.model.app.AppListData
+import com.android.settingslib.spaprivileged.model.app.AppListModel
+import com.android.settingslib.spaprivileged.model.app.AppListViewModel
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import kotlinx.coroutines.Dispatchers
+
+private const val TAG = "AppList"
+
+@Composable
+fun <T : AppRecord> AppList(
+ userInfo: UserInfo,
+ listModel: AppListModel<T>,
+ showSystem: State<Boolean>,
+ option: State<Int>,
+ searchQuery: State<String>,
+ appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
+) {
+ LogCompositions(TAG, userInfo.id.toString())
+ val appListData = loadAppEntries(userInfo, listModel, showSystem, option, searchQuery)
+ AppListWidget(appListData, listModel, appItem)
+}
+
+@Composable
+private fun <T : AppRecord> AppListWidget(
+ appListData: State<AppListData<T>?>,
+ listModel: AppListModel<T>,
+ appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
+) {
+ appListData.value?.let { (list, option) ->
+ if (list.isEmpty()) {
+ PlaceholderTitle(stringResource(R.string.no_applications))
+ return
+ }
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = rememberLazyListState(),
+ contentPadding = PaddingValues(bottom = SettingsDimension.itemPaddingVertical),
+ ) {
+ items(count = list.size, key = { option to list[it].record.app.packageName }) {
+ val appEntry = list[it]
+ val summary = getSummary(listModel, option, appEntry.record)
+ val itemModel = remember(appEntry) {
+ AppListItemModel(appEntry.record, appEntry.label, summary)
+ }
+ appItem(itemModel)
+ }
+ }
+ }
+}
+
+@Composable
+private fun <T : AppRecord> loadAppEntries(
+ userInfo: UserInfo,
+ listModel: AppListModel<T>,
+ showSystem: State<Boolean>,
+ option: State<Int>,
+ searchQuery: State<String>,
+): State<AppListData<T>?> {
+ val viewModel: AppListViewModel<T> = viewModel(key = userInfo.id.toString())
+ viewModel.userInfo.setIfAbsent(userInfo)
+ viewModel.listModel.setIfAbsent(listModel)
+ viewModel.showSystem.Sync(showSystem)
+ viewModel.option.Sync(option)
+ viewModel.searchQuery.Sync(searchQuery)
+
+ return viewModel.appListDataFlow.collectAsState(null, Dispatchers.Default)
+}
+
+@Composable
+private fun <T : AppRecord> getSummary(
+ listModel: AppListModel<T>,
+ option: Int,
+ record: T,
+): State<String> = remember(option) { listModel.getSummary(option, record) }
+ ?.collectAsState(stringResource(R.string.summary_placeholder), Dispatchers.Default)
+ ?: "".toState()
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt
new file mode 100644
index 0000000..ac3f8ff
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 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.settingslib.spaprivileged.template.app
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.compose.toState
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+
+class AppListItemModel<T : AppRecord>(
+ val record: T,
+ val label: String,
+ val summary: State<String>,
+)
+
+@Composable
+fun <T : AppRecord> AppListItem(
+ itemModel: AppListItemModel<T>,
+ onClick: () -> Unit,
+) {
+ Preference(remember {
+ object : PreferenceModel {
+ override val title = itemModel.label
+ override val summary = itemModel.summary
+ override val icon = @Composable {
+ AppIcon(app = itemModel.record.app, size = SettingsDimension.appIconItemSize)
+ }
+ override val onClick = onClick
+ }
+ })
+}
+
+@Preview
+@Composable
+private fun AppListItemPreview() {
+ SettingsTheme {
+ val record = object : AppRecord {
+ override val app = LocalContext.current.applicationInfo
+ }
+ val itemModel = AppListItemModel<AppRecord>(record, "Chrome", "Allowed".toState())
+ AppListItem(itemModel) {}
+ }
+}