fix: 首次提交
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user