fix: 首次提交

This commit is contained in:
2024-12-09 11:25:23 +08:00
parent d0c01071e9
commit 2c2109a5f3
4741 changed files with 290641 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2023 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.database
import android.content.Context
import android.database.ContentObserver
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
/** Content change flow for the given [uri]. */
internal fun Context.contentChangeFlow(
uri: Uri,
sendInitial: Boolean = true,
): Flow<Unit> = callbackFlow {
val contentObserver = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
trySend(Unit)
}
}
contentResolver.registerContentObserver(uri, false, contentObserver)
if (sendInitial) {
trySend(Unit)
}
awaitClose { contentResolver.unregisterContentObserver(contentObserver) }
}.conflate().flowOn(Dispatchers.Default)

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2023 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.framework.common
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.UserHandle
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
private const val TAG = "BroadcastReceiverAsUser"
/**
* A [BroadcastReceiver] flow for the given [intentFilter].
*/
fun Context.broadcastReceiverAsUserFlow(
intentFilter: IntentFilter,
userHandle: UserHandle,
): Flow<Intent> = callbackFlow {
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
trySend(intent)
}
}
registerReceiverAsUser(
broadcastReceiver,
userHandle,
intentFilter,
null,
null,
Context.RECEIVER_NOT_EXPORTED,
)
awaitClose { unregisterReceiver(broadcastReceiver) }
}.catch { e ->
Log.e(TAG, "Error while broadcastReceiverAsUserFlow", e)
}.conflate().flowOn(Dispatchers.Default)

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 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.framework.common
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
private const val TAG = "BroadcastReceiverFlow"
/**
* A [BroadcastReceiver] flow for the given [intentFilter].
*/
fun Context.broadcastReceiverFlow(intentFilter: IntentFilter): Flow<Intent> = callbackFlow {
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "onReceive: $intent")
trySend(intent)
}
}
registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_VISIBLE_TO_INSTANT_APPS)
awaitClose { unregisterReceiver(broadcastReceiver) }
}.catch { e ->
Log.e(TAG, "Error while broadcastReceiverFlow", e)
}.conflate().flowOn(Dispatchers.Default)

View File

@@ -0,0 +1,65 @@
/*
* 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.framework.common
import android.app.ActivityManager
import android.app.AlarmManager
import android.app.AppOpsManager
import android.app.admin.DevicePolicyManager
import android.app.usage.StorageStatsManager
import android.apphibernation.AppHibernationManager
import android.content.Context
import android.content.pm.CrossProfileApps
import android.content.pm.verify.domain.DomainVerificationManager
import android.os.UserHandle
import android.os.UserManager
import android.permission.PermissionControllerManager
/** The [ActivityManager] instance. */
val Context.activityManager get() = getSystemService(ActivityManager::class.java)!!
/** The [AlarmManager] instance. */
val Context.alarmManager get() = getSystemService(AlarmManager::class.java)!!
/** The [AppHibernationManager] instance. */
val Context.appHibernationManager get() = getSystemService(AppHibernationManager::class.java)!!
/** The [AppOpsManager] instance. */
val Context.appOpsManager get() = getSystemService(AppOpsManager::class.java)!!
/** The [CrossProfileApps] instance. */
val Context.crossProfileApps get() = getSystemService(CrossProfileApps::class.java)!!
/** The [DevicePolicyManager] instance. */
val Context.devicePolicyManager get() = getSystemService(DevicePolicyManager::class.java)!!
/** The [DomainVerificationManager] instance. */
val Context.domainVerificationManager
get() = getSystemService(DomainVerificationManager::class.java)!!
/** The [PermissionControllerManager] instance. */
val Context.permissionControllerManager
get() = getSystemService(PermissionControllerManager::class.java)!!
/** The [StorageStatsManager] instance. */
val Context.storageStatsManager get() = getSystemService(StorageStatsManager::class.java)!!
/** The [UserManager] instance. */
val Context.userManager get() = getSystemService(UserManager::class.java)!!
/** Gets a new [Context] for the given [UserHandle]. */
fun Context.asUser(userHandle: UserHandle): Context = createContextAsUser(userHandle, 0)

View File

@@ -0,0 +1,40 @@
/*
* 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.framework.compose
import android.content.BroadcastReceiver
import android.content.Intent
import android.content.IntentFilter
import android.os.UserHandle
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverAsUserFlow
/**
* A [BroadcastReceiver] which registered when on start and unregistered when on stop.
*/
@Composable
fun DisposableBroadcastReceiverAsUser(
intentFilter: IntentFilter,
userHandle: UserHandle,
onReceive: (Intent) -> Unit,
) {
LocalContext.current.broadcastReceiverAsUserFlow(intentFilter, userHandle)
.collectLatestWithLifecycle(LocalLifecycleOwner.current, action = onReceive)
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 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.framework.compose
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.android.settingslib.R
/** An empty placer holder string. */
@Composable
fun placeholder() = stringResource(R.string.summary_placeholder)
/** Gets an empty placer holder string. */
fun Context.getPlaceholder(): String = getString(R.string.summary_placeholder)

View File

@@ -0,0 +1,97 @@
/*
* 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.pm.ApplicationInfo
import android.icu.text.CollationKey
import androidx.compose.runtime.Composable
import com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.template.app.AppListItem
import com.android.settingslib.spaprivileged.template.app.AppListItemModel
import kotlinx.coroutines.flow.Flow
data class AppEntry<T : AppRecord>(
val record: T,
val label: String,
val labelCollationKey: CollationKey,
)
/**
* Implement this interface to build an App List.
*/
interface AppListModel<T : AppRecord> {
/**
* Returns the spinner options available to the App List.
*
* Default no spinner will be shown.
*/
fun getSpinnerOptions(recordList: List<T>): List<SpinnerOption> = emptyList()
/**
* Loads the extra info for the App List, and generates the [AppRecord] List.
*/
fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>): Flow<List<T>>
/**
* Filters the [AppRecord] list.
*
* @return the [AppRecord] list which will be displayed.
*/
fun filter(userIdFlow: Flow<Int>, option: Int, recordListFlow: Flow<List<T>>): Flow<List<T>> =
recordListFlow
/**
* This function is called when the App List's loading is finished and displayed to the user.
*
* Could do some pre-cache here.
*
* @return true to enable pre-fetching app labels.
*/
suspend fun onFirstLoaded(recordList: List<T>) = false
/**
* Gets the comparator to sort the App List.
*
* Default sorting is based on the app label.
*/
fun getComparator(option: Int): Comparator<AppEntry<T>> = compareBy(
{ it.labelCollationKey },
{ it.record.app.packageName },
{ it.record.app.uid },
)
/**
* Gets the group title of this item.
*
* Note: Items should be sorted by group in [getComparator] first, this [getGroupTitle] will not
* change the list order.
*/
fun getGroupTitle(option: Int, record: T): String? = null
/**
* Gets the summary for the given app record.
*
* @return null if no summary should be displayed.
*/
@Composable
fun getSummary(option: Int, record: T): (() -> String)? = null
@Composable
fun AppListItemModel<T>.AppItem() {
AppListItem {}
}
}

View File

