diff options
| author | 2022-08-24 09:14:57 +0000 | |
|---|---|---|
| committer | 2022-08-24 09:14:57 +0000 | |
| commit | f1d9af06a534278ffdb419ad19111875575b7d6f (patch) | |
| tree | e70dcb8f1c5df0d59fbb8d9673c1b30b9c32e73c | |
| parent | df030f3fbdec0c0d27824ccc291957036603ebf4 (diff) | |
| parent | f2b3938aebf67d70f8147b2427dd9ac8dd634783 (diff) | |
Merge "Create AppList for SpaPrivilegedLib"
15 files changed, 630 insertions, 13 deletions
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 5351ea65da47..8e2c3d6667a9 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.framework.theme.SettingsTheme 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 @@ object SettingsPagerPageProvider : SettingsPageProvider { @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 000000000000..4eef2a8ffbbf --- /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 7d3e10768e46..965436847c64 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 @@ object SettingsDimension { 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 000000000000..ba253368d505 --- /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 000000000000..999d8d7dd16a --- /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 a414c89dc24c..59b413cef56e 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 @@ fun SettingsBody(body: String) { ) } } + +@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 ecbb219c64b5..a6469b5172bb 100644 --- a/packages/SettingsLib/SpaPrivileged/Android.bp +++ b/packages/SettingsLib/SpaPrivileged/Android.bp @@ -28,5 +28,8 @@ android_library { "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 000000000000..8f8dd2b01ecd --- /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 000000000000..2fa869c529d4 --- /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 000000000000..9265158b3b4a --- /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 7ffa9384bfe2..34f12af28dce 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 @@ import kotlinx.coroutines.withContext fun rememberAppRepository(): AppRepository = rememberContext(::AppRepositoryImpl) interface AppRepository { + fun loadLabel(app: ApplicationInfo): String + @Composable fun produceLabel(app: ApplicationInfo): State<String> @@ -38,9 +40,11 @@ interface AppRepository { 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 000000000000..6a6462098230 --- /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 d802b049b830..58d0f8d398f2 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.Composable 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 @@ fun AppInfo(packageName: String, userId: Int) { 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 @@ fun AppInfo(packageName: String, userId: Int) { } @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 000000000000..c60976ddea4d --- /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 000000000000..ac3f8ff79091 --- /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) {} + } +} |