Implement Managed configurations (#198)

This commit is contained in:
BinTianqi
2025-11-24 18:01:49 +08:00
parent d375a9bae6
commit 4bcd2d8150
11 changed files with 450 additions and 1 deletions

View File

@@ -137,6 +137,8 @@ import com.bintianqi.owndroid.dpm.LockTaskMode
import com.bintianqi.owndroid.dpm.LockTaskModeScreen
import com.bintianqi.owndroid.dpm.ManageAppGroups
import com.bintianqi.owndroid.dpm.ManageAppGroupsScreen
import com.bintianqi.owndroid.dpm.ManagedConfiguration
import com.bintianqi.owndroid.dpm.ManagedConfigurationScreen
import com.bintianqi.owndroid.dpm.MtePolicy
import com.bintianqi.owndroid.dpm.MtePolicyScreen
import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy
@@ -598,6 +600,12 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<SetDefaultDialer> {
SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp)
}
composable<ManagedConfiguration> {
ManagedConfigurationScreen(
it.toRoute(), vm.appRestrictions, vm::getAppRestrictions, vm::setAppRestrictions,
vm::clearAppRestrictions, ::navigateUp
)
}
composable<ManageAppGroups> {
ManageAppGroupsScreen(
vm.appGroups,

View File

@@ -25,6 +25,8 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.RestrictionEntry
import android.content.RestrictionsManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
@@ -39,6 +41,7 @@ import android.net.wifi.WifiManager
import android.net.wifi.WifiSsid
import android.os.Binder
import android.os.Build.VERSION
import android.os.Bundle
import android.os.HardwarePropertiesManager
import android.os.UserHandle
import android.os.UserManager
@@ -60,6 +63,7 @@ import com.bintianqi.owndroid.dpm.ApnConfig
import com.bintianqi.owndroid.dpm.ApnMvnoType
import com.bintianqi.owndroid.dpm.ApnProtocol
import com.bintianqi.owndroid.dpm.AppGroup
import com.bintianqi.owndroid.dpm.AppRestriction
import com.bintianqi.owndroid.dpm.AppStatus
import com.bintianqi.owndroid.dpm.CaCertInfo
import com.bintianqi.owndroid.dpm.CreateUserResult
@@ -510,6 +514,79 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
val appRestrictions = MutableStateFlow(emptyList<AppRestriction>())
@RequiresApi(23)
fun getAppRestrictions(name: String) {
val rm = application.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
val bundle = DPM.getApplicationRestrictions(DAR, name)
println(bundle.keySet())
appRestrictions.value = rm.getManifestRestrictions(name).mapNotNull {
transformRestrictionEntry(it)
}.map {
if (bundle.containsKey(it.key)) {
when (it) {
is AppRestriction.BooleanItem -> it.value = bundle.getBoolean(it.key)
is AppRestriction.StringItem -> it.value = bundle.getString(it.key)
is AppRestriction.IntItem -> it.value = bundle.getInt(it.key)
is AppRestriction.ChoiceItem -> it.value = bundle.getString(it.key)
is AppRestriction.MultiSelectItem -> it.value = bundle.getStringArray(it.key)
}
}
it
}
}
@RequiresApi(23)
fun setAppRestrictions(name: String, item: AppRestriction) {
viewModelScope.launch(Dispatchers.IO) {
appRestrictions.value = emptyList()
DPM.setApplicationRestrictions(
DAR, name,
transformAppRestriction(appRestrictions.value.filter { it.key != item.key }.plus(item))
)
getAppRestrictions(name)
}
}
@RequiresApi(23)
fun clearAppRestrictions(name: String) {
viewModelScope.launch(Dispatchers.IO) {
DPM.setApplicationRestrictions(DAR, name, Bundle())
getAppRestrictions(name)
}
}
fun transformRestrictionEntry(e: RestrictionEntry): AppRestriction? {
return when (e.type) {
RestrictionEntry.TYPE_INTEGER ->
AppRestriction.IntItem(e.key, e.title, e.description, null)
RestrictionEntry.TYPE_STRING ->
AppRestriction.StringItem(e.key, e.title, e.description, null)
RestrictionEntry.TYPE_BOOLEAN ->
AppRestriction.BooleanItem(e.key, e.title, e.description, null)
RestrictionEntry.TYPE_CHOICE -> AppRestriction.ChoiceItem(e.key, e.title,
e.description, e.choiceEntries, e.choiceValues, null)
RestrictionEntry.TYPE_MULTI_SELECT -> AppRestriction.MultiSelectItem(e.key, e.title,
e.description, e.choiceEntries, e.choiceValues, null)
else -> null
}
}
fun transformAppRestriction(list: List<AppRestriction>): Bundle {
val b = Bundle()
for (r in list) {
when (r) {
is AppRestriction.IntItem -> r.value?.let { b.putInt(r.key, it) }
is AppRestriction.StringItem -> r.value?.let { b.putString(r.key, it) }
is AppRestriction.BooleanItem -> r.value?.let { b.putBoolean(r.key, it) }
is AppRestriction.ChoiceItem -> r.value?.let { b.putString(r.key, it) }
is AppRestriction.MultiSelectItem -> r.value?.let { b.putStringArray(r.key, r.value) }
}
}
return b
}
val appGroups = MutableStateFlow(emptyList<AppGroup>())
init {
getAppGroups()

View File

@@ -232,7 +232,7 @@ fun AppLockSettingsScreen(
config: AppLockConfig, setConfig: (AppLockConfig) -> Unit,
onNavigateUp: () -> Unit
) = MyScaffold(R.string.app_lock, onNavigateUp) {
var password by rememberSaveable { mutableStateOf(config.password ?: "") }
var password by rememberSaveable { mutableStateOf("") }
var confirmPassword by rememberSaveable { mutableStateOf("") }
var allowBiometrics by rememberSaveable { mutableStateOf(config.biometrics) }
var lockWhenLeaving by rememberSaveable { mutableStateOf(config.whenLeaving) }

View File

@@ -14,6 +14,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -21,6 +22,7 @@ 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.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items
@@ -29,6 +31,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
@@ -39,8 +42,10 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -53,7 +58,10 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -77,9 +85,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.AppInfo
@@ -285,6 +295,11 @@ fun ApplicationDetailsScreen(
state = status.keepUninstalled,
onCheckedChange = { vm.adSetPackageKu(packageName, it) }
)
if (VERSION.SDK_INT >= 23) {
FunctionItem(R.string.managed_configuration, icon = R.drawable.description_fill0) {
onNavigate(ManagedConfiguration(packageName))
}
}
if(VERSION.SDK_INT >= 28) FunctionItem(R.string.clear_app_storage, icon = R.drawable.mop_fill0) { dialog = 1 }
FunctionItem(R.string.uninstall, icon = R.drawable.delete_fill0) { dialog = 2 }
Spacer(Modifier.height(BottomPadding))
@@ -981,3 +996,301 @@ fun EditAppGroupScreen(
}
}
}
@Serializable class ManagedConfiguration(val packageName: String)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ManagedConfigurationScreen(
params: ManagedConfiguration, appRestrictions: StateFlow<List<AppRestriction>>,
getRestriction: (String) -> Unit, setRestriction: (String, AppRestriction) -> Unit,
clearRestriction: (String) -> Unit, navigateUp: () -> Unit
) {
val restrictions by appRestrictions.collectAsStateWithLifecycle()
var dialog by remember { mutableIntStateOf(-1) }
var clearRestrictionDialog by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
getRestriction(params.packageName)
}
Scaffold(
topBar = {
TopAppBar(
{ Text(stringResource(R.string.managed_configuration)) },
navigationIcon = { NavIcon(navigateUp) },
actions = {
IconButton({
clearRestrictionDialog = true
}) {
Icon(Icons.Outlined.Delete, null)
}
}
)
}
) { paddingValues ->
LazyColumn(Modifier.padding(paddingValues)) {
itemsIndexed(restrictions) { index, entry ->
Row(
Modifier.fillMaxWidth().clickable {
dialog = index
}.padding(HorizontalPadding, 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
val iconId = when (entry) {
is AppRestriction.IntItem -> R.drawable.number_123_fill0
is AppRestriction.StringItem -> R.drawable.abc_fill0
is AppRestriction.BooleanItem -> R.drawable.toggle_off_fill0
is AppRestriction.ChoiceItem -> R.drawable.radio_button_checked_fill0
is AppRestriction.MultiSelectItem -> R.drawable.check_box_fill0
}
Icon(painterResource(iconId), null, Modifier.padding(end = 12.dp))
Column {
if (entry.title != null) {
Text(entry.title!!, style = typography.labelLarge)
Text(entry.key, style = typography.bodyMedium)
} else {
Text(entry.key, style = typography.labelLarge)
}
val text = when (entry) {
is AppRestriction.IntItem -> entry.value?.toString()
is AppRestriction.StringItem -> entry.value?.take(30)
is AppRestriction.BooleanItem -> entry.value?.toString()
is AppRestriction.ChoiceItem -> entry.value
is AppRestriction.MultiSelectItem -> entry.value?.joinToString(limit = 30)
}
Text(
text ?: "null", Modifier.alpha(0.7F),
fontStyle = if(text == null) FontStyle.Italic else null,
style = typography.bodyMedium
)
}
}
}
item {
Spacer(Modifier.height(BottomPadding))
}
}
}
if (dialog != -1) Dialog({
dialog = -1
}) {
Surface(
color = AlertDialogDefaults.containerColor,
shape = AlertDialogDefaults.shape,
tonalElevation = AlertDialogDefaults.TonalElevation,
) {
Column(Modifier.verticalScroll(rememberScrollState()).padding(12.dp)) {
ManagedConfigurationDialog(restrictions[dialog]) {
if (it != null) {
setRestriction(params.packageName, it)
}
dialog = -1
}
}
}
}
if (clearRestrictionDialog) AlertDialog(
text = {
Text(stringResource(R.string.clear_configurations))
},
confirmButton = {
TextButton({
clearRestriction(params.packageName)
clearRestrictionDialog = false
}) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton({
clearRestrictionDialog = false
}) {
Text(stringResource(R.string.cancel))
}
},
onDismissRequest = {
clearRestrictionDialog = false
}
)
}
@Composable
fun ColumnScope.ManagedConfigurationDialog(
restriction: AppRestriction, setRestriction: (AppRestriction?) -> Unit
) {
var specifyValue by remember { mutableStateOf(false) }
var input by remember { mutableStateOf("") }
var inputState by remember { mutableStateOf(false) }
val inputSelections = remember { mutableStateListOf<String>() }
LaunchedEffect(Unit) {
when (restriction) {
is AppRestriction.IntItem -> restriction.value?.let {
input = it.toString()
specifyValue = true
}
is AppRestriction.StringItem -> restriction.value?.let {
input = it
specifyValue = true
}
is AppRestriction.BooleanItem -> restriction.value?.let {
inputState = it
specifyValue = true
}
is AppRestriction.ChoiceItem -> restriction.value?.let {
input = it
specifyValue = true
}
is AppRestriction.MultiSelectItem -> restriction.value?.let {
inputSelections.addAll(it)
specifyValue = true
}
}
}
SelectionContainer {
Column {
restriction.title?.let {
Text(it, style = typography.titleLarge)
}
Text(restriction.key, Modifier.padding(vertical = 4.dp), style = typography.labelLarge)
Spacer(Modifier.height(4.dp))
restriction.description?.let {
Text(it, Modifier.alpha(0.8F), style = typography.bodyMedium)
}
Spacer(Modifier.height(8.dp))
}
}
Row(
Modifier.fillMaxWidth().padding(bottom = 4.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Text(stringResource(R.string.specify_value))
Switch(specifyValue, { specifyValue = it })
}
if (specifyValue) when (restriction) {
is AppRestriction.IntItem -> {
OutlinedTextField(
input, { input = it }, Modifier.fillMaxWidth(),
isError = input.toIntOrNull() == null
)
}
is AppRestriction.StringItem -> {
OutlinedTextField(
input, { input = it }, Modifier.fillMaxWidth()
)
}
is AppRestriction.BooleanItem -> {
Switch(inputState, { inputState = it })
}
is AppRestriction.ChoiceItem -> {
restriction.entryValues.forEachIndexed { index, value ->
val label = restriction.entries.getOrNull(index)
Row(
Modifier.fillMaxWidth().clickable {
input = value
}.padding(8.dp, 4.dp)
) {
RadioButton(input == value, { input = value })
Spacer(Modifier.width(8.dp))
if (label == null) {
Text(value)
} else {
Column {
Text(label)
Text(value, Modifier.alpha(0.7F), style = typography.bodyMedium)
}
}
}
}
}
is AppRestriction.MultiSelectItem -> {
restriction.entryValues.forEachIndexed { index, value ->
val label = restriction.entries.getOrNull(index)
Row(
Modifier.fillMaxWidth().clickable {
if (value in inputSelections)
inputSelections -= value else inputSelections += value
}.padding(8.dp, 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(value in inputSelections, null)
Spacer(Modifier.width(8.dp))
if (label == null) {
Text(value)
} else {
Column {
Text(label)
Text(value, Modifier.alpha(0.7F), style = typography.bodyMedium)
}
}
}
}
}
}
Row(Modifier.align(Alignment.End).padding(top = 4.dp)) {
TextButton({
setRestriction(null)
}, Modifier.padding(end = 4.dp)) {
Text(stringResource(R.string.cancel))
}
TextButton({
val newRestriction = when (restriction) {
is AppRestriction.IntItem -> restriction.copy(
value = if (specifyValue) input.toIntOrNull() else null
)
is AppRestriction.StringItem -> restriction.copy(
value = if (specifyValue) input else null
)
is AppRestriction.BooleanItem -> restriction.copy(
value = if (specifyValue) inputState else null
)
is AppRestriction.ChoiceItem -> restriction.copy(
value = if (specifyValue) input else null
)
is AppRestriction.MultiSelectItem -> restriction.copy(
value = if (specifyValue) inputSelections.toTypedArray() else null
)
}
setRestriction(newRestriction)
}) {
Text(stringResource(R.string.confirm))
}
}
}
sealed class AppRestriction(
open val key: String, open val title: String?, open val description: String?
) {
data class IntItem(
override val key: String,
override val title: String?,
override val description: String?,
var value: Int?,
) : AppRestriction(key, title, description)
data class StringItem(
override val key: String,
override val title: String?,
override val description: String?,
var value: String?
) : AppRestriction(key, title, description)
data class BooleanItem(
override val key: String,
override val title: String?,
override val description: String?,
var value: Boolean?
) : AppRestriction(key, title, description)
data class ChoiceItem(
override val key: String,
override val title: String?,
override val description: String?,
val entries: Array<String>,
val entryValues: Array<String>,
var value: String?
) : AppRestriction(key, title, description)
data class MultiSelectItem(
override val key: String,
override val title: String?,
override val description: String?,
val entries: Array<String>,
val entryValues: Array<String>,
var value: Array<String>?
) : AppRestriction(key, title, description)
}