@@ -0,0 +1,233 @@
/*
* 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.FeatureFlags
import android.content.pm.FeatureFlagsImpl
import android.content.pm.Flags
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.ResolveInfo
import android.os.SystemProperties
import android.util.Log
import com.android.internal.R
import com.android.settingslib.spaprivileged.framework.common.userManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.runBlocking
/**
* The repository to load the App List data.
*/
interface AppListRepository {
/** Loads the list of [ApplicationInfo]. */
suspend fun loadApps(
userId: Int,
loadInstantApps: Boolean = false,
matchAnyUserForAdmin: Boolean = false,
): List<ApplicationInfo>
/** Gets the flow of predicate that could used to filter system app. */
fun showSystemPredicate(
userIdFlow: Flow<Int>,
showSystemFlow: Flow<Boolean>,
): Flow<(app: ApplicationInfo) -> Boolean>
/** Gets the system app package names. */
fun getSystemPackageNamesBlocking(userId: Int): Set<String>
/** Loads the list of [ApplicationInfo], and filter base on `isSystemApp`. */
suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean): List<ApplicationInfo>
}
/**
* Util for app list repository.
*/
object AppListRepositoryUtil {
/** Gets the system app package names. */
@JvmStatic
fun getSystemPackageNames(context: Context, userId: Int): Set<String> =
AppListRepositoryImpl(context).getSystemPackageNamesBlocking(userId)
}
/**
* This constructor is visible for tests only in order to override `featureFlags`.
*/
class AppListRepositoryImpl(
private val context: Context,
private val featureFlags: FeatureFlags
) : AppListRepository {
private val packageManager = context.packageManager
private val userManager = context.userManager
constructor(context: Context) : this(context, FeatureFlagsImpl())
override suspend fun loadApps(
userId: Int,
loadInstantApps: Boolean,
matchAnyUserForAdmin: Boolean,
): List<ApplicationInfo> = try {
coroutineScope {
val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() }
val hideWhenDisabledPackagesDeferred = async {
context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)
}
val installedApplicationsAsUser =
getInstalledApplications(userId, matchAnyUserForAdmin)
val hiddenSystemModules = hiddenSystemModulesDeferred.await()
val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await()
installedApplicationsAsUser.filter { app ->
app.isInAppList(loadInstantApps, hiddenSystemModules, hideWhenDisabledPackages)
}
}
} catch (e: Exception) {
Log.e(TAG, "loadApps failed", e)
emptyList()
}
private suspend fun getInstalledApplications(
userId: Int,
matchAnyUserForAdmin: Boolean,
): List<ApplicationInfo> {
val disabledComponentsFlag = (PackageManager.MATCH_DISABLED_COMPONENTS or
PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
val archivedPackagesFlag: Long = if (isArchivingEnabled(featureFlags))
PackageManager.MATCH_ARCHIVED_PACKAGES else 0L
val regularFlags = ApplicationInfoFlags.of(
disabledComponentsFlag or
archivedPackagesFlag
)
return if (!matchAnyUserForAdmin || !userManager.getUserInfo(userId).isAdmin) {
packageManager.getInstalledApplicationsAsUser(regularFlags, userId)
} else {
coroutineScope {
val deferredPackageNamesInChildProfiles =
userManager.getProfileIdsWithDisabled(userId)
.filter { it != userId }
.map {
async {
packageManager.getInstalledApplicationsAsUser(regularFlags, it)
.map { it.packageName }
}
}
val adminFlags = ApplicationInfoFlags.of(
PackageManager.MATCH_ANY_USER.toLong() or regularFlags.value
)
val allInstalledApplications =
packageManager.getInstalledApplicationsAsUser(adminFlags, userId)
val packageNamesInChildProfiles = deferredPackageNamesInChildProfiles
.awaitAll()
.flatten()
.toSet()
// If an app is for a child profile and not installed on the owner, not display as
// 'not installed for this user' in the owner. This will prevent duplicates of work
// only apps showing up in the personal profile.
allInstalledApplications.filter {
it.installed || it.packageName !in packageNamesInChildProfiles
}
}
}
}
private fun isArchivingEnabled(featureFlags: FeatureFlags) =
featureFlags.archiving() || SystemProperties.getBoolean("pm.archiving.enabled", false)
override fun showSystemPredicate(
userIdFlow: Flow<Int>,
showSystemFlow: Flow<Boolean>,
): Flow<(app: ApplicationInfo) -> Boolean> =
userIdFlow.combine(showSystemFlow, ::showSystemPredicate)
override fun getSystemPackageNamesBlocking(userId: Int) = runBlocking {
loadAndFilterApps(userId = userId, isSystemApp = true).map { it.packageName }.toSet()
}
override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) = coroutineScope {
val loadAppsDeferred = async { loadApps(userId) }
val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
loadAppsDeferred.await().filter { app ->
isSystemApp(app, homeOrLauncherPackages) == isSystemApp
}
}
private suspend fun showSystemPredicate(
userId: Int,
showSystem: Boolean,
): (app: ApplicationInfo) -> Boolean {
if (showSystem) return { true }
val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
return { app -> !isSystemApp(app, 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()
}
}
private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean =
app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages
private fun PackageManager.getHiddenSystemModules(): Set<String> {
val moduleInfos = getInstalledModules(0).filter { it.isHidden }
val hiddenApps = moduleInfos.mapNotNull { it.packageName }.toMutableSet()
if (Flags.provideInfoOfApkInApex()) {
hiddenApps += moduleInfos.flatMap { it.apkInApexPackageNames }
}
return hiddenApps
}
companion object {
private const val TAG = "AppListRepository"
private fun ApplicationInfo.isInAppList(
showInstantApps: Boolean,
hiddenSystemModules: Set<String>,
hideWhenDisabledPackages: Array<String>,
) = when {
!showInstantApps && isInstantApp -> false
packageName in hiddenSystemModules -> false
packageName in hideWhenDisabledPackages -> enabled && !isDisabledUntilUsed
enabled -> true
else -> enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
}
}
}

View File

@@ -0,0 +1,192 @@
/*
* 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.Context
import android.content.pm.ApplicationInfo
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 com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.template.app.AppListConfig
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
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)
}
internal interface IAppListViewModel<T : AppRecord> {
val optionFlow: MutableStateFlow<Int?>
val spinnerOptionsFlow: Flow<List<SpinnerOption>>
val appListDataFlow: Flow<AppListData<T>>
}
internal class AppListViewModel<T : AppRecord>(
application: Application,
) : AppListViewModelImpl<T>(application)
@OptIn(ExperimentalCoroutinesApi::class)
internal open class AppListViewModelImpl<T : AppRecord>(
application: Application,
appListRepositoryFactory: (Context) -> AppListRepository = ::AppListRepositoryImpl,
appRepositoryFactory: (Context) -> AppRepository = ::AppRepositoryImpl,
) : AndroidViewModel(application), IAppListViewModel<T> {
val appListConfig = StateFlowBridge<AppListConfig>()
val listModel = StateFlowBridge<AppListModel<T>>()
val showSystem = StateFlowBridge<Boolean>()
final override val optionFlow = MutableStateFlow<Int?>(null)
val searchQuery = StateFlowBridge<String>()
private val appListRepository = appListRepositoryFactory(application)
private val appRepository = appRepositoryFactory(application)
private val collator = Collator.getInstance().freeze()
private val labelMap = ConcurrentHashMap<String, String>()
private val scope = viewModelScope + Dispatchers.IO
private val userSubGraphsFlow = appListConfig.flow.map { config ->
config.userIds.map { userId ->
UserSubGraph(userId, config.showInstantApps, config.matchAnyUserForAdmin)
}
}.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
private inner class UserSubGraph(
private val userId: Int,
private val showInstantApps: Boolean,
private val matchAnyUserForAdmin: Boolean,
) {
private val userIdFlow = flowOf(userId)
private val appsStateFlow = MutableStateFlow<List<ApplicationInfo>?>(null)
val recordListFlow = listModel.flow
.flatMapLatest { it.transform(userIdFlow, appsStateFlow.filterNotNull()) }
.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
private val systemFilteredFlow =
appListRepository.showSystemPredicate(userIdFlow, showSystem.flow)
.combine(recordListFlow) { showAppPredicate, recordList ->
recordList.filter { showAppPredicate(it.app) }
}
.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
val listModelFilteredFlow = optionFlow.filterNotNull().flatMapLatest { option ->
listModel.flow.flatMapLatest { listModel ->
listModel.filter(this.userIdFlow, option, this.systemFilteredFlow)
}
}.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
fun reloadApps() {
scope.launch {
appsStateFlow.value =
appListRepository.loadApps(userId, showInstantApps, matchAnyUserForAdmin)
}
}
}
private val combinedRecordListFlow = userSubGraphsFlow.flatMapLatest { userSubGraphList ->
combine(userSubGraphList.map { it.recordListFlow }) { it.toList().flatten() }
}.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
override val spinnerOptionsFlow =
combinedRecordListFlow.combine(listModel.flow) { recordList, listModel ->
listModel.getSpinnerOptions(recordList)
}.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
private val appEntryListFlow = userSubGraphsFlow.flatMapLatest { userSubGraphList ->
combine(userSubGraphList.map { it.listModelFilteredFlow }) { it.toList().flatten() }
}.asyncMapItem { record ->
val label = getLabel(record.app)
AppEntry(
record = record,
label = label,
labelCollationKey = collator.getCollationKey(label),
)
}
override val appListDataFlow =
combine(
appEntryListFlow,
listModel.flow,
optionFlow.filterNotNull(),
) { appEntries, listModel, option ->
AppListData(
appEntries = appEntries.sortedWith(listModel.getComparator(option)),
option = option,
)
}.combine(searchQuery.flow) { appListData, searchQuery ->
appListData.filter {
it.label.contains(other = searchQuery, ignoreCase = true)
}
}.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
init {
scheduleOnFirstLoaded()
}
fun reloadApps() {
scope.launch {
userSubGraphsFlow.collect { userSubGraphList ->
for (userSubGraph in userSubGraphList) {
userSubGraph.reloadApps()
}
}
}
}
private fun scheduleOnFirstLoaded() {
combinedRecordListFlow
.waitFirst(appListDataFlow)
.combine(listModel.flow) { recordList, listModel ->
if (listModel.onFirstLoaded(recordList)) {
preFetchLabels(recordList)
}
}
.launchIn(scope)
}
private fun preFetchLabels(recordList: List<T>) {
for (record in recordList) {
getLabel(record.app)
}
}
private fun getLabel(app: ApplicationInfo) = labelMap.computeIfAbsent(app.packageName) {
appRepository.loadLabel(app)
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2024 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.AppOpsManager
import android.app.AppOpsManager.MODE_ALLOWED
import android.app.AppOpsManager.MODE_ERRORED
import android.app.AppOpsManager.Mode
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.UserHandle
import com.android.settingslib.spaprivileged.framework.common.appOpsManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
interface IAppOpsController {
val mode: Flow<Int>
val isAllowed: Flow<Boolean>
get() = mode.map { it == MODE_ALLOWED }
fun setAllowed(allowed: Boolean)
@Mode fun getMode(): Int
}
class AppOpsController(
context: Context,
private val app: ApplicationInfo,
private val op: Int,
private val modeForNotAllowed: Int = MODE_ERRORED,
private val setModeByUid: Boolean = false,
) : IAppOpsController {
private val appOpsManager = context.appOpsManager
private val packageManager = context.packageManager
override val mode = appOpsManager.opModeFlow(op, app)
override fun setAllowed(allowed: Boolean) {
val mode = if (allowed) MODE_ALLOWED else modeForNotAllowed
if (setModeByUid) {
appOpsManager.setUidMode(op, app.uid, mode)
} else {
appOpsManager.setMode(op, app.uid, app.packageName, mode)
}
val permission = AppOpsManager.opToPermission(op)
if (permission != null) {
packageManager.updatePermissionFlags(permission, app.packageName,
PackageManager.FLAG_PERMISSION_USER_SET,
PackageManager.FLAG_PERMISSION_USER_SET,
UserHandle.getUserHandleForUid(app.uid))
}
}
@Mode override fun getMode(): Int = appOpsManager.getOpMode(op, app)
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2024 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.AppOpsManager
import android.content.pm.ApplicationInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
fun AppOpsManager.getOpMode(op: Int, app: ApplicationInfo) =
checkOpNoThrow(op, app.uid, app.packageName)
fun AppOpsManager.opModeFlow(op: Int, app: ApplicationInfo) =
opChangedFlow(op, app).map { getOpMode(op, app) }.flowOn(Dispatchers.Default)
private fun AppOpsManager.opChangedFlow(op: Int, app: ApplicationInfo) = callbackFlow {
val listener = object : AppOpsManager.OnOpChangedListener {
override fun onOpChanged(op: String, packageName: String) {}
override fun onOpChanged(op: String, packageName: String, userId: Int) {
if (userId == app.userId) trySend(Unit)
}
}
startWatchingMode(op, app.packageName, listener)
trySend(Unit)
awaitClose { stopWatchingMode(listener) }
}.conflate().flowOn(Dispatchers.Default)

View File

@@ -0,0 +1,23 @@
/*
* 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.pm.ApplicationInfo
interface AppRecord {
val app: ApplicationInfo
}

View File

@@ -0,0 +1,94 @@
/*
* 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.pm.ApplicationInfo
import android.graphics.drawable.Drawable
import android.util.IconDrawableFactory
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import androidx.compose.ui.platform.LocalContext
import com.android.settingslib.spa.framework.compose.rememberContext
import com.android.settingslib.spaprivileged.R
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.framework.compose.placeholder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun rememberAppRepository(): AppRepository = rememberContext(::AppRepositoryImpl)
interface AppRepository {
fun loadLabel(app: ApplicationInfo): String
@Suppress("ABSTRACT_COMPOSABLE_DEFAULT_PARAMETER_VALUE")
@Composable
fun produceLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false): State<String> {
val context = LocalContext.current
return produceState(initialValue = placeholder(), app) {
withContext(Dispatchers.IO) {
value = if (isClonedAppPage || isCloneApp(context, app)) {
context.getString(R.string.cloned_app_info_label, loadLabel(app))
} else {
loadLabel(app)
}
}
}
}
private fun isCloneApp(context: Context, app: ApplicationInfo): Boolean {
val userInfo = context.userManager.getUserInfo(app.userId)
return userInfo != null && userInfo.isCloneProfile
}
@Composable
fun produceIcon(app: ApplicationInfo): State<Drawable?>
@Composable
fun produceIconContentDescription(app: ApplicationInfo): State<String?>
}
internal class AppRepositoryImpl(private val context: Context) : AppRepository {
private val packageManager = context.packageManager
private val iconDrawableFactory = IconDrawableFactory.newInstance(context)
override fun loadLabel(app: ApplicationInfo): String = app.loadLabel(packageManager).toString()
@Composable
override fun produceIcon(app: ApplicationInfo) =
produceState<Drawable?>(initialValue = null, app) {
withContext(Dispatchers.IO) {
value = iconDrawableFactory.getBadgedIcon(app)
}
}
@Composable
override fun produceIconContentDescription(app: ApplicationInfo) =
produceState<String?>(initialValue = null, app) {
withContext(Dispatchers.IO) {
value = when {
context.userManager.isManagedProfile(app.userId) -> {
context.getString(com.android.settingslib.R.string.category_work)
}
else -> null
}
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.UserHandle
import android.os.UserManager
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
import com.android.settingslib.spaprivileged.framework.common.userManager
/** The user id for a given application. */
val ApplicationInfo.userId: Int
get() = UserHandle.getUserId(uid)
/** The [UserHandle] for a given application. */
val ApplicationInfo.userHandle: UserHandle
get() = UserHandle.getUserHandleForUid(uid)
/** Checks whether a flag is associated with the application. */
fun ApplicationInfo.hasFlag(flag: Int): Boolean = (flags and flag) > 0
/** Checks whether the application is currently installed. */
val ApplicationInfo.installed: Boolean get() = hasFlag(ApplicationInfo.FLAG_INSTALLED)
/** Checks whether the application is disabled until used. */
val ApplicationInfo.isDisabledUntilUsed: Boolean
get() = enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
/** Checks whether the application is disallowed control. */
fun ApplicationInfo.isDisallowControl(context: Context) =
context.userManager.hasBaseUserRestriction(UserManager.DISALLOW_APPS_CONTROL, userHandle)
/** Checks whether the application is an active admin. */
fun ApplicationInfo.isActiveAdmin(context: Context): Boolean =
context.devicePolicyManager.packageHasActiveAdmins(packageName, userId)
/** Converts to the route string which used in navigation. */
fun ApplicationInfo.toRoute() = "$packageName/$userId"

