Merge "Create AppList for SpaPrivilegedLib"

This commit is contained in:
Chaohui Wang
2022-08-24 09:14:57 +00:00
committed by Android (Google) Code Review
15 changed files with 630 additions and 13 deletions

View File

@@ -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")
}
}

View File

@@ -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}")
}
}

View File

@@ -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
}

View File

@@ -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 }

View File

@@ -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
}
}
}

View File

@@ -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,
)
}
}

View File

@@ -28,5 +28,8 @@ android_library {
"SettingsLib",
"androidx.compose.runtime_runtime",
],
kotlincflags: ["-Xjvm-default=all"],
kotlincflags: [
"-Xjvm-default=all",
"-Xopt-in=kotlin.RequiresOptIn",
],
}

View File

@@ -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>

View File

@@ -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>?
}

View File

@@ -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)
}
}

View File

@@ -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) {

View File

@@ -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
}
}
}

View File

@@ -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)
)
}

View File

@@ -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()

View File

@@ -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) {}
}
}