Merge "Create AppList for SpaPrivilegedLib"
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,5 +28,8 @@ android_library {
|
||||
"SettingsLib",
|
||||
"androidx.compose.runtime_runtime",
|
||||
],
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
kotlincflags: [
|
||||
"-Xjvm-default=all",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
],
|
||||
}
|
||||
|
||||
20
packages/SettingsLib/SpaPrivileged/res/values/strings.xml
Normal file
20
packages/SettingsLib/SpaPrivileged/res/values/strings.xml
Normal 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>
|
||||
@@ -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>?
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user