View File

@@ -0,0 +1,49 @@
/*
* 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.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
/**
* Checks if a package is system module.
*/
fun PackageManager.isSystemModule(packageName: String): Boolean = try {
getModuleInfo(packageName, 0)
true
} catch (_: PackageManager.NameNotFoundException) {
// Expected, not system module
false
}
/**
* Resolves the activity to start for a given application and action.
*/
fun PackageManager.resolveActionForApp(
app: ApplicationInfo,
action: String,
flags: Int = 0,
): ActivityInfo? {
val intent = Intent(action).apply {
`package` = app.packageName
}
return resolveActivityAsUser(intent, ResolveInfoFlags.of(flags.toLong()), app.userId)
?.activityInfo
}

View File

@@ -0,0 +1,100 @@
/*
* 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.AppGlobals
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED
import android.content.pm.PackageManager
import com.android.settingslib.spa.framework.util.asyncFilter
interface IPackageManagers {
fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo?
fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo?
/** Checks whether a package is installed for a given user. */
fun isPackageInstalledAsUser(packageName: String, userId: Int): Boolean
fun ApplicationInfo.hasRequestPermission(permission: String): Boolean
/** Checks whether a permission is currently granted to the application. */
fun ApplicationInfo.hasGrantPermission(permission: String): Boolean
suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String>
fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo?
}
object PackageManagers : IPackageManagers by PackageManagersImpl(PackageManagerWrapperImpl)
internal interface PackageManagerWrapper {
fun getPackageInfoAsUserCached(
packageName: String,
flags: Long,
userId: Int,
): PackageInfo?
}
internal object PackageManagerWrapperImpl : PackageManagerWrapper {
override fun getPackageInfoAsUserCached(
packageName: String,
flags: Long,
userId: Int,
): PackageInfo? = PackageManager.getPackageInfoAsUserCached(packageName, flags, userId)
}
internal class PackageManagersImpl(
private val packageManagerWrapper: PackageManagerWrapper,
) : IPackageManagers {
private val iPackageManager by lazy { AppGlobals.getPackageManager() }
override fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo? =
getPackageInfoAsUser(packageName, 0, userId)
override fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo? =
PackageManager.getApplicationInfoAsUserCached(packageName, 0, userId)
override fun isPackageInstalledAsUser(packageName: String, userId: Int): Boolean =
getApplicationInfoAsUser(packageName, userId)?.hasFlag(ApplicationInfo.FLAG_INSTALLED)
?: false
override fun ApplicationInfo.hasRequestPermission(permission: String): Boolean {
val packageInfo =
getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
return packageInfo?.requestedPermissions?.let {
permission in it
} ?: false
}
override fun ApplicationInfo.hasGrantPermission(permission: String): Boolean {
val packageInfo =
getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
val index = packageInfo?.requestedPermissions?.indexOf(permission) ?: return false
return index >= 0 &&
checkNotNull(packageInfo.requestedPermissionsFlags)[index]
.hasFlag(REQUESTED_PERMISSION_GRANTED)
}
override suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String> =
iPackageManager.getAppOpPermissionPackages(permission, userId).asIterable().asyncFilter {
iPackageManager.isPackageAvailable(it, userId)
}.toSet()
override fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo? =
packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags, userId)
private fun Int.hasFlag(flag: Int) = (this and flag) > 0
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2024 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.pm.ApplicationInfo
import android.content.pm.PackageManager
import com.android.settingslib.spaprivileged.framework.common.asUser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
/**
* Creates an instance of a cold Flow for permissions changed callback of given [app].
*
* An initial element will be always sent.
*/
fun Context.permissionsChangedFlow(app: ApplicationInfo) = callbackFlow {
val userPackageManager = asUser(app.userHandle).packageManager
val onPermissionsChangedListener = PackageManager.OnPermissionsChangedListener { uid ->
if (uid == app.uid) trySend(Unit)
}
userPackageManager.addOnPermissionsChangeListener(onPermissionsChangedListener)
trySend(Unit)
awaitClose {
userPackageManager.removeOnPermissionsChangeListener(onPermissionsChangedListener)
}
}.conflate().flowOn(Dispatchers.Default)

View File

