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 5351ea65da476..8e2c3d6667a9d 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 0000000000000..4eef2a8ffbbfc --- /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 7d3e10768e460..965436847c642 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 0000000000000..ba253368d505e --- /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 Iterable.asyncMap(crossinline transform: (T) -> R): List = + coroutineScope { + map { item -> + async { transform(item) } + }.awaitAll() + } + +suspend inline fun Iterable.asyncFilter(crossinline predicate: (T) -> Boolean): List = + 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 0000000000000..999d8d7dd16a9 --- /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 Flow>.asyncMapItem(crossinline transform: (T) -> R): Flow> = + map { list -> list.asyncMap(transform) } + +@OptIn(ExperimentalCoroutinesApi::class) +inline fun Flow.mapState(crossinline block: (T) -> State): Flow = + flatMapLatest { snapshotFlow { block(it).value } } + +fun Flow.waitFirst(flow: Flow): Flow = + combine(flow.distinctUntilChangedBy {}) { value, _ -> value } + +class StateFlowBridge { + private val stateFlow = MutableStateFlow(null) + val flow = stateFlow.filterNotNull() + + fun setIfAbsent(value: T) { + if (stateFlow.value == null) { + stateFlow.value = value + } + } + + @Composable + fun Sync(state: State) { + 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 a414c89dc24c6..59b413cef56e0 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) { @@ -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 ecbb219c64b57..a6469b5172bb6 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 0000000000000..8f8dd2b01ecde --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/res/values/strings.xml @@ -0,0 +1,20 @@ + + + + + No apps. + 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 0000000000000..2fa869c529d40 --- /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( + val record: T, + val label: String, + val labelCollationKey: CollationKey, +) + +interface AppListModel { + fun getSpinnerOptions(): List = emptyList() + fun transform(userIdFlow: Flow, appListFlow: Flow>): Flow> + fun filter(userIdFlow: Flow, option: Int, recordListFlow: Flow>): Flow> + + suspend fun onFirstLoaded(recordList: List) {} + fun getComparator(option: Int): Comparator> = compareBy( + { it.labelCollationKey }, + { it.record.app.packageName }, + { it.record.app.uid }, + ) + + fun getSummary(option: Int, record: T): Flow? +} 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 0000000000000..9265158b3b4a6 --- /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( + val appEntries: List>, + val option: Int, +) { + fun filter(predicate: (AppEntry) -> Boolean) = + AppListData(appEntries.filter(predicate), option) +} + +@OptIn(ExperimentalCoroutinesApi::class) +internal class AppListViewModel( + application: Application, +) : AndroidViewModel(application) { + val userInfo = StateFlowBridge() + val listModel = StateFlowBridge>() + val showSystem = StateFlowBridge() + val option = StateFlowBridge() + val searchQuery = StateFlowBridge() + + private val appsRepository = AppsRepository(application) + private val appRepository = AppRepositoryImpl(application) + private val collator = Collator.getInstance().freeze() + private val labelMap = ConcurrentHashMap() + 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.maybePreFetchLabels(recordList: List) { + 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 7ffa9384bfe29..34f12af28dce5 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 @@ -38,9 +40,11 @@ interface AppRepository { fun produceIcon(app: ApplicationInfo): State } -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 0000000000000..6a6462098230e --- /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): Flow> = userInfoFlow + .map { loadApps(it) } + .flowOn(Dispatchers.Default) + + private suspend fun loadApps(userInfo: UserInfo): List { + 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, + showSystemFlow: Flow, + ): 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 { + 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() + packageManager.getHomeActivities(homeActivities) + (launcherActivities.await() + homeActivities) + .map { it.activityInfo.packageName } + .toSet() + } + } + + companion object { + private fun ApplicationInfo.isInAppList(hiddenSystemModules: Set) = + 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 d802b049b8301..58d0f8d398f2c 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 0000000000000..c60976ddea4de --- /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 AppList( + userInfo: UserInfo, + listModel: AppListModel, + showSystem: State, + option: State, + searchQuery: State, + appItem: @Composable (itemState: AppListItemModel) -> Unit, +) { + LogCompositions(TAG, userInfo.id.toString()) + val appListData = loadAppEntries(userInfo, listModel, showSystem, option, searchQuery) + AppListWidget(appListData, listModel, appItem) +} + +@Composable +private fun AppListWidget( + appListData: State?>, + listModel: AppListModel, + appItem: @Composable (itemState: AppListItemModel) -> 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 loadAppEntries( + userInfo: UserInfo, + listModel: AppListModel, + showSystem: State, + option: State, + searchQuery: State, +): State?> { + val viewModel: AppListViewModel = 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 getSummary( + listModel: AppListModel, + option: Int, + record: T, +): State = 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 0000000000000..ac3f8ff79091e --- /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( + val record: T, + val label: String, + val summary: State, +) + +@Composable +fun AppListItem( + itemModel: AppListItemModel, + 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(record, "Chrome", "Allowed".toState()) + AppListItem(itemModel) {} + } +}