diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 446d856..3dc3486 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -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 { SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp) } + composable { + ManagedConfigurationScreen( + it.toRoute(), vm.appRestrictions, vm::getAppRestrictions, vm::setAppRestrictions, + vm::clearAppRestrictions, ::navigateUp + ) + } composable { ManageAppGroupsScreen( vm.appGroups, diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 654ee05..83f0606 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -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()) + + @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): 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()) init { getAppGroups() diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index a004147..a005535 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -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) } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index c4ffd1b..3426cd7 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -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>, + 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() } + 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, + val entryValues: Array, + 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, + val entryValues: Array, + var value: Array? + ) : AppRestriction(key, title, description) +} diff --git a/app/src/main/res/drawable/abc_fill0.xml b/app/src/main/res/drawable/abc_fill0.xml new file mode 100644 index 0000000..c87da90 --- /dev/null +++ b/app/src/main/res/drawable/abc_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/check_box_fill0.xml b/app/src/main/res/drawable/check_box_fill0.xml new file mode 100644 index 0000000..0a6bfb1 --- /dev/null +++ b/app/src/main/res/drawable/check_box_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/number_123_fill0.xml b/app/src/main/res/drawable/number_123_fill0.xml new file mode 100644 index 0000000..29ff3ae --- /dev/null +++ b/app/src/main/res/drawable/number_123_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/radio_button_checked_fill0.xml b/app/src/main/res/drawable/radio_button_checked_fill0.xml new file mode 100644 index 0000000..3f7a1bc --- /dev/null +++ b/app/src/main/res/drawable/radio_button_checked_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/toggle_off_fill0.xml b/app/src/main/res/drawable/toggle_off_fill0.xml new file mode 100644 index 0000000..9179260 --- /dev/null +++ b/app/src/main/res/drawable/toggle_off_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 18c10cc..58fffd4 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -374,6 +374,9 @@ 编辑组 添加到列表 从列表中移除 + 托管配置 + 清除配置 + 指定值 用户限制 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e3baaaf..36c4ef8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -408,6 +408,9 @@ Edit group Add to list Remove from list + Managed configuration + Clear configurations + Specify value User restriction