@@ -0,0 +1,44 @@
/*
* 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.enterprise
import android.app.admin.DevicePolicyResources.Strings.Settings.PERSONAL_CATEGORY_HEADER
import android.app.admin.DevicePolicyResources.Strings.Settings.PRIVATE_CATEGORY_HEADER
import android.app.admin.DevicePolicyResources.Strings.Settings.WORK_CATEGORY_HEADER
import android.content.Context
import android.content.pm.UserInfo
import com.android.settingslib.R
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
interface IEnterpriseRepository {
fun getEnterpriseString(updatableStringId: String, resId: Int): String
}
class EnterpriseRepository(private val context: Context) : IEnterpriseRepository {
private val resources by lazy { context.devicePolicyManager.resources }
override fun getEnterpriseString(updatableStringId: String, resId: Int): String =
checkNotNull(resources.getString(updatableStringId) { context.getString(resId) })
fun getProfileTitle(userInfo: UserInfo): String = if (userInfo.isManagedProfile) {
getEnterpriseString(WORK_CATEGORY_HEADER, R.string.category_work)
} else if (userInfo.isPrivateProfile) {
getEnterpriseString(PRIVATE_CATEGORY_HEADER, R.string.category_private)
} else {
getEnterpriseString(PERSONAL_CATEGORY_HEADER, R.string.category_personal)
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2023 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.enterprise
import android.app.admin.DevicePolicyResources.Strings.Settings
import android.content.Context
import android.content.Intent
import com.android.settingslib.RestrictedLockUtils
import com.android.settingslib.widget.restricted.R
sealed interface RestrictedMode
data object NoRestricted : RestrictedMode
data object BaseUserRestricted : RestrictedMode
interface BlockedByAdmin : RestrictedMode {
fun getSummary(checked: Boolean?): String
fun sendShowAdminSupportDetailsIntent()
}
interface BlockedByEcm : RestrictedMode {
fun showRestrictedSettingsDetails()
}
internal data class BlockedByAdminImpl(
private val context: Context,
private val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin,
private val enterpriseRepository: IEnterpriseRepository = EnterpriseRepository(context),
) : BlockedByAdmin {
override fun getSummary(checked: Boolean?) = when (checked) {
true -> enterpriseRepository.getEnterpriseString(
updatableStringId = Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY,
resId = R.string.enabled_by_admin,
)
false -> enterpriseRepository.getEnterpriseString(
updatableStringId = Settings.DISABLED_BY_ADMIN_SWITCH_SUMMARY,
resId = R.string.disabled_by_admin,
)
else -> ""
}
override fun sendShowAdminSupportDetailsIntent() {
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, enforcedAdmin)
}
}
internal data class BlockedByEcmImpl(
private val context: Context,
private val intent: Intent,
) : BlockedByEcm {
override fun showRestrictedSettingsDetails() {
context.startActivity(intent)
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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.enterprise
import android.content.Context
import android.os.UserHandle
import android.os.UserManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.RestrictedLockUtilsInternal
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
data class EnhancedConfirmation(
val key: String,
val packageName: String,
)
data class Restrictions(
val userId: Int = UserHandle.myUserId(),
val keys: List<String>,
val enhancedConfirmation: EnhancedConfirmation? = null,
) {
fun isEmpty(): Boolean {
return keys.isEmpty() && enhancedConfirmation == null
}
}
interface RestrictionsProvider {
@Composable
fun restrictedModeState(): State<RestrictedMode?>
}
typealias RestrictionsProviderFactory = (Context, Restrictions) -> RestrictionsProvider
@Composable
internal fun RestrictionsProviderFactory.rememberRestrictedMode(
restrictions: Restrictions,
): State<RestrictedMode?> {
val context = LocalContext.current
val restrictionsProvider = remember(restrictions) {
this(context, restrictions)
}
return restrictionsProvider.restrictedModeState()
}
internal class RestrictionsProviderImpl(
private val context: Context,
private val restrictions: Restrictions,
) : RestrictionsProvider {
private val userManager by lazy { UserManager.get(context) }
private val restrictedMode = flow {
emit(getRestrictedMode())
}.flowOn(Dispatchers.IO)
@Composable
override fun restrictedModeState() =
restrictedMode.collectAsStateWithLifecycle(initialValue = null)
private fun getRestrictedMode(): RestrictedMode {
for (key in restrictions.keys) {
if (userManager.hasBaseUserRestriction(key, UserHandle.of(restrictions.userId))) {
return BaseUserRestricted
}
}
for (key in restrictions.keys) {
RestrictedLockUtilsInternal
.checkIfRestrictionEnforced(context, key, restrictions.userId)
?.let { return BlockedByAdminImpl(context = context, enforcedAdmin = it) }
}
restrictions.enhancedConfirmation?.let { ec ->
RestrictedLockUtilsInternal
.checkIfRequiresEnhancedConfirmation(context, ec.key,
ec.packageName)
?.let { intent -> return BlockedByEcmImpl(context = context, intent = intent) }
}
return NoRestricted
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 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.settingsprovider
import android.content.ContentResolver
import android.content.Context
import android.provider.Settings
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
fun Context.settingsGlobalBoolean(name: String, defaultValue: Boolean = false):
ReadWriteProperty<Any?, Boolean> = SettingsGlobalBooleanDelegate(this, name, defaultValue)
fun Context.settingsGlobalBooleanFlow(name: String, defaultValue: Boolean = false): Flow<Boolean> {
val value by settingsGlobalBoolean(name, defaultValue)
return settingsGlobalChangeFlow(name).map { value }.distinctUntilChanged()
}
private class SettingsGlobalBooleanDelegate(
context: Context,
private val name: String,
private val defaultValue: Boolean = false,
) : ReadWriteProperty<Any?, Boolean> {
private val contentResolver: ContentResolver = context.contentResolver
override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean =
Settings.Global.getInt(contentResolver, name, if (defaultValue) 1 else 0) != 0
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
Settings.Global.putInt(contentResolver, name, if (value) 1 else 0)
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 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.settingsprovider
import android.content.Context
import android.provider.Settings
import com.android.settingslib.spaprivileged.database.contentChangeFlow
import kotlinx.coroutines.flow.Flow
fun Context.settingsGlobalChangeFlow(name: String, sendInitialValue: Boolean = true): Flow<Unit> =
contentChangeFlow(Settings.Global.getUriFor(name), sendInitialValue)

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2023 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.settingsprovider
import android.content.ContentResolver
import android.content.Context
import android.provider.Settings
import com.android.settingslib.spaprivileged.database.contentChangeFlow
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
fun Context.settingsSecureBoolean(name: String, defaultValue: Boolean = false):
ReadWriteProperty<Any?, Boolean> = SettingsSecureBooleanDelegate(this, name, defaultValue)
fun Context.settingsSecureBooleanFlow(name: String, defaultValue: Boolean = false): Flow<Boolean> {
val value by settingsSecureBoolean(name, defaultValue)
return contentChangeFlow(Settings.Secure.getUriFor(name)).map { value }.distinctUntilChanged()
}
private class SettingsSecureBooleanDelegate(
context: Context,
private val name: String,
private val defaultValue: Boolean = false,
) : ReadWriteProperty<Any?, Boolean> {
private val contentResolver: ContentResolver = context.contentResolver
override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean =
Settings.Secure.getInt(contentResolver, name, if (defaultValue) 1 else 0) != 0
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
Settings.Secure.putInt(contentResolver, name, if (value) 1 else 0)
}
}

View File

@@ -0,0 +1,138 @@
/*
* 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.ApplicationInfo
import android.content.pm.PackageInfo
import android.text.BidiFormatter
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import com.android.settingslib.development.DevelopmentSettingsEnabler
import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.ui.CopyableBody
import com.android.settingslib.spa.widget.ui.SettingsBody
import com.android.settingslib.spa.widget.ui.SettingsTitle
import com.android.settingslib.spaprivileged.R
import com.android.settingslib.spaprivileged.model.app.rememberAppRepository
class AppInfoProvider(private val packageInfo: PackageInfo) {
@Composable
fun AppInfo(displayVersion: Boolean = false, isClonedAppPage: Boolean = false) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = SettingsDimension.itemPaddingStart,
vertical = SettingsDimension.itemPaddingVertical,
)
.semantics(mergeDescendants = true) {},
horizontalAlignment = Alignment.CenterHorizontally,
) {
val app = checkNotNull(packageInfo.applicationInfo)
Box(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) {
AppIcon(app = app, size = SettingsDimension.appIconInfoSize)
}
AppLabel(app, isClonedAppPage)
InstallType(app)
if (displayVersion) AppVersion()
}
}
@Composable
private fun InstallType(app: ApplicationInfo) {
if (!app.isInstantApp) return
Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
SettingsBody(
stringResource(
com.android.settingslib.widget.preference.app.R.string.install_type_instant
)
)
}
@Composable
private fun AppVersion() {
val versionName = packageInfo.versionNameBidiWrapped ?: return
Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
SettingsBody(versionName)
}
@Composable
fun FooterAppVersion(showPackageName: Boolean = rememberIsDevelopmentSettingsEnabled()) {
val context = LocalContext.current
val footer = remember(packageInfo, showPackageName) {
val list = mutableListOf<String>()
packageInfo.versionNameBidiWrapped?.let {
list += context.getString(R.string.version_text, it)
}
if (showPackageName) {
list += packageInfo.packageName
}
list.joinToString(separator = System.lineSeparator())
}
if (footer.isBlank()) return
HorizontalDivider()
Column(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
CopyableBody(footer)
}
}
@Composable
private fun rememberIsDevelopmentSettingsEnabled(): Boolean {
val context = LocalContext.current
return remember {
DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context)
}
}
private companion object {
/** Wrapped the version name, so its directionality still keep same when RTL. */
val PackageInfo.versionNameBidiWrapped: String?
get() = BidiFormatter.getInstance().unicodeWrap(versionName)
}
}
@Composable
internal fun AppIcon(app: ApplicationInfo, size: Dp) {
val appRepository = rememberAppRepository()
Image(
painter = rememberDrawablePainter(appRepository.produceIcon(app).value),
contentDescription = appRepository.produceIconContentDescription(app).value,
modifier = Modifier.size(size),
)
}
@Composable
internal fun AppLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false) {
val appRepository = rememberAppRepository()
SettingsTitle(appRepository.produceLabel(app, isClonedAppPage).value, useMediumWeight = true)
}

View File

@@ -0,0 +1,45 @@
/*
* 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.PackageInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Footer
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
@Composable
fun AppInfoPage(
title: String,
packageName: String,
userId: Int,
footerContent: @Composable () -> Unit,
packageManagers: IPackageManagers,
content: @Composable PackageInfo.() -> Unit,
) {
val packageInfo = remember(packageName, userId) {
packageManagers.getPackageInfoAsUser(packageName, userId)
} ?: return
RegularScaffold(title = title) {
remember(packageInfo) { AppInfoProvider(packageInfo) }.AppInfo(displayVersion = true)
packageInfo.content()
Footer(footerContent)
}
}

View File

@@ -0,0 +1,198 @@
/*
* 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.Intent
import android.content.IntentFilter
import android.os.UserHandle
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.compose.ui.unit.Dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.settingslib.spa.framework.compose.LifecycleEffect
import com.android.settingslib.spa.framework.compose.LogCompositions
import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer
import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll
import com.android.settingslib.spa.widget.ui.CategoryTitle
import com.android.settingslib.spa.widget.ui.PlaceholderTitle
import com.android.settingslib.spa.widget.ui.Spinner
import com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.R
import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
import com.android.settingslib.spaprivileged.model.app.AppEntry
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 com.android.settingslib.spaprivileged.model.app.IAppListViewModel
import com.android.settingslib.spaprivileged.model.app.userId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
private const val TAG = "AppList"
private const val CONTENT_TYPE_HEADER = "header"
/**
* The config used to load the App List.
*/
data class AppListConfig(
val userIds: List<Int>,
val showInstantApps: Boolean,
val matchAnyUserForAdmin: Boolean,
)
data class AppListState(
val showSystem: () -> Boolean,
val searchQuery: () -> String,
)
data class AppListInput<T : AppRecord>(
val config: AppListConfig,
val listModel: AppListModel<T>,
val state: AppListState,
val header: @Composable () -> Unit,
val noItemMessage: String? = null,
val bottomPadding: Dp,
)
/**
* The template to render an App List.
*
* This UI element will take the remaining space on the screen to show the App List.
*/
@Composable
fun <T : AppRecord> AppListInput<T>.AppList() {
AppListImpl { rememberViewModel(config, listModel, state) }
}
@Composable
internal fun <T : AppRecord> AppListInput<T>.AppListImpl(
viewModelSupplier: @Composable () -> IAppListViewModel<T>,
) {
LogCompositions(TAG, config.userIds.toString())
val viewModel = viewModelSupplier()
Column(Modifier.fillMaxSize()) {
val optionsState = viewModel.spinnerOptionsFlow.collectAsState(null, Dispatchers.IO)
SpinnerOptions(optionsState, viewModel.optionFlow)
val appListData = viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO)
listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage)
}
}
@Composable
private fun SpinnerOptions(
optionsState: State<List<SpinnerOption>?>,
optionFlow: MutableStateFlow<Int?>,
) {
val options = optionsState.value
LaunchedEffect(options) {
if (options != null && !options.any { it.id == optionFlow.value }) {
// Reset to first option if the available options changed, and the current selected one
// does not in the new options.
optionFlow.value = options.let { it.firstOrNull()?.id ?: -1 }
}
}
if (options != null) {
Spinner(options, optionFlow.collectAsState().value) { optionFlow.value = it }
}
}
@Composable
private fun <T : AppRecord> AppListModel<T>.AppListWidget(
appListData: State<AppListData<T>?>,
header: @Composable () -> Unit,
bottomPadding: Dp,
noItemMessage: String?
) {
val timeMeasurer = rememberTimeMeasurer(TAG)
appListData.value?.let { (list, option) ->
timeMeasurer.logFirst("app list first loaded")
if (list.isEmpty()) {
header()
PlaceholderTitle(noItemMessage ?: stringResource(R.string.no_applications))
return
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
contentPadding = PaddingValues(bottom = bottomPadding),
) {
item(contentType = CONTENT_TYPE_HEADER) {
header()
}
items(count = list.size, key = { list[it].record.itemKey(option) }) {
remember(list) { getGroupTitleIfFirst(option, list, it) }
?.let { group -> CategoryTitle(title = group) }
val appEntry = list[it]
val summary = getSummary(option, appEntry.record) ?: { "" }
remember(appEntry) {
AppListItemModel(appEntry.record, appEntry.label, summary)
}.AppItem()
}
}
}
}
private fun <T : AppRecord> T.itemKey(option: Int) =
listOf(option, app.packageName, app.userId)
/** Returns group title if this is the first item of the group. */
private fun <T : AppRecord> AppListModel<T>.getGroupTitleIfFirst(
option: Int,
list: List<AppEntry<T>>,
index: Int,
): String? = getGroupTitle(option, list[index].record)?.takeIf {
index == 0 || it != getGroupTitle(option, list[index - 1].record)
}
@Composable
private fun <T : AppRecord> rememberViewModel(
config: AppListConfig,
listModel: AppListModel<T>,
state: AppListState,
): AppListViewModel<T> {
val viewModel: AppListViewModel<T> = viewModel(key = config.userIds.toString())
viewModel.appListConfig.setIfAbsent(config)
viewModel.listModel.setIfAbsent(listModel)
viewModel.showSystem.Sync(state.showSystem)
viewModel.searchQuery.Sync(state.searchQuery)
LifecycleEffect(onStart = { viewModel.reloadApps() })
val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package")
}
for (userId in config.userIds) {
DisposableBroadcastReceiverAsUser(
intentFilter = intentFilter,
userHandle = UserHandle.of(userId),
) { viewModel.reloadApps() }
}
return viewModel
}

View File

@@ -0,0 +1,41 @@
/*
* 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.ui.graphics.vector.ImageVector
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spa.widget.preference.TwoTargetButtonPreference
@Composable
fun <T : AppRecord> AppListItemModel<T>.AppListButtonItem (
onClick: () -> Unit,
onButtonClick: () -> Unit,
buttonIcon: ImageVector,
buttonIconDescription: String,
) {
TwoTargetButtonPreference(
title = label,
summary = this@AppListButtonItem.summary,
icon = { AppIcon(record.app, SettingsDimension.appIconItemSize) },
onClick = onClick,
buttonIcon = buttonIcon,
buttonIconDescription = buttonIconDescription,
onButtonClick = onButtonClick
)
}

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.spaprivileged.template.app
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
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
data class AppListItemModel<T : AppRecord>(
val record: T,
val label: String,
val summary: () -> String,
)
@Composable
fun <T : AppRecord> AppListItemModel<T>.AppListItem(onClick: () -> Unit) {
Preference(remember {
object : PreferenceModel {
override val title = label
override val summary = this@AppListItem.summary
override val icon = @Composable {
AppIcon(app = 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
}
AppListItemModel<AppRecord>(record, "Chrome", { "Allowed" }).AppListItem {}
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction
import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope
import com.android.settingslib.spa.widget.scaffold.SearchScaffold
import com.android.settingslib.spaprivileged.R
import com.android.settingslib.spaprivileged.model.app.AppListModel
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.template.common.UserProfilePager
/**
* The full screen template for an App List page.
*
* @param noMoreOptions default false. If true, then do not display more options action button,
* including the "Show System" / "Hide System" action.
* @param header the description header appears before all the applications.
*/
@Composable
fun <T : AppRecord> AppListPage(
title: String,
listModel: AppListModel<T>,
showInstantApps: Boolean = false,
noMoreOptions: Boolean = false,
matchAnyUserForAdmin: Boolean = false,
noItemMessage: String? = null,
moreOptions: @Composable MoreOptionsScope.() -> Unit = {},
header: @Composable () -> Unit = {},
appList: @Composable AppListInput<T>.() -> Unit = { AppList() },
) {
var showSystem by rememberSaveable { mutableStateOf(false) }
SearchScaffold(
title = title,
actions = {
if (!noMoreOptions) {
MoreOptionsAction {
ShowSystemAction(showSystem) { showSystem = it }
moreOptions()
}
}
},
) { bottomPadding, searchQuery ->
UserProfilePager { userGroup ->
val appListInput = AppListInput(
config = AppListConfig(
userIds = userGroup.userInfos.map { it.id },
showInstantApps = showInstantApps,
matchAnyUserForAdmin = matchAnyUserForAdmin,
),
listModel = listModel,
state = AppListState(
showSystem = { showSystem },
searchQuery = searchQuery,
),
header = header,
bottomPadding = bottomPadding,
noItemMessage = noItemMessage,
)
appList(appListInput)
}
}
}
@Composable
private fun MoreOptionsScope.ShowSystemAction(
showSystem: Boolean,
setShowSystem: (showSystem: Boolean) -> Unit,
) {
val menuText = if (showSystem) R.string.menu_hide_system else R.string.menu_show_system
MenuItem(text = stringResource(menuText)) {
setShowSystem(!showSystem)
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 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 com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.model.app.AppRecord
@Composable
fun <T : AppRecord> AppListItemModel<T>.AppListSwitchItem(
checked: () -> Boolean?,
changeable: () -> Boolean,
onCheckedChange: ((newChecked: Boolean) -> Unit)?,
) {
SwitchPreference(
model = object : SwitchPreferenceModel {
override val title = label
override val summary = this@AppListSwitchItem.summary
override val icon = @Composable {
AppIcon(record.app, SettingsDimension.appIconItemSize)
}
override val checked = checked
override val changeable = changeable
override val onCheckedChange = onCheckedChange
},
)
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 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 com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
import com.android.settingslib.spaprivileged.model.app.AppRecord
@Composable
fun <T : AppRecord> AppListItemModel<T>.AppListTwoTargetSwitchItem(
onClick: () -> Unit,
checked: () -> Boolean?,
changeable: () -> Boolean,
onCheckedChange: ((newChecked: Boolean) -> Unit)?,
) {
TwoTargetSwitchPreference(
model = object : SwitchPreferenceModel {
override val title = label
override val summary = this@AppListTwoTargetSwitchItem.summary
override val checked = checked
override val changeable = changeable
override val onCheckedChange = onCheckedChange
},
icon = { AppIcon(record.app, SettingsDimension.appIconItemSize) },
onClick = onClick,
)
}

View File

@@ -0,0 +1,181 @@
/*
* 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.app.AppOpsManager
import android.content.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.spa.framework.util.asyncMapItem
import com.android.settingslib.spa.framework.util.filterItem
import com.android.settingslib.spaprivileged.model.app.AppOpsController
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.IAppOpsController
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.model.app.PackageManagers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
data class AppOpPermissionRecord(
override val app: ApplicationInfo,
val hasRequestBroaderPermission: Boolean,
val hasRequestPermission: Boolean,
var appOpsController: IAppOpsController,
) : AppRecord
abstract class AppOpPermissionListModel(
protected val context: Context,
private val packageManagers: IPackageManagers = PackageManagers,
) : TogglePermissionAppListModel<AppOpPermissionRecord> {
abstract val appOp: Int
abstract val permission: String
override val enhancedConfirmationKey: String?
get() = AppOpsManager.opToPublicName(appOp)
/**
* When set, specifies the broader permission who trumps the [permission].
*
* When trumped, the [permission] is not changeable and model shows the [permission] as allowed.
*/
open val broaderPermission: String? = null
/**
* Indicates whether [permission] has protection level appop flag.
*
* If true, it uses getAppOpPermissionPackages() to fetch bits to decide whether the permission
* is requested.
*/
open val permissionHasAppOpFlag: Boolean = true
open val modeForNotAllowed: Int = AppOpsManager.MODE_ERRORED
/**
* Use AppOpsManager#setUidMode() instead of AppOpsManager#setMode() when set allowed.
*
* Security or privacy related app-ops should be set with setUidMode() instead of setMode().
*/
open val setModeByUid = false
/** These not changeable packages will also be hidden from app list. */
private val notChangeablePackages =
setOf("android", "com.android.systemui", context.packageName)
private fun createAppOpsController(app: ApplicationInfo) =
AppOpsController(
context = context,
app = app,
op = appOp,
setModeByUid = setModeByUid,
modeForNotAllowed = modeForNotAllowed,
)
private fun createRecord(
app: ApplicationInfo,
hasRequestPermission: Boolean
): AppOpPermissionRecord =
with(packageManagers) {
AppOpPermissionRecord(
app = app,
hasRequestBroaderPermission = broaderPermission?.let {
app.hasRequestPermission(it)
} ?: false,
hasRequestPermission = hasRequestPermission,
appOpsController = createAppOpsController(app),
)
}
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
if (permissionHasAppOpFlag) {
userIdFlow
.map { userId -> packageManagers.getAppOpPermissionPackages(userId, permission) }
.combine(appListFlow) { packageNames, appList ->
appList.map { app ->
createRecord(
app = app,
hasRequestPermission = app.packageName in packageNames,
)
}
}
} else {
appListFlow.asyncMapItem { app ->
with(packageManagers) { createRecord(app, app.hasRequestPermission(permission)) }
}
}
override fun transformItem(app: ApplicationInfo) =
with(packageManagers) {
createRecord(
app = app,
hasRequestPermission = app.hasRequestPermission(permission),
)
}
override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) =
recordListFlow.filterItem(::isChangeable)
@Composable
override fun isAllowed(record: AppOpPermissionRecord): () -> Boolean? =
isAllowed(
record = record,
appOpsController = record.appOpsController,
permission = permission,
packageManagers = packageManagers,
)
override fun isChangeable(record: AppOpPermissionRecord) =
record.hasRequestPermission &&
!record.hasRequestBroaderPermission &&
record.app.packageName !in notChangeablePackages
override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
record.appOpsController.setAllowed(newAllowed)
}
}
/**
* Defining the default behavior as permissible as long as the package requested this permission
* (This means pre-M gets approval during install time; M apps gets approval during runtime).
*/
@Composable
internal fun isAllowed(
record: AppOpPermissionRecord,
appOpsController: IAppOpsController,
permission: String,
packageManagers: IPackageManagers = PackageManagers,
): () -> Boolean? {
if (record.hasRequestBroaderPermission) {
// Broader permission trumps the specific permission.
return { true }
}
val mode = appOpsController.mode.collectAsStateWithLifecycle(initialValue = null)
return {
when (mode.value) {
null -> null
AppOpsManager.MODE_ALLOWED -> true
AppOpsManager.MODE_DEFAULT -> {
with(packageManagers) { record.app.hasGrantPermission(permission) }
}
else -> false
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.Context
import android.content.pm.ApplicationInfo
import android.text.format.Formatter
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
import com.android.settingslib.spaprivileged.framework.compose.placeholder
import com.android.settingslib.spaprivileged.model.app.userHandle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
private const val TAG = "AppStorageSize"
@Composable
fun ApplicationInfo.getStorageSize(): State<String> {
val context = LocalContext.current
return remember(this) {
flow {
val sizeBytes = calculateSizeBytes(context)
this.emit(if (sizeBytes != null) Formatter.formatFileSize(context, sizeBytes) else "")
}.flowOn(Dispatchers.IO)
}.collectAsStateWithLifecycle(initialValue = placeholder())
}
fun ApplicationInfo.calculateSizeBytes(context: Context): Long? {
val storageStatsManager = context.storageStatsManager
return try {
val stats = storageStatsManager.queryStatsForPackage(storageUuid, packageName, userHandle)
stats.codeBytes + stats.dataBytes
} catch (e: Exception) {
Log.w(TAG, "Failed to query stats: $e")
null
}
}

View File

@@ -0,0 +1,183 @@
/*
* 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.ApplicationInfo
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spa.widget.ui.AnnotatedText
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.model.app.PackageManagers
import com.android.settingslib.spaprivileged.model.app.toRoute
import com.android.settingslib.spaprivileged.model.enterprise.EnhancedConfirmation
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
internal class TogglePermissionAppInfoPageProvider(
private val appListTemplate: TogglePermissionAppListTemplate,
) : SettingsPageProvider {
override val name = PAGE_NAME
override val parameter = PAGE_PARAMETER
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
val owner = SettingsPage.create(name, parameter = parameter, arguments = arguments)
return listOf(SettingsEntryBuilder.create("AllowControl", owner).build())
}
@Composable
override fun Page(arguments: Bundle?) {
val permissionType = arguments?.getString(PERMISSION)!!
val packageName = arguments.getString(PACKAGE_NAME)!!
val userId = arguments.getInt(USER_ID)
appListTemplate.rememberModel(permissionType)
.TogglePermissionAppInfoPage(packageName, userId)
}
companion object {
private const val PAGE_NAME = "TogglePermissionAppInfoPage"
private const val PERMISSION = "permission"
private const val PACKAGE_NAME = "rt_packageName"
private const val USER_ID = "rt_userId"
private val PAGE_PARAMETER = listOf(
navArgument(PERMISSION) { type = NavType.StringType },
navArgument(PACKAGE_NAME) { type = NavType.StringType },
navArgument(USER_ID) { type = NavType.IntType },
)
/**
* Gets the route prefix to this page.
*
* Expose route prefix to enable enter from non-SPA pages.
*/
fun getRoutePrefix(permissionType: String) = "$PAGE_NAME/$permissionType"
@Composable
fun navigator(permissionType: String, app: ApplicationInfo) =
navigator(route = "$PAGE_NAME/$permissionType/${app.toRoute()}")
fun buildPageData(permissionType: String): SettingsPage {
return SettingsPage.create(
name = PAGE_NAME,
parameter = PAGE_PARAMETER,
arguments = bundleOf(PERMISSION to permissionType)
)
}
}
}
@Composable
internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionAppInfoPageEntryItem(
permissionType: String,
app: ApplicationInfo,
) {
val record = remember { transformItem(app) }
if (!remember { isChangeable(record) }) return
val context = LocalContext.current
val internalListModel = remember {
TogglePermissionInternalAppListModel(
context = context,
permissionType = permissionType,
listModel = this,
restrictionsProviderFactory = ::RestrictionsProviderImpl,
)
}
Preference(
object : PreferenceModel {
override val title = stringResource(pageTitleResId)
override val summary = internalListModel.getSummary(record)
override val onClick =
TogglePermissionAppInfoPageProvider.navigator(permissionType, app)
}
)
}
@VisibleForTesting
@Composable
internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionAppInfoPage(
packageName: String,
userId: Int,
packageManagers: IPackageManagers = PackageManagers,
restrictionsProviderFactory: RestrictionsProviderFactory = ::RestrictionsProviderImpl,
) {
AppInfoPage(
title = stringResource(pageTitleResId),
packageName = packageName,
userId = userId,
footerContent = { AnnotatedText(footerResId) },
packageManagers = packageManagers,
) {
val app = applicationInfo ?: return@AppInfoPage
val record = rememberRecord(app).value ?: return@AppInfoPage
val isAllowed = isAllowed(record)
val isChangeable by rememberIsChangeable(record)
val switchModel = object : SwitchPreferenceModel {
override val title = stringResource(switchTitleResId)
override val checked = isAllowed
override val changeable = { isChangeable }
override val onCheckedChange: (Boolean) -> Unit = { setAllowed(record, it) }
}
val restrictions = Restrictions(userId = userId,
keys = switchRestrictionKeys,
enhancedConfirmation = enhancedConfirmationKey?.let { EnhancedConfirmation(
key = it,
packageName = packageName) })
RestrictedSwitchPreference(switchModel, restrictions, restrictionsProviderFactory)
InfoPageAdditionalContent(record, isAllowed)
}
}
@Composable
private fun <T : AppRecord> TogglePermissionAppListModel<T>.rememberRecord(app: ApplicationInfo) =
remember(app) {
flow {
emit(transformItem(app))
}.flowOn(Dispatchers.Default)
}.collectAsStateWithLifecycle(initialValue = null)
@Composable
private fun <T : AppRecord> TogglePermissionAppListModel<T>.rememberIsChangeable(record: T) =
remember(record) {
flow {
emit(isChangeable(record))
}.flowOn(Dispatchers.Default)
}.collectAsStateWithLifecycle(initialValue = false)

View File

@@ -0,0 +1,130 @@
/*
* 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.Context
import android.content.pm.ApplicationInfo
import androidx.compose.runtime.Composable
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.rememberContext
import com.android.settingslib.spa.framework.util.asyncMapItem
import com.android.settingslib.spaprivileged.model.app.AppRecord
import kotlinx.coroutines.flow.Flow
/**
* Implement this interface to build an App List which toggles a permission on / off.
*/
interface TogglePermissionAppListModel<T : AppRecord> {
val pageTitleResId: Int
val switchTitleResId: Int
val footerResId: Int
val switchRestrictionKeys: List<String>
get() = emptyList()
val enhancedConfirmationKey: String?
get() = null
/**
* Loads the extra info for the App List, and generates the [AppRecord] List.
*
* Default is implemented by [transformItem]
*/
fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>): Flow<List<T>> =
appListFlow.asyncMapItem(::transformItem)
/**
* Loads the extra info for one app, and generates the [AppRecord].
*
* This must be implemented, because when show the App Info page for single app, this will be
* used instead of [transform].
*/
fun transformItem(app: ApplicationInfo): T
/**
* Filters the [AppRecord] list.
*
* @return the [AppRecord] list which will be displayed.
*/
fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<T>>): Flow<List<T>>
/**
* Gets whether the permission is allowed for the given app.
*/
@Composable
fun isAllowed(record: T): () -> Boolean?
/**
* Gets whether the permission on / off is changeable for the given app.
*/
fun isChangeable(record: T): Boolean
/**
* Sets whether the permission is allowed for the given app.
*/
fun setAllowed(record: T, newAllowed: Boolean)
@Composable
fun InfoPageAdditionalContent(record: T, isAllowed: () -> Boolean?){}
}
interface TogglePermissionAppListProvider {
val permissionType: String
fun createModel(context: Context): TogglePermissionAppListModel<out AppRecord>
fun buildAppListInjectEntry(): SettingsEntryBuilder =
TogglePermissionAppListPageProvider.buildInjectEntry(permissionType) { createModel(it) }
/**
* Gets the route to the toggle permission App List page.
*
* Expose route to enable enter from non-SPA pages.
*/
fun getAppListRoute(): String =
TogglePermissionAppListPageProvider.getRoute(permissionType)
/**
* Gets the route prefix to the toggle permission App Info page.
*
* Expose route prefix to enable enter from non-SPA pages.
*/
fun getAppInfoRoutePrefix(): String =
TogglePermissionAppInfoPageProvider.getRoutePrefix(permissionType)
@Composable
fun InfoPageEntryItem(app: ApplicationInfo) {
val listModel = rememberContext(::createModel)
listModel.TogglePermissionAppInfoPageEntryItem(permissionType, app)
}
}
class TogglePermissionAppListTemplate(
allProviders: List<TogglePermissionAppListProvider>,
) {
private val listModelProviderMap = allProviders.associateBy { it.permissionType }
fun createPageProviders(): List<SettingsPageProvider> = listOf(
TogglePermissionAppListPageProvider(this),
TogglePermissionAppInfoPageProvider(this),
)
@Composable
internal fun rememberModel(permissionType: String) = rememberContext { context ->
listModelProviderMap.getValue(permissionType).createModel(context)
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.Context
import android.content.pm.ApplicationInfo
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.android.settingslib.spa.framework.common.SettingsEntry
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.rememberContext
import com.android.settingslib.spa.framework.util.getStringArg
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.R
import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
import com.android.settingslib.spaprivileged.model.app.AppListModel
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.userId
import com.android.settingslib.spaprivileged.model.enterprise.EnhancedConfirmation
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel
import kotlinx.coroutines.flow.Flow
private const val ENTRY_NAME = "AppList"
private const val PERMISSION = "permission"
private const val PAGE_NAME = "TogglePermissionAppList"
private val PAGE_PARAMETER = listOf(
navArgument(PERMISSION) { type = NavType.StringType },
)
internal class TogglePermissionAppListPageProvider(
private val appListTemplate: TogglePermissionAppListTemplate,
) : SettingsPageProvider {
override val name = PAGE_NAME
override val parameter = PAGE_PARAMETER
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
val permissionType = parameter.getStringArg(PERMISSION, arguments)!!
val appListPage = SettingsPage.create(name, parameter = parameter, arguments = arguments)
val appInfoPage = TogglePermissionAppInfoPageProvider.buildPageData(permissionType)
// TODO: add more categories, such as personal, work, cloned, etc.
return listOf("personal").map { category ->
SettingsEntryBuilder.createLinkFrom("${ENTRY_NAME}_$category", appListPage)
.setLink(toPage = appInfoPage)
.build()
}
}
@Composable
override fun Page(arguments: Bundle?) {
val permissionType = arguments?.getString(PERMISSION)!!
appListTemplate.rememberModel(permissionType).TogglePermissionAppList(permissionType)
}
companion object {
/**
* Gets the route to this page.
*
* Expose route to enable enter from non-SPA pages.
*/
fun getRoute(permissionType: String) = "$PAGE_NAME/$permissionType"
fun buildInjectEntry(
permissionType: String,
listModelSupplier: (Context) -> TogglePermissionAppListModel<out AppRecord>,
): SettingsEntryBuilder {
val appListPage = SettingsPage.create(
name = PAGE_NAME,
parameter = PAGE_PARAMETER,
arguments = bundleOf(PERMISSION to permissionType)
)
return SettingsEntryBuilder.createInject(owner = appListPage)
.setUiLayoutFn {
val listModel = rememberContext(listModelSupplier)
Preference(
object : PreferenceModel {
override val title = stringResource(listModel.pageTitleResId)
override val onClick = navigator(route = getRoute(permissionType))
}
)
}
}
}
}
@Composable
internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionAppList(
permissionType: String,
restrictionsProviderFactory: RestrictionsProviderFactory = ::RestrictionsProviderImpl,
appList: @Composable AppListInput<T>.() -> Unit = { AppList() },
) {
val context = LocalContext.current
AppListPage(
title = stringResource(pageTitleResId),
listModel = remember {
TogglePermissionInternalAppListModel(
context = context,
permissionType = permissionType,
listModel = this,
restrictionsProviderFactory = restrictionsProviderFactory,
)
},
appList = appList,
)
}
internal class TogglePermissionInternalAppListModel<T : AppRecord>(
private val context: Context,
private val permissionType: String,
private val listModel: TogglePermissionAppListModel<T>,
private val restrictionsProviderFactory: RestrictionsProviderFactory,
) : AppListModel<T> {
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
listModel.transform(userIdFlow, appListFlow)
override fun filter(userIdFlow: Flow<Int>, option: Int, recordListFlow: Flow<List<T>>) =
listModel.filter(userIdFlow, recordListFlow)
@Composable
override fun getSummary(option: Int, record: T) = getSummary(record)
@Composable
fun getSummary(record: T): () -> String {
val restrictions = remember(record.app.userId, record.app.packageName) {
Restrictions(
userId = record.app.userId,
keys = listModel.switchRestrictionKeys,
enhancedConfirmation = listModel.enhancedConfirmationKey?.let {
EnhancedConfirmation(
key = it,
packageName = record.app.packageName)
})
}
val restrictedMode by restrictionsProviderFactory.rememberRestrictedMode(restrictions)
val allowed = listModel.isAllowed(record)
return RestrictedSwitchPreferenceModel.getSummary(
context = context,
restrictedModeSupplier = { restrictedMode },
summaryIfNoRestricted = { getSummaryIfNoRestricted(allowed()) },
checked = allowed,
)
}
private fun getSummaryIfNoRestricted(allowed: Boolean?): String = when (allowed) {
true -> context.getString(R.string.app_permission_summary_allowed)
false -> context.getString(R.string.app_permission_summary_not_allowed)
null -> context.getPlaceholder()
}
@Composable
override fun AppListItemModel<T>.AppItem() {
AppListItem(
onClick = TogglePermissionAppInfoPageProvider.navigator(
permissionType = permissionType,
app = record.app,
),
)
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2023 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.common
import android.content.pm.UserInfo
import android.content.pm.UserProperties
import android.os.UserHandle
import android.os.UserManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.android.settingslib.spa.widget.scaffold.SettingsPager
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.model.enterprise.EnterpriseRepository
/**
* Info about how to group multiple profiles for Settings.
*
* @see [UserProperties.ShowInSettings]
*/
data class UserGroup(
/** The users in this user group, if multiple users, the first one is parent user. */
val userInfos: List<UserInfo>,
)
@Composable
fun UserProfilePager(content: @Composable (userGroup: UserGroup) -> Unit) {
val context = LocalContext.current
val userGroups = remember { context.userManager.getUserGroups() }
val titles = remember {
val enterpriseRepository = EnterpriseRepository(context)
userGroups.map { userGroup ->
enterpriseRepository.getProfileTitle(
userGroup.userInfos.first(),
)
}
}
SettingsPager(titles) { page ->
content(userGroups[page])
}
}
private fun UserManager.getUserGroups(): List<UserGroup> {
val userGroupList = mutableListOf<UserGroup>()
val showInSettingsMap = getProfiles(UserHandle.myUserId()).groupBy { showInSettings(it) }
showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_WITH_PARENT]?.let {
userGroupList += UserGroup(it)
}
showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_SEPARATE]?.forEach {
userGroupList += UserGroup(listOf(it))
}
return userGroupList
}
private fun UserManager.showInSettings(userInfo: UserInfo): Int {
val userProperties = getUserProperties(userInfo.userHandle)
return if (userInfo.isQuietModeEnabled && userProperties.showInQuietMode
== UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) {
UserProperties.SHOW_IN_SETTINGS_NO
} else {
userProperties.showInSettings
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2023 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.preference
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import com.android.settingslib.spa.widget.preference.MainSwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel.Companion.RestrictedSwitchWrapper
@Composable
fun RestrictedMainSwitchPreference(model: SwitchPreferenceModel, restrictions: Restrictions) {
RestrictedMainSwitchPreference(model, restrictions, ::RestrictionsProviderImpl)
}
@VisibleForTesting
@Composable
internal fun RestrictedMainSwitchPreference(
model: SwitchPreferenceModel,
restrictions: Restrictions,
restrictionsProviderFactory: RestrictionsProviderFactory,
) {
if (restrictions.keys.isEmpty()) {
MainSwitchPreference(model)
return
}
restrictionsProviderFactory.RestrictedSwitchWrapper(model, restrictions) {
MainSwitchPreference(it)
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2023 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.preference
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
@Composable
fun RestrictedPreference(
model: PreferenceModel,
restrictions: Restrictions,
) {
RestrictedPreference(model, restrictions, ::RestrictionsProviderImpl)
}
@VisibleForTesting
@Composable
internal fun RestrictedPreference(
model: PreferenceModel,
restrictions: Restrictions,
restrictionsProviderFactory: RestrictionsProviderFactory,
) {
if (restrictions.keys.isEmpty()) {
Preference(model)
return
}
val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value
val restrictedSwitchModel = remember(restrictedMode) {
RestrictedPreferenceModel(model, restrictedMode)
}
restrictedSwitchModel.RestrictionWrapper {
Preference(restrictedSwitchModel)
}
}
private class RestrictedPreferenceModel(
model: PreferenceModel,
private val restrictedMode: RestrictedMode?,
) : PreferenceModel {
override val title = model.title
override val summary = model.summary
override val icon = model.icon
override val enabled = when (restrictedMode) {
NoRestricted -> model.enabled
else -> ({ false })
}
override val onClick = when (restrictedMode) {
NoRestricted -> model.onClick
// Need to passthrough onClick for clickable semantics, although since enabled is false so
// this will not be called.
BaseUserRestricted -> model.onClick
else -> null
}
@Composable
fun RestrictionWrapper(content: @Composable () -> Unit) {
if (restrictedMode !is BlockedByAdmin) {
content()
return
}
Box(
Modifier
.clickable(
role = Role.Button,
onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
)
) { content() }
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.preference
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel.Companion.RestrictedSwitchWrapper
@Composable
fun RestrictedSwitchPreference(
model: SwitchPreferenceModel,
restrictions: Restrictions,
) {
RestrictedSwitchPreference(model, restrictions, ::RestrictionsProviderImpl)
}
@VisibleForTesting
@Composable
internal fun RestrictedSwitchPreference(
model: SwitchPreferenceModel,
restrictions: Restrictions,
restrictionsProviderFactory: RestrictionsProviderFactory,
) {
if (restrictions.isEmpty()) {
SwitchPreference(model)
return
}
restrictionsProviderFactory.RestrictedSwitchWrapper(model, restrictions) {
SwitchPreference(it)
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright (C) 2023 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.preference
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.state.ToggleableState
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByEcm
import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
internal class RestrictedSwitchPreferenceModel(
context: Context,
model: SwitchPreferenceModel,
private val restrictedMode: RestrictedMode?,
) : SwitchPreferenceModel {
override val title = model.title
override val summary = getSummary(
context = context,
restrictedModeSupplier = { restrictedMode },
summaryIfNoRestricted = model.summary,
checked = model.checked,
)
override val checked = when (restrictedMode) {
null -> ({ null })
is NoRestricted -> model.checked
is BaseUserRestricted -> ({ false })
is BlockedByAdmin -> model.checked
is BlockedByEcm -> model.checked
}
override val changeable = if (restrictedMode is NoRestricted) model.changeable else ({ false })
override val onCheckedChange = when (restrictedMode) {
null -> null
is NoRestricted -> model.onCheckedChange
// Need to passthrough onCheckedChange for toggleable semantics, although since changeable
// is false so this will not be called.
is BaseUserRestricted -> model.onCheckedChange
// Pass null since semantics ToggleableState is provided in RestrictionWrapper.
is BlockedByAdmin -> null
is BlockedByEcm -> null
}
@Composable
fun RestrictionWrapper(content: @Composable () -> Unit) {
when (restrictedMode) {
is BlockedByAdmin -> {
Box(
Modifier
.clickable(
role = Role.Switch,
onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
)
.semantics {
this.toggleableState = ToggleableState(checked())
},
) { content() }
}
is BlockedByEcm -> {
Box(
Modifier
.clickable(
role = Role.Switch,
onClick = { restrictedMode.showRestrictedSettingsDetails() },
)
.semantics {
this.toggleableState = ToggleableState(checked())
},
) { content() }
}
else -> {
content()
}
}
}
private fun ToggleableState(value: Boolean?) = when (value) {
true -> ToggleableState.On
false -> ToggleableState.Off
null -> ToggleableState.Indeterminate
}
companion object {
@Composable
fun RestrictionsProviderFactory.RestrictedSwitchWrapper(
model: SwitchPreferenceModel,
restrictions: Restrictions,
content: @Composable (SwitchPreferenceModel) -> Unit,
) {
val context = LocalContext.current
val restrictedMode = rememberRestrictedMode(restrictions).value
val restrictedSwitchPreferenceModel = remember(restrictedMode) {
RestrictedSwitchPreferenceModel(context, model, restrictedMode)
}
restrictedSwitchPreferenceModel.RestrictionWrapper {
content(restrictedSwitchPreferenceModel)
}
}
fun getSummary(
context: Context,
restrictedModeSupplier: () -> RestrictedMode?,
summaryIfNoRestricted: () -> String,
checked: () -> Boolean?,
): () -> String = {
when (val restrictedMode = restrictedModeSupplier()) {
is NoRestricted -> summaryIfNoRestricted()
is BaseUserRestricted ->
context.getString(com.android.settingslib.R.string.disabled)
is BlockedByAdmin -> restrictedMode.getSummary(checked())
is BlockedByEcm ->
context.getString(com.android.settingslib.R.string.disabled)
null -> context.getPlaceholder()
}
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.scaffold
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope
import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByEcm
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
@Composable
fun MoreOptionsScope.RestrictedMenuItem(
text: String,
restrictions: Restrictions,
onClick: () -> Unit,
) {
RestrictedMenuItemImpl(text, restrictions, onClick, ::RestrictionsProviderImpl)
}
@VisibleForTesting
@Composable
internal fun MoreOptionsScope.RestrictedMenuItemImpl(
text: String,
restrictions: Restrictions,
onClick: () -> Unit,
restrictionsProviderFactory: RestrictionsProviderFactory,
) {
val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value
MenuItem(text = text, enabled = restrictedMode !== BaseUserRestricted) {
when (restrictedMode) {
is BlockedByAdmin -> restrictedMode.sendShowAdminSupportDetailsIntent()
is BlockedByEcm -> restrictedMode.showRestrictedSettingsDetails()
else -> onClick()
}
}
}