From 4bcd2d8150c7371c228b3125437c96dfb4e7959f Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Mon, 24 Nov 2025 18:01:49 +0800 Subject: [PATCH 01/20] Implement Managed configurations (#198) --- .../com/bintianqi/owndroid/MainActivity.kt | 8 + .../com/bintianqi/owndroid/MyViewModel.kt | 77 +++++ .../java/com/bintianqi/owndroid/Settings.kt | 2 +- .../bintianqi/owndroid/dpm/Applications.kt | 313 ++++++++++++++++++ app/src/main/res/drawable/abc_fill0.xml | 9 + app/src/main/res/drawable/check_box_fill0.xml | 9 + .../main/res/drawable/number_123_fill0.xml | 9 + .../drawable/radio_button_checked_fill0.xml | 9 + .../main/res/drawable/toggle_off_fill0.xml | 9 + app/src/main/res/values-zh-rCN/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 11 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/abc_fill0.xml create mode 100644 app/src/main/res/drawable/check_box_fill0.xml create mode 100644 app/src/main/res/drawable/number_123_fill0.xml create mode 100644 app/src/main/res/drawable/radio_button_checked_fill0.xml create mode 100644 app/src/main/res/drawable/toggle_off_fill0.xml 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 From b242488a2a890315e8bceacaf486a09218b20659 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 25 Nov 2025 13:31:20 +0800 Subject: [PATCH 02/20] Fix Managed configurations bugs, add search bar (#198) --- .../com/bintianqi/owndroid/MainActivity.kt | 2 +- .../com/bintianqi/owndroid/MyViewModel.kt | 16 ++-- .../com/bintianqi/owndroid/PackageChooser.kt | 16 ++-- .../bintianqi/owndroid/dpm/Applications.kt | 95 +++++++++++++++---- 4 files changed, 89 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 3dc3486..3ba2c7f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -602,7 +602,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { } composable { ManagedConfigurationScreen( - it.toRoute(), vm.appRestrictions, vm::getAppRestrictions, vm::setAppRestrictions, + it.toRoute(), vm.appRestrictions, vm::setAppRestrictions, vm::clearAppRestrictions, ::navigateUp ) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 83f0606..5bfdcca 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -518,12 +518,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) { @RequiresApi(23) fun getAppRestrictions(name: String) { - val rm = application.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager + val rm = application.getSystemService(RestrictionsManager::class.java) val bundle = DPM.getApplicationRestrictions(DAR, name) - println(bundle.keySet()) - appRestrictions.value = rm.getManifestRestrictions(name).mapNotNull { + appRestrictions.value = rm.getManifestRestrictions(name)?.mapNotNull { transformRestrictionEntry(it) - }.map { + }?.map { if (bundle.containsKey(it.key)) { when (it) { is AppRestriction.BooleanItem -> it.value = bundle.getBoolean(it.key) @@ -534,17 +533,16 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } } it - } + } ?: emptyList() } @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)) + val bundle = transformAppRestriction( + appRestrictions.value.filter { it.key != item.key }.plus(item) ) + DPM.setApplicationRestrictions(DAR, name, bundle) getAppRestrictions(name) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index ec7b1f7..271e234 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.outlined.Clear import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -122,15 +123,12 @@ fun AppChooserScreen( keyboardActions = KeyboardActions { focusMgr.clearFocus() }, placeholder = { Text(stringResource(R.string.search)) }, trailingIcon = { - Icon( - painter = painterResource(R.drawable.close_fill0), - contentDescription = null, - modifier = Modifier.clickable { - focusMgr.clearFocus() - query = "" - searchMode = false - } - ) + IconButton({ + query = "" + searchMode = false + }) { + Icon(Icons.Outlined.Clear, null) + } }, textStyle = typography.bodyLarge, modifier = Modifier.fillMaxWidth().focusRequester(fr) 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 3426cd7..201b66b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -40,7 +40,9 @@ import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.Button @@ -79,6 +81,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -258,7 +262,11 @@ fun ApplicationDetailsScreen( var dialog by rememberSaveable { mutableIntStateOf(0) } // 1: clear storage, 2: uninstall val info = vm.getAppInfo(packageName) val status by vm.appStatus.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { vm.getAppStatus(packageName) } + val appRestrictions by vm.appRestrictions.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + vm.getAppStatus(packageName) + vm.getAppRestrictions(packageName) + } MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) { Column(Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { Image(rememberDrawablePainter(info.icon), null, Modifier.size(50.dp)) @@ -295,7 +303,7 @@ fun ApplicationDetailsScreen( state = status.keepUninstalled, onCheckedChange = { vm.adSetPackageKu(packageName, it) } ) - if (VERSION.SDK_INT >= 23) { + if (VERSION.SDK_INT >= 23 && appRestrictions.isNotEmpty()) { FunctionItem(R.string.managed_configuration, icon = R.drawable.description_fill0) { onNavigate(ManagedConfiguration(packageName)) } @@ -1003,35 +1011,74 @@ fun EditAppGroupScreen( @Composable fun ManagedConfigurationScreen( params: ManagedConfiguration, appRestrictions: StateFlow>, - getRestriction: (String) -> Unit, setRestriction: (String, AppRestriction) -> Unit, - clearRestriction: (String) -> Unit, navigateUp: () -> 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) + var searchMode by remember { mutableStateOf(false) } + var searchKeyword by remember { mutableStateOf("") } + val displayRestrictions = if (searchKeyword.isEmpty()) { + restrictions + } else { + restrictions.filter { + it.key.contentEquals(searchKeyword, true) || + it.title?.contains(searchKeyword, true) ?: true + } } + var dialog by remember { mutableStateOf(null) } + var clearRestrictionDialog by remember { mutableStateOf(false) } Scaffold( topBar = { TopAppBar( - { Text(stringResource(R.string.managed_configuration)) }, + { + if (searchMode) { + val fr = remember { FocusRequester() } + LaunchedEffect(Unit) { + fr.requestFocus() + } + OutlinedTextField( + searchKeyword, { searchKeyword = it }, + Modifier.fillMaxWidth().focusRequester(fr), + textStyle = typography.bodyLarge, + placeholder = { Text(stringResource(R.string.search)) }, + trailingIcon = { + IconButton({ + searchKeyword = "" + searchMode = false + }) { + Icon(Icons.Outlined.Clear, null) + } + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) + ) + } else { + Text(stringResource(R.string.managed_configuration)) + } + }, navigationIcon = { NavIcon(navigateUp) }, actions = { - IconButton({ - clearRestrictionDialog = true - }) { - Icon(Icons.Outlined.Delete, null) + if (!searchMode) { + IconButton({ + searchMode = true + }) { + Icon(Icons.Outlined.Search, null) + } + IconButton({ + clearRestrictionDialog = true + }) { + Icon(Icons.Outlined.Delete, null) + } } } ) - } + }, + contentWindowInsets = adaptiveInsets() ) { paddingValues -> LazyColumn(Modifier.padding(paddingValues)) { - itemsIndexed(restrictions) { index, entry -> + items(displayRestrictions, { it.key }) { entry -> Row( Modifier.fillMaxWidth().clickable { - dialog = index + dialog = entry }.padding(HorizontalPadding, 8.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -1055,7 +1102,13 @@ fun ManagedConfigurationScreen( 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) + is AppRestriction.MultiSelectItem -> { + if (entry.value != null) { + entry.entryValues + .filter { entry.value?.contains(it) ?: false } + .joinToString(limit = 30) + } else null + } } Text( text ?: "null", Modifier.alpha(0.7F), @@ -1070,8 +1123,8 @@ fun ManagedConfigurationScreen( } } } - if (dialog != -1) Dialog({ - dialog = -1 + if (dialog != null) Dialog({ + dialog = null }) { Surface( color = AlertDialogDefaults.containerColor, @@ -1079,11 +1132,11 @@ fun ManagedConfigurationScreen( tonalElevation = AlertDialogDefaults.TonalElevation, ) { Column(Modifier.verticalScroll(rememberScrollState()).padding(12.dp)) { - ManagedConfigurationDialog(restrictions[dialog]) { + ManagedConfigurationDialog(dialog!!) { if (it != null) { setRestriction(params.packageName, it) } - dialog = -1 + dialog = null } } } From c8c428e92915b0b3a6b78fbcdf793a29b5368814 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 25 Nov 2025 23:22:57 +0800 Subject: [PATCH 03/20] Items ordering in managed configurations Check FingerprintManager exist before startBiometricsUnlock(), fix #200, #201, #202 --- app/build.gradle.kts | 1 + .../java/com/bintianqi/owndroid/AppLock.kt | 2 + .../bintianqi/owndroid/dpm/Applications.kt | 222 ++++++++++-------- .../res/drawable/drag_indicator_fill0.xml | 9 + gradle/libs.versions.toml | 2 + 5 files changed, 138 insertions(+), 98 deletions(-) create mode 100644 app/src/main/res/drawable/drag_indicator_fill0.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 84c2009..f6a280d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,6 +103,7 @@ dependencies { implementation(libs.androidx.fragment) implementation(libs.hiddenApiBypass) implementation(libs.libsu) + implementation(libs.reoderable) implementation(libs.serialization) implementation(kotlin("reflect")) } \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt index 820c554..9c64c20 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt @@ -3,6 +3,7 @@ package com.bintianqi.owndroid import android.content.Context import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback +import android.hardware.fingerprint.FingerprintManager import android.os.Build import android.os.CancellationSignal import androidx.activity.compose.BackHandler @@ -92,6 +93,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi @RequiresApi(28) fun startBiometricsUnlock(context: Context, onSucceed: () -> Unit) { + context.getSystemService(FingerprintManager::class.java) ?: return val callback = object : AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) { super.onAuthenticationSucceeded(result) 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 201b66b..e5807e5 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -118,6 +119,8 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyListState val String.isValidPackageName get() = Regex("""^(?:[a-zA-Z]\w*\.)+[a-zA-Z]\w*$""").matches(this) @@ -265,7 +268,7 @@ fun ApplicationDetailsScreen( val appRestrictions by vm.appRestrictions.collectAsStateWithLifecycle() LaunchedEffect(Unit) { vm.getAppStatus(packageName) - vm.getAppRestrictions(packageName) + if (VERSION.SDK_INT >= 23) vm.getAppRestrictions(packageName) } MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) { Column(Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { @@ -1102,13 +1105,7 @@ fun ManagedConfigurationScreen( is AppRestriction.StringItem -> entry.value?.take(30) is AppRestriction.BooleanItem -> entry.value?.toString() is AppRestriction.ChoiceItem -> entry.value - is AppRestriction.MultiSelectItem -> { - if (entry.value != null) { - entry.entryValues - .filter { entry.value?.contains(it) ?: false } - .joinToString(limit = 30) - } else null - } + is AppRestriction.MultiSelectItem -> entry.value?.joinToString(limit = 30) } Text( text ?: "null", Modifier.alpha(0.7F), @@ -1131,13 +1128,11 @@ fun ManagedConfigurationScreen( shape = AlertDialogDefaults.shape, tonalElevation = AlertDialogDefaults.TonalElevation, ) { - Column(Modifier.verticalScroll(rememberScrollState()).padding(12.dp)) { - ManagedConfigurationDialog(dialog!!) { - if (it != null) { - setRestriction(params.packageName, it) - } - dialog = null + ManagedConfigurationDialog(dialog!!) { + if (it != null) { + setRestriction(params.packageName, it) } + dialog = null } } } @@ -1167,13 +1162,24 @@ fun ManagedConfigurationScreen( } @Composable -fun ColumnScope.ManagedConfigurationDialog( +fun 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() } + val multiSelectList = remember { + mutableStateListOf( + *(if (restriction is AppRestriction.MultiSelectItem) { + restriction.entryValues.mapIndexed { index, value -> + MultiSelectEntry( + value, restriction.entries.getOrNull(index), + restriction.value?.contains(value) ?: false + ) + } + } else emptyList()).toTypedArray() + ) + } LaunchedEffect(Unit) { when (restriction) { is AppRestriction.IntItem -> restriction.value?.let { @@ -1193,48 +1199,54 @@ fun ColumnScope.ManagedConfigurationDialog( 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)) - } + val listState = rememberLazyListState() + val reorderableListState = rememberReorderableLazyListState(listState) { from, to -> + // `-1` because there's an `item` before items + multiSelectList.add(from.index - 1, multiSelectList.removeAt(to.index - 1)) } - 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 - ) + LazyColumn(Modifier.padding(12.dp), listState) { + item { + 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 }) + } } - is AppRestriction.StringItem -> { - OutlinedTextField( - input, { input = it }, Modifier.fillMaxWidth() - ) - } - is AppRestriction.BooleanItem -> { - Switch(inputState, { inputState = it }) - } - is AppRestriction.ChoiceItem -> { - restriction.entryValues.forEachIndexed { index, value -> + if (specifyValue) when (restriction) { + is AppRestriction.IntItem -> item { + OutlinedTextField( + input, { input = it }, Modifier.fillMaxWidth(), + isError = input.toIntOrNull() == null + ) + } + is AppRestriction.StringItem -> item { + OutlinedTextField( + input, { input = it }, Modifier.fillMaxWidth() + ) + } + is AppRestriction.BooleanItem -> item { + Switch(inputState, { inputState = it }) + } + is AppRestriction.ChoiceItem -> itemsIndexed(restriction.entryValues) { index, value -> val label = restriction.entries.getOrNull(index) Row( Modifier.fillMaxWidth().clickable { @@ -1253,58 +1265,70 @@ fun ColumnScope.ManagedConfigurationDialog( } } } - } - 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) + is AppRestriction.MultiSelectItem -> itemsIndexed( + multiSelectList, { _, v -> v.value } + ) { index, entry -> + ReorderableItem(reorderableListState, entry.value) { + Row( + Modifier.fillMaxWidth().clickable { + val old = multiSelectList[index] + multiSelectList[index] = old.copy(selected = !old.selected) + }.padding(8.dp, 4.dp), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) { + Checkbox(entry.selected, null) + Spacer(Modifier.width(8.dp)) + if (entry.title == null) { + Text(entry.value) + } else { + Column { + Text(entry.title) + Text(entry.value, Modifier.alpha(0.7F), style = typography.bodyMedium) + } + } } + Icon( + painterResource(R.drawable.drag_indicator_fill0), null, + Modifier.draggableHandle() + ) } } } } - } - 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 - ) + item { + Row(Modifier.fillMaxWidth().padding(top = 4.dp), Arrangement.End) { + 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) + multiSelectList.filter { it.selected } + .map { it.value }.toTypedArray() + else null + ) + } + setRestriction(newRestriction) + }) { + Text(stringResource(R.string.confirm)) + } } - setRestriction(newRestriction) - }) { - Text(stringResource(R.string.confirm)) } } } @@ -1347,3 +1371,5 @@ sealed class AppRestriction( var value: Array? ) : AppRestriction(key, title, description) } + +data class MultiSelectEntry(val value: String, val title: String?, val selected: Boolean) diff --git a/app/src/main/res/drawable/drag_indicator_fill0.xml b/app/src/main/res/drawable/drag_indicator_fill0.xml new file mode 100644 index 0000000..3719a6b --- /dev/null +++ b/app/src/main/res/drawable/drag_indicator_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a68b36..1090da1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ dhizuku = "2.5.4" dhizuku-server = "0.0.10" hiddenApiBypass = "6.1" libsu = "6.0.0" +reoderable = "3.0.0" serialization = "1.9.0" [libraries] @@ -33,6 +34,7 @@ dhizuku-api = { module = "io.github.iamr0s:Dhizuku-API", version.ref = "dhizuku" dhizuku-server-api = { group = "io.github.iamr0s", name = "Dhizuku-SERVER_API", version.ref = "dhizuku-server" } hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" } libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" } +reoderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reoderable" } serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } From a716c4bf2dc317cfe6da32b9a02199df5f0c95f1 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Wed, 26 Nov 2025 08:42:47 +1100 Subject: [PATCH 04/20] fix: use contains instead of contentEqual for managed configurations search --- app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e5807e5..557adc4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -1024,7 +1024,7 @@ fun ManagedConfigurationScreen( restrictions } else { restrictions.filter { - it.key.contentEquals(searchKeyword, true) || + it.key.contains(searchKeyword, true) || it.title?.contains(searchKeyword, true) ?: true } } From ed6908a5cd93ef966a2628f918c4c1b2e121ccff Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Wed, 26 Nov 2025 13:11:33 +0800 Subject: [PATCH 05/20] Fix AppLock bug (#200, #201, #202) Fix Managed configuration item ordering (#198) Remove app from list after uninstall (#205) --- .../java/com/bintianqi/owndroid/AppLock.kt | 2 +- .../com/bintianqi/owndroid/MyViewModel.kt | 13 +++++++----- .../bintianqi/owndroid/dpm/Applications.kt | 20 +++++++++++++----- .../java/com/bintianqi/owndroid/dpm/System.kt | 21 +++++++++++++++---- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 43 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt index 9c64c20..88b59f5 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt @@ -84,7 +84,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi } } } - Button(::unlock, Modifier.align(Alignment.End).padding(top = 8.dp), input.length >= 4) { + Button(::unlock, Modifier.align(Alignment.End).padding(top = 8.dp)) { Text(stringResource(R.string.unlock)) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 5bfdcca..6f02c1a 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -158,10 +158,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) { return AppLockConfig(passwordHash?.ifEmpty { null }, SP.biometricsUnlock, SP.lockWhenLeaving) } fun setAppLockConfig(config: AppLockConfig) { - SP.lockPasswordHash = if (config.password == null) { - "" - } else { - config.password.hash() + if (config.password == null) { + SP.lockPasswordHash = "" + } else if (!config.password.isEmpty()) { + SP.lockPasswordHash = config.password.hash() } SP.biometricsUnlock = config.biometrics SP.lockWhenLeaving = config.whenLeaving @@ -359,8 +359,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) { context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?) } else { context.unregisterReceiver(this) - if(statusExtra == PackageInstaller.STATUS_SUCCESS) { + if (statusExtra == PackageInstaller.STATUS_SUCCESS) { onComplete(null) + installedPackages.update { pkg -> + pkg.filter { it.name != packageName } + } } else { onComplete(parsePackageInstallerMessage(context, intent)) } 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 557adc4..fad0691 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -121,6 +121,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState +import kotlin.collections.indexOf val String.isValidPackageName get() = Regex("""^(?:[a-zA-Z]\w*\.)+[a-zA-Z]\w*$""").matches(this) @@ -317,7 +318,10 @@ fun ApplicationDetailsScreen( } if(dialog == 1 && VERSION.SDK_INT >= 28) ClearAppStorageDialog(packageName, vm::clearAppData) { dialog = 0 } - if(dialog == 2) UninstallAppDialog(packageName, vm::uninstallPackage) { dialog = 0 } + if(dialog == 2) UninstallAppDialog(packageName, vm::uninstallPackage) { + dialog = 0 + if (it) onNavigateUp() + } } @Serializable object Suspend @@ -522,7 +526,8 @@ fun UninstallAppScreen( @Composable private fun UninstallAppDialog( - packageName: String, onUninstall: (String, (String?) -> Unit) -> Unit, onClose: () -> Unit + packageName: String, onUninstall: (String, (String?) -> Unit) -> Unit, + onClose: (Boolean) -> Unit ) { var uninstalling by rememberSaveable { mutableStateOf(false) } var errorMessage by rememberSaveable { mutableStateOf(null) } @@ -538,7 +543,7 @@ private fun UninstallAppDialog( uninstalling = true onUninstall(packageName) { uninstalling = false - if(it == null) onClose() else errorMessage = it + if (it == null) onClose(true) else errorMessage = it } }, enabled = !uninstalling, @@ -548,9 +553,11 @@ private fun UninstallAppDialog( } }, dismissButton = { - TextButton(onClose, enabled = !uninstalling) { Text(stringResource(R.string.cancel)) } + TextButton({ + onClose(false) + }, enabled = !uninstalling) { Text(stringResource(R.string.cancel)) } }, - onDismissRequest = onClose, + onDismissRequest = { onClose(false) }, properties = DialogProperties(false, false) ) } @@ -1176,6 +1183,9 @@ fun ManagedConfigurationDialog( value, restriction.entries.getOrNull(index), restriction.value?.contains(value) ?: false ) + }.sortedBy { entry -> + val index = restriction.value?.indexOf(entry.value) + if (index == null || index == -1) Int.MAX_VALUE else index } } else emptyList()).toTypedArray() ) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 777e865..2ca218c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -72,7 +72,6 @@ import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TimePicker @@ -97,6 +96,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -181,7 +181,7 @@ fun SystemManagerScreen( if(VERSION.SDK_INT >= 31) { FunctionItem(R.string.nearby_streaming_policy, icon = R.drawable.share_fill0) { onNavigate(NearbyStreamingPolicy) } } - if (VERSION.SDK_INT >= 28 && privilege.device && !privilege.dhizuku) { + if (VERSION.SDK_INT >= 28 && privilege.device) { FunctionItem(R.string.lock_task_mode, icon = R.drawable.lock_fill0) { onNavigate(LockTaskMode) } } FunctionItem(R.string.ca_cert, icon = R.drawable.license_fill0) { onNavigate(CaCert) } @@ -1175,7 +1175,7 @@ fun LockTaskModeScreen( .fillMaxSize() .padding(paddingValues) ) { - TabRow(tabIndex) { + PrimaryTabRow(tabIndex) { Tab( tabIndex == 0, onClick = { coroutine.launch { pagerState.animateScrollToPage(0) } }, text = { Text(stringResource(R.string.start)) } @@ -1210,6 +1210,7 @@ private fun StartLockTaskMode( ) { val context = LocalContext.current val focusMgr = LocalFocusManager.current + val privilege by Privilege.status.collectAsStateWithLifecycle() var packageName by rememberSaveable { mutableStateOf("") } var activity by rememberSaveable { mutableStateOf("") } var specifyActivity by rememberSaveable { mutableStateOf(false) } @@ -1223,6 +1224,17 @@ private fun StartLockTaskMode( .verticalScroll(rememberScrollState()) ) { Spacer(Modifier.height(5.dp)) + if (privilege.dhizuku) Column( + Modifier + .fillMaxWidth().padding(vertical = 8.dp) + .background(colorScheme.errorContainer, RoundedCornerShape(10.dp)) + .padding(8.dp) + ) { + Text( + stringResource(R.string.start_lock_task_mode_not_supported), + color = colorScheme.onErrorContainer + ) + } PackageNameTextField(packageName, onChoosePackage) { packageName = it } Row( Modifier @@ -1252,10 +1264,11 @@ private fun StartLockTaskMode( if (!result) context.showOperationResultToast(false) }, enabled = packageName.isNotBlank() && (!specifyActivity || activity.isNotBlank()) + && !privilege.dhizuku ) { Text(stringResource(R.string.start)) } - Notes(R.string.info_start_lock_task_mode) + if (!privilege.dhizuku) Notes(R.string.info_start_lock_task_mode) } } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 58fffd4..e494d21 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -173,6 +173,7 @@ 附近通知传输 在足够安全时启用 锁定任务模式 + Dhizuku模式下不支持启动锁定任务模式 应用未被允许 禁用全部 允许状态栏信息 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36c4ef8..5e8682b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -201,6 +201,7 @@ Nearby notification streaming policy Same managed account only Lock task mode + Starting lock task mode is not supported under Dhizuku mode App is not allowed Disable all From 001d013b0c393dbcdf252a3929c753ea36e03881 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Wed, 26 Nov 2025 22:20:26 +0800 Subject: [PATCH 06/20] Fix bugs of application functions (#205, #206, #207) --- .../com/bintianqi/owndroid/MainActivity.kt | 3 ++ .../com/bintianqi/owndroid/MyViewModel.kt | 50 +++++++++++-------- .../main/java/com/bintianqi/owndroid/Utils.kt | 19 ++++++- .../bintianqi/owndroid/dpm/Applications.kt | 37 ++++++++++---- 4 files changed, 78 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 3ba2c7f..f7c81d2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -255,6 +255,9 @@ class MainActivity : FragmentActivity() { val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {} launcher.launch(Manifest.permission.POST_NOTIFICATIONS) } + registerPackageRemovedReceiver(this) { + vm.onPackageRemoved(it) + } setContent { var appLockDialog by rememberSaveable { mutableStateOf(false) } val theme by vm.theme.collectAsStateWithLifecycle() diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 6f02c1a..625d204 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -200,6 +200,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } } } + fun onPackageRemoved(name: String) { + installedPackages.update { list -> + list.filter { it.name != name } + } + } fun getAppInfo(info: ApplicationInfo) = AppInfo(info.packageName, info.loadLabel(PM).toString(), info.loadIcon(PM), info.flags) fun getAppInfo(name: String): AppInfo { @@ -361,9 +366,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { context.unregisterReceiver(this) if (statusExtra == PackageInstaller.STATUS_SUCCESS) { onComplete(null) - installedPackages.update { pkg -> - pkg.filter { it.name != packageName } - } } else { onComplete(parsePackageInstallerMessage(context, intent)) } @@ -464,14 +466,18 @@ class MyViewModel(application: Application): AndroidViewModel(application) { DPM.isUninstallBlocked(DAR, name), if (VERSION.SDK_INT >= 30) name in DPM.getUserControlDisabledPackages(DAR) else false, if (VERSION.SDK_INT >= 28) name in DPM.getMeteredDataDisabledPackages(DAR) else false, - if (VERSION.SDK_INT >= 28) DPM.getKeepUninstalledPackages(DAR)?.contains(name) == true else false + if (VERSION.SDK_INT >= 28 && Privilege.status.value.device) + DPM.getKeepUninstalledPackages(DAR)?.contains(name) == true + else false ) } // Application details @RequiresApi(24) fun adSetPackageSuspended(name: String, status: Boolean) { - DPM.setPackagesSuspended(DAR, arrayOf(name), status) - appStatus.update { it.copy(suspend = DPM.isPackageSuspended(DAR, name)) } + try { + DPM.setPackagesSuspended(DAR, arrayOf(name), status) + appStatus.update { it.copy(suspend = DPM.isPackageSuspended(DAR, name)) } + } catch (_: Exception) {} } fun adSetPackageHidden(name: String, status: Boolean) { DPM.setApplicationHidden(DAR, name, status) @@ -522,21 +528,25 @@ class MyViewModel(application: Application): AndroidViewModel(application) { @RequiresApi(23) fun getAppRestrictions(name: String) { val rm = application.getSystemService(RestrictionsManager::class.java) - val bundle = DPM.getApplicationRestrictions(DAR, name) - 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) + try { + val bundle = DPM.getApplicationRestrictions(DAR, name) + 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 - } ?: emptyList() + it + } ?: emptyList() + } catch (e: Exception) { + e.printStackTrace() + } } @RequiresApi(23) diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 700658d..8dd3858 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -1,9 +1,12 @@ package com.bintianqi.owndroid +import android.content.BroadcastReceiver import android.content.ClipData import android.content.ClipboardManager import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageInfo import android.net.Uri import android.os.Build @@ -159,4 +162,18 @@ fun Modifier.clickableTextField(onClick: () -> Unit) = fun adaptiveInsets(): WindowInsets { val navbar = WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal) return WindowInsets.ime.union(navbar).union(WindowInsets.displayCutout) -} \ No newline at end of file +} + +fun registerPackageRemovedReceiver( + ctx: Context, callback: (String) -> Unit +) { + val br = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + callback(intent.data!!.schemeSpecificPart) + } + } + val filter = IntentFilter() + filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED) + filter.addDataScheme("package") + ctx.registerReceiver(br, filter) +} 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 fad0691..c058038 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -14,7 +14,6 @@ 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 @@ -63,6 +62,9 @@ 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.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -121,7 +123,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState -import kotlin.collections.indexOf val String.isValidPackageName get() = Regex("""^(?:[a-zA-Z]\w*\.)+[a-zA-Z]\w*$""").matches(this) @@ -540,20 +541,23 @@ private fun UninstallAppDialog( confirmButton = { TextButton( { - uninstalling = true - onUninstall(packageName) { - uninstalling = false - if (it == null) onClose(true) else errorMessage = it + if (errorMessage == null) { + uninstalling = true + onUninstall(packageName) { + uninstalling = false + if (it == null) onClose(true) else errorMessage = it + } + } else { + onClose(false) } }, - enabled = !uninstalling, - colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) + enabled = !uninstalling ) { Text(stringResource(R.string.confirm)) } }, dismissButton = { - TextButton({ + if (errorMessage == null) TextButton({ onClose(false) }, enabled = !uninstalling) { Text(stringResource(R.string.cancel)) } }, @@ -1254,7 +1258,20 @@ fun ManagedConfigurationDialog( ) } is AppRestriction.BooleanItem -> item { - Switch(inputState, { inputState = it }) + SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) { + SegmentedButton( + inputState, { inputState = true }, + SegmentedButtonDefaults.itemShape(0, 2) + ) { + Text("true") + } + SegmentedButton( + !inputState, { inputState = false }, + SegmentedButtonDefaults.itemShape(1, 2) + ) { + Text("false") + } + } } is AppRestriction.ChoiceItem -> itemsIndexed(restriction.entryValues) { index, value -> val label = restriction.entries.getOrNull(index) From c55cc2d3e1bfca7ff5dfbe14a5612ef5fdd19f86 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Mon, 1 Dec 2025 12:46:06 +0800 Subject: [PATCH 07/20] Fix some crash on profile owner mode (#206) --- app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt | 3 ++- app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt | 3 ++- app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 625d204..f6972b4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -681,7 +681,8 @@ class MyViewModel(application: Application): AndroidViewModel(application) { backupServiceEnabled = if (VERSION.SDK_INT >= 26) DPM.isBackupServiceEnabled(DAR) else false, btContactSharingDisabled = if (VERSION.SDK_INT >= 23 && privilege.work) DPM.getBluetoothContactSharingDisabled(DAR) else false, - commonCriteriaMode = if (VERSION.SDK_INT >= 30) DPM.isCommonCriteriaModeEnabled(DAR) else false, + commonCriteriaMode = if (VERSION.SDK_INT >= 30 && privilege.run { device || org }) + DPM.isCommonCriteriaModeEnabled(DAR) else false, usbSignalEnabled = if (VERSION.SDK_INT >= 31) DPM.isUsbDataSignalingEnabled else false, canDisableUsbSignal = if (VERSION.SDK_INT >= 31) DPM.canUsbDataSignalingBeDisabled() else false ) 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 c058038..235516b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -468,7 +468,8 @@ private fun ClearAppStorageDialog( AlertDialog( title = { Text(stringResource(R.string.clear_app_storage)) }, text = { - if(clearing) LinearProgressIndicator(Modifier.fillMaxWidth()) + if (clearing) LinearProgressIndicator(Modifier.fillMaxWidth()) + else Text(stringResource(R.string.clear_app_storage_confirmation)) }, confirmButton = { TextButton( diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index 5e1abd2..8d6b2da 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -186,9 +186,9 @@ fun NetworkOptionsScreen( ) { val privilege by Privilege.status.collectAsStateWithLifecycle() var dialog by rememberSaveable { mutableIntStateOf(0) } - var lanEnabled by rememberSaveable { mutableStateOf(getLanEnabled()) } MyScaffold(R.string.options, onNavigateUp, 0.dp) { if(VERSION.SDK_INT >= 30 && (privilege.device || privilege.org)) { + var lanEnabled by rememberSaveable { mutableStateOf(getLanEnabled()) } SwitchItem(R.string.lockdown_admin_configured_network, icon = R.drawable.wifi_password_fill0, state = lanEnabled, onCheckedChange = { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e494d21..9fab479 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -363,6 +363,7 @@ 许可的输入法 卸载后保留的应用 清除应用存储 + 你确定要删除应用数据吗? 设置默认拨号器 卸载应用 安装应用 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e8682b..332e8ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -398,6 +398,7 @@ Permitted IME Keep uninstalled packages Clear app storage + Are you sure to delete app data? Set default dialer Uninstall app Install app From 82efc071b8aab98edd287bc3b2898ed9cb837429 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 9 Dec 2025 23:23:51 +0800 Subject: [PATCH 08/20] Improve create user error message (#209) --- app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index f6972b4..ee9371f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -1443,6 +1443,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) { UserManager.USER_OPERATION_ERROR_UNKNOWN -> R.string.unknown_error UserManager.USER_OPERATION_ERROR_MANAGED_PROFILE-> R.string.fail_managed_profile UserManager.USER_OPERATION_ERROR_MAX_RUNNING_USERS -> R.string.limit_reached + UserManager.USER_OPERATION_ERROR_MAX_USERS -> R.string.limit_reached UserManager.USER_OPERATION_ERROR_CURRENT_USER -> R.string.fail_current_user else -> R.string.unknown } From 8cf725b3c4e53dad85700da28aeb7ef2e1e6cb90 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Tue, 16 Dec 2025 16:29:51 +1100 Subject: [PATCH 09/20] feat: hide password input for app lock (with trailing icon to show) --- .../main/java/com/bintianqi/owndroid/AppLock.kt | 17 ++++++++++++++++- app/src/main/res/drawable/visibility_fill0.xml | 11 +++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/visibility_fill0.xml diff --git a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt index 88b59f5..9c4d8ec 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,6 +41,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -51,6 +54,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi val fr = remember { FocusRequester() } var input by rememberSaveable { mutableStateOf("") } var isError by rememberSaveable { mutableStateOf(false) } + var showPassword by remember { mutableStateOf(false) } fun unlock() { if(input.hash() == SP.lockPasswordHash) { fm.clearFocus() @@ -76,7 +80,18 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, imeAction = if(input.length >= 4) ImeAction.Go else ImeAction.Done ), - keyboardActions = KeyboardActions({ fm.clearFocus() }, { unlock() }) + keyboardActions = KeyboardActions({ fm.clearFocus() }, { unlock() }), + visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon( + painter = painterResource( + id = if (showPassword) R.drawable.visibility_off_fill0 else R.drawable.visibility_fill0 + ), + contentDescription = if (showPassword) "Hide password" else "Show password" + ) + } + } ) if(Build.VERSION.SDK_INT >= 28 && SP.biometricsUnlock) { FilledTonalIconButton({ startBiometricsUnlock(context, onSucceed) }, Modifier.padding(start = 4.dp)) { diff --git a/app/src/main/res/drawable/visibility_fill0.xml b/app/src/main/res/drawable/visibility_fill0.xml new file mode 100644 index 0000000..62b8445 --- /dev/null +++ b/app/src/main/res/drawable/visibility_fill0.xml @@ -0,0 +1,11 @@ + + + From dad57a88466c4f9eb94e6f3fa24702738da59f22 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Fri, 19 Dec 2025 00:57:16 +0800 Subject: [PATCH 10/20] Improve Lock task mode Make this feature available in Dhizuku mode Use foreground service to display the notification --- app/src/main/AndroidManifest.xml | 10 ++- .../java/com/bintianqi/owndroid/AppLock.kt | 2 +- .../com/bintianqi/owndroid/LockTaskService.kt | 82 +++++++++++++++++++ .../com/bintianqi/owndroid/MyViewModel.kt | 2 + .../java/com/bintianqi/owndroid/Receiver.kt | 33 -------- .../java/com/bintianqi/owndroid/dpm/System.kt | 12 --- app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values/strings.xml | 4 +- 10 files changed, 95 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 506c110..e328d50 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,8 @@ + + + android:exported="false"> @@ -99,6 +101,12 @@ android:name=".ApiReceiver" android:exported="true"> + + + Unit, onDismiss: () -> Unit) = Dialog(onDismi IconButton(onClick = { showPassword = !showPassword }) { Icon( painter = painterResource( - id = if (showPassword) R.drawable.visibility_off_fill0 else R.drawable.visibility_fill0 + id = if (showPassword) R.drawable.visibility_fill0 else R.drawable.visibility_off_fill0 ), contentDescription = if (showPassword) "Hide password" else "Show password" ) diff --git a/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt new file mode 100644 index 0000000..2efcc5f --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt @@ -0,0 +1,82 @@ +package com.bintianqi.owndroid + +import android.app.ActivityManager +import android.app.PendingIntent +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@RequiresApi(28) +class LockTaskService: Service() { + val coroutineScope = CoroutineScope(Dispatchers.IO) + + override fun onBind(intent: Intent?): IBinder? = null + + val stopReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + coroutineScope.cancel() + stopLockTask() + stop() + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + val filter = IntentFilter(STOP_ACTION) + ContextCompat.registerReceiver( + this, stopReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED + ) + val pendingIntent = PendingIntent.getBroadcast( + this, 0, Intent(STOP_ACTION).setPackage(this.packageName), PendingIntent.FLAG_IMMUTABLE + ) + val notification = NotificationCompat.Builder(this, MyNotificationChannel.LockTaskMode.id) + .setContentTitle(getText(R.string.lock_task_mode)) + .setSmallIcon(R.drawable.lock_fill0) + .addAction(NotificationCompat.Action.Builder(null, getString(R.string.stop), pendingIntent).build()) + .build() + ServiceCompat.startForeground( + this, NotificationType.LockTaskMode.id, notification, + if (Build.VERSION.SDK_INT < 34) 0 else ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST + ) + coroutineScope.launch { + val am = getSystemService(ActivityManager::class.java) + delay(3000) + while (am.lockTaskModeState == ActivityManager.LOCK_TASK_MODE_LOCKED) { + delay(1000) + } + stop() + } + return START_NOT_STICKY + } + + fun stop() { + unregisterReceiver(stopReceiver) + stopSelf() + } + + fun stopLockTask() { + val features = Privilege.DPM.getLockTaskFeatures(Privilege.DAR) + val packages = Privilege.DPM.getLockTaskPackages(Privilege.DAR) + Privilege.DPM.setLockTaskPackages(Privilege.DAR, arrayOf()) + Privilege.DPM.setLockTaskPackages(Privilege.DAR, packages) + Privilege.DPM.setLockTaskFeatures(Privilege.DAR, features) + } + + companion object { + const val STOP_ACTION = "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index ee9371f..f20e3c5 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -2,6 +2,7 @@ package com.bintianqi.owndroid import android.accounts.Account import android.annotation.SuppressLint +import android.app.ActivityManager import android.app.ActivityOptions import android.app.Application import android.app.KeyguardManager @@ -886,6 +887,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) { if (intent != null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) application.startActivity(intent, options.toBundle()) + application.startForegroundService(Intent(application, LockTaskService::class.java)) return true } else { return false diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index b542132..f5cd676 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -1,31 +1,17 @@ package com.bintianqi.owndroid -import android.app.NotificationManager -import android.app.PendingIntent import android.app.admin.DeviceAdminReceiver -import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.Binder import android.os.Build.VERSION import android.os.UserHandle import android.os.UserManager -import androidx.core.app.NotificationCompat import com.bintianqi.owndroid.dpm.handlePrivilegeChange import com.bintianqi.owndroid.dpm.retrieveNetworkLogs import com.bintianqi.owndroid.dpm.retrieveSecurityLogs class Receiver : DeviceAdminReceiver() { - override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) - if(VERSION.SDK_INT >= 26 && intent.action == "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") { - val receiver = ComponentName(context, this::class.java) - val packages = Privilege.DPM.getLockTaskPackages(receiver) - Privilege.DPM.setLockTaskPackages(receiver, arrayOf()) - Privilege.DPM.setLockTaskPackages(receiver, packages) - } - } - override fun onEnabled(context: Context, intent: Intent) { super.onEnabled(context, intent) Privilege.updateStatus() @@ -56,25 +42,6 @@ class Receiver : DeviceAdminReceiver() { } } - override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) { - super.onLockTaskModeEntering(context, intent, pkg) - val stopIntent = Intent(context, this::class.java) - .setAction("com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") - val pendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE) - val notification = NotificationCompat.Builder(context, MyNotificationChannel.LockTaskMode.id) - .setContentTitle(context.getText(R.string.lock_task_mode)) - .setSmallIcon(R.drawable.lock_fill0) - .addAction(NotificationCompat.Action.Builder(null, context.getString(R.string.stop), pendingIntent).build()) - .build() - val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - nm.notify(NotificationType.LockTaskMode.id, notification) - } - - override fun onLockTaskModeExiting(context: Context, intent: Intent) { - super.onLockTaskModeExiting(context, intent) - NotificationUtils.cancel(context, NotificationType.LockTaskMode) - } - override fun onPasswordChanged(context: Context, intent: Intent, userHandle: UserHandle) { super.onPasswordChanged(context, intent, userHandle) sendUserRelatedNotification(context, userHandle, NotificationType.PasswordChanged) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 2ca218c..07b61e6 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -1224,17 +1224,6 @@ private fun StartLockTaskMode( .verticalScroll(rememberScrollState()) ) { Spacer(Modifier.height(5.dp)) - if (privilege.dhizuku) Column( - Modifier - .fillMaxWidth().padding(vertical = 8.dp) - .background(colorScheme.errorContainer, RoundedCornerShape(10.dp)) - .padding(8.dp) - ) { - Text( - stringResource(R.string.start_lock_task_mode_not_supported), - color = colorScheme.onErrorContainer - ) - } PackageNameTextField(packageName, onChoosePackage) { packageName = it } Row( Modifier @@ -1264,7 +1253,6 @@ private fun StartLockTaskMode( if (!result) context.showOperationResultToast(false) }, enabled = packageName.isNotBlank() && (!specifyActivity || activity.isNotBlank()) - && !privilege.dhizuku ) { Text(stringResource(R.string.start)) } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 258abcd..ea24a39 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -278,7 +278,6 @@ Сетевой журнал Удалить журналы Экспортировать журналы - Пара ключей Wi-Fi Предпочтительная сетевая служба Add config Идентификатор сети diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 33d5f12..9195b73 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -307,7 +307,6 @@ Ağ Kayıtları Kayıtları Sil Kayıtları Dışa Aktar - Wi-Fi Kimlik Doğrulama Anahtar Çifti Tercihli Ağ Servisi Yapılandırma Ekle Ağ Kimliği diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9fab479..b67a883 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -173,7 +173,6 @@ 附近通知传输 在足够安全时启用 锁定任务模式 - Dhizuku模式下不支持启动锁定任务模式 应用未被允许 禁用全部 允许状态栏信息 @@ -294,7 +293,6 @@ 网络日志已收集 删除日志 导出日志 - Wi-Fi密钥对 首选网络服务 添加配置 网络ID diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 332e8ec..924b30f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -102,7 +102,7 @@ Organization name Disable account management Account type - Transfer Ownership + Transfer ownership Lock screen info Support Messages Short message @@ -201,7 +201,6 @@ Nearby notification streaming policy Same managed account only Lock task mode - Starting lock task mode is not supported under Dhizuku mode App is not allowed Disable all @@ -328,7 +327,6 @@ Network logs collected Delete logs Export logs - Wi-Fi keypair Preferential network service Add config Network ID From 98a5b1fb320a09b0274972e75784d579bfeff27d Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Fri, 19 Dec 2025 08:19:13 +1100 Subject: [PATCH 11/20] fix: set ongoing true for lock task mode exit notification --- app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt index 2efcc5f..f9b1675 100644 --- a/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt +++ b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt @@ -47,6 +47,7 @@ class LockTaskService: Service() { .setContentTitle(getText(R.string.lock_task_mode)) .setSmallIcon(R.drawable.lock_fill0) .addAction(NotificationCompat.Action.Builder(null, getString(R.string.stop), pendingIntent).build()) + .setOngoing(true) .build() ServiceCompat.startForeground( this, NotificationType.LockTaskMode.id, notification, From 68cb5395c0765fcfd3315061b4c412ee1e1932f9 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Fri, 19 Dec 2025 10:12:13 +1100 Subject: [PATCH 12/20] feat: add clear task (start fresh) option for lock task mode --- .../com/bintianqi/owndroid/MyViewModel.kt | 7 ++-- .../java/com/bintianqi/owndroid/dpm/System.kt | 34 ++++++++++++++++--- app/src/main/res/values/strings.xml | 1 + 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index f20e3c5..f82b5cc 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -874,7 +874,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) { getLockTaskPackages() } @RequiresApi(28) - fun startLockTaskMode(packageName: String, activity: String): Boolean { + fun startLockTaskMode(packageName: String, activity: String, clearTask: Boolean): Boolean { if (!DPM.isLockTaskPermitted(packageName)) { val list = lockTaskPackages.value.map { it.name } + packageName DPM.setLockTaskPackages(DAR, list.toTypedArray()) @@ -885,7 +885,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) { Intent().setComponent(ComponentName(packageName, activity)) } else PM.getLaunchIntentForPackage(packageName) if (intent != null) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + or (if (clearTask) Intent.FLAG_ACTIVITY_CLEAR_TASK else 0) + ) application.startActivity(intent, options.toBundle()) application.startForegroundService(Intent(application, LockTaskService::class.java)) return true diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 07b61e6..4f361fd 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -31,6 +31,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -1150,7 +1151,7 @@ fun NearbyStreamingPolicyScreen( fun LockTaskModeScreen( chosenPackage: Channel, onChoosePackage: () -> Unit, lockTaskPackages: StateFlow>, getLockTaskPackages: () -> Unit, - setLockTaskPackage: (String, Boolean) -> Unit, startLockTaskMode: (String, String) -> Boolean, + setLockTaskPackage: (String, Boolean) -> Unit, startLockTaskMode: (String, String, Boolean) -> Boolean, getLockTaskFeatures: () -> Int, setLockTaskFeature: (Int) -> String?, onNavigateUp: () -> Unit ) { val coroutine = rememberCoroutineScope() @@ -1205,7 +1206,7 @@ fun LockTaskModeScreen( @RequiresApi(28) @Composable private fun StartLockTaskMode( - startLockTaskMode: (String, String) -> Boolean, + startLockTaskMode: (String, String, Boolean) -> Boolean, chosenPackage: Channel, onChoosePackage: () -> Unit ) { val context = LocalContext.current @@ -1214,6 +1215,8 @@ private fun StartLockTaskMode( var packageName by rememberSaveable { mutableStateOf("") } var activity by rememberSaveable { mutableStateOf("") } var specifyActivity by rememberSaveable { mutableStateOf(false) } + var clearTask by rememberSaveable { mutableStateOf(true) } + LaunchedEffect(Unit) { packageName = chosenPackage.receive() } @@ -1228,7 +1231,30 @@ private fun StartLockTaskMode( Row( Modifier .fillMaxWidth() - .padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically + .padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = clearTask, + onCheckedChange = { clearTask = it } + ) + Text( + text = stringResource(R.string.lock_task_mode_start_clear_task), + modifier = Modifier + .padding(start = 8.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + clearTask = !clearTask + } + ) + } + Row( + Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically ) { Checkbox(specifyActivity, { specifyActivity = it @@ -1249,7 +1275,7 @@ private fun StartLockTaskMode( .fillMaxWidth() .padding(bottom = 5.dp), onClick = { - val result = startLockTaskMode(packageName, activity) + val result = startLockTaskMode(packageName, activity, clearTask) if (!result) context.showOperationResultToast(false) }, enabled = packageName.isNotBlank() && (!specifyActivity || activity.isNotBlank()) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 924b30f..31ff108 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -732,4 +732,5 @@ Activate method ADB command This app uses Device owner or Profile owner privileges. These privileges are extremely dangerous, please use them with caution. If used improperly, they may result in severe losses. The developers will not be responsible for this. + Clear task (start fresh) From a53e05645cb492daeb54c99e824e55bc67eb36b0 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Date: Fri, 19 Dec 2025 22:29:31 +1100 Subject: [PATCH 13/20] fix: set NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE to avoid delays --- app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt index f9b1675..301117d 100644 --- a/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt +++ b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt @@ -48,6 +48,7 @@ class LockTaskService: Service() { .setSmallIcon(R.drawable.lock_fill0) .addAction(NotificationCompat.Action.Builder(null, getString(R.string.stop), pendingIntent).build()) .setOngoing(true) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .build() ServiceCompat.startForeground( this, NotificationType.LockTaskMode.id, notification, From 5f95aa81c00d31b992d33923f8b047d7cb90efe2 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 21 Dec 2025 21:27:34 +0800 Subject: [PATCH 14/20] feat: 'Show a notification to exit' option for lock task mode --- .../com/bintianqi/owndroid/MyViewModel.kt | 17 ++++-- .../java/com/bintianqi/owndroid/dpm/System.kt | 52 +++++++------------ app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values/strings.xml | 3 +- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index f82b5cc..2b9debb 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -2,7 +2,6 @@ package com.bintianqi.owndroid import android.accounts.Account import android.annotation.SuppressLint -import android.app.ActivityManager import android.app.ActivityOptions import android.app.Application import android.app.KeyguardManager @@ -874,12 +873,22 @@ class MyViewModel(application: Application): AndroidViewModel(application) { getLockTaskPackages() } @RequiresApi(28) - fun startLockTaskMode(packageName: String, activity: String, clearTask: Boolean): Boolean { + fun startLockTaskMode( + packageName: String, activity: String, clearTask: Boolean, showNotification: Boolean + ): Boolean { if (!DPM.isLockTaskPermitted(packageName)) { val list = lockTaskPackages.value.map { it.name } + packageName DPM.setLockTaskPackages(DAR, list.toTypedArray()) getLockTaskPackages() } + if (showNotification) { + DPM.setLockTaskFeatures( + DAR, + DPM.getLockTaskFeatures(DAR) or + DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS or + DevicePolicyManager.LOCK_TASK_FEATURE_HOME + ) + } val options = ActivityOptions.makeBasic().setLockTaskEnabled(true) val intent = if(activity.isNotEmpty()) { Intent().setComponent(ComponentName(packageName, activity)) @@ -890,7 +899,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) { or (if (clearTask) Intent.FLAG_ACTIVITY_CLEAR_TASK else 0) ) application.startActivity(intent, options.toBundle()) - application.startForegroundService(Intent(application, LockTaskService::class.java)) + if (showNotification) { + application.startForegroundService(Intent(application, LockTaskService::class.java)) + } return true } else { return false diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 4f361fd..64bcbab 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -31,7 +31,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -97,7 +96,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -110,9 +108,9 @@ import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SP +import com.bintianqi.owndroid.adaptiveInsets import com.bintianqi.owndroid.clickableTextField import com.bintianqi.owndroid.formatDate -import com.bintianqi.owndroid.adaptiveInsets import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CheckBoxItem @@ -1151,7 +1149,8 @@ fun NearbyStreamingPolicyScreen( fun LockTaskModeScreen( chosenPackage: Channel, onChoosePackage: () -> Unit, lockTaskPackages: StateFlow>, getLockTaskPackages: () -> Unit, - setLockTaskPackage: (String, Boolean) -> Unit, startLockTaskMode: (String, String, Boolean) -> Boolean, + setLockTaskPackage: (String, Boolean) -> Unit, + startLockTaskMode: (String, String, Boolean, Boolean) -> Boolean, getLockTaskFeatures: () -> Int, setLockTaskFeature: (Int) -> String?, onNavigateUp: () -> Unit ) { val coroutine = rememberCoroutineScope() @@ -1206,7 +1205,7 @@ fun LockTaskModeScreen( @RequiresApi(28) @Composable private fun StartLockTaskMode( - startLockTaskMode: (String, String, Boolean) -> Boolean, + startLockTaskMode: (String, String, Boolean, Boolean) -> Boolean, chosenPackage: Channel, onChoosePackage: () -> Unit ) { val context = LocalContext.current @@ -1216,44 +1215,28 @@ private fun StartLockTaskMode( var activity by rememberSaveable { mutableStateOf("") } var specifyActivity by rememberSaveable { mutableStateOf(false) } var clearTask by rememberSaveable { mutableStateOf(true) } - + var showNotification by rememberSaveable() { mutableStateOf(true) } LaunchedEffect(Unit) { packageName = chosenPackage.receive() } Column( Modifier .fillMaxWidth() - .padding(horizontal = HorizontalPadding) .verticalScroll(rememberScrollState()) ) { - Spacer(Modifier.height(5.dp)) - PackageNameTextField(packageName, onChoosePackage) { packageName = it } + PackageNameTextField( + packageName, onChoosePackage, Modifier.padding(HorizontalPadding, 8.dp) + ) { packageName = it } + FullWidthCheckBoxItem( + R.string.lock_task_mode_start_clear_task, clearTask + ) { clearTask = it } + FullWidthCheckBoxItem( + R.string.lock_taso_mode_show_notification, showNotification + ) { showNotification = it } Row( Modifier .fillMaxWidth() - .padding(top = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = clearTask, - onCheckedChange = { clearTask = it } - ) - Text( - text = stringResource(R.string.lock_task_mode_start_clear_task), - modifier = Modifier - .padding(start = 8.dp) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - clearTask = !clearTask - } - ) - } - Row( - Modifier - .fillMaxWidth() - .padding(bottom = 4.dp), + .padding(start = 4.dp, top = 4.dp, end = HorizontalPadding, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Checkbox(specifyActivity, { @@ -1273,15 +1256,16 @@ private fun StartLockTaskMode( Button( modifier = Modifier .fillMaxWidth() - .padding(bottom = 5.dp), + .padding(horizontal = HorizontalPadding), onClick = { - val result = startLockTaskMode(packageName, activity, clearTask) + val result = startLockTaskMode(packageName, activity, clearTask, showNotification) if (!result) context.showOperationResultToast(false) }, enabled = packageName.isNotBlank() && (!specifyActivity || activity.isNotBlank()) ) { Text(stringResource(R.string.start)) } + Spacer(Modifier.height(5.dp)) if (!privilege.dhizuku) Notes(R.string.info_start_lock_task_mode) } } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b67a883..3b95803 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -173,6 +173,8 @@ 附近通知传输 在足够安全时启用 锁定任务模式 + 清除任务(新实例) + 显示通知以退出 应用未被允许 禁用全部 允许状态栏信息 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31ff108..9add4f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -201,6 +201,8 @@ Nearby notification streaming policy Same managed account only Lock task mode + Clear task (start fresh) + Show a notification to exit App is not allowed Disable all @@ -732,5 +734,4 @@ Activate method ADB command This app uses Device owner or Profile owner privileges. These privileges are extremely dangerous, please use them with caution. If used improperly, they may result in severe losses. The developers will not be responsible for this. - Clear task (start fresh) From 434e9c1b25757059ad172ae164de1663e44457ec Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 27 Dec 2025 13:58:07 +0800 Subject: [PATCH 15/20] feat: multi-select in Package chooser (#215) Update dependencies Set minSdk to 23 since many compose libraries requires it --- app/build.gradle.kts | 2 +- .../com/bintianqi/owndroid/MainActivity.kt | 128 ++++++++---- .../com/bintianqi/owndroid/MyViewModel.kt | 86 +++++--- .../com/bintianqi/owndroid/PackageChooser.kt | 129 ++++++++++-- .../main/java/com/bintianqi/owndroid/Utils.kt | 2 + .../bintianqi/owndroid/dpm/Applications.kt | 195 ++++++++++-------- .../java/com/bintianqi/owndroid/dpm/System.kt | 8 +- app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values/strings.xml | 4 +- gradle/libs.versions.toml | 8 +- 10 files changed, 381 insertions(+), 185 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f6a280d..9ee1a2c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,7 +24,7 @@ android { defaultConfig { applicationId = "com.bintianqi.owndroid" - minSdk = 21 + minSdk = 23 targetSdk = 36 versionCode = 41 versionName = "7.2" diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index f7c81d2..0324052 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -157,7 +157,6 @@ import com.bintianqi.owndroid.dpm.OrganizationOwnedProfileScreen import com.bintianqi.owndroid.dpm.OverrideApn import com.bintianqi.owndroid.dpm.OverrideApnScreen import com.bintianqi.owndroid.dpm.PackageFunctionScreen -import com.bintianqi.owndroid.dpm.PackageFunctionScreenWithoutResult import com.bintianqi.owndroid.dpm.Password import com.bintianqi.owndroid.dpm.PasswordInfo import com.bintianqi.owndroid.dpm.PasswordInfoScreen @@ -290,7 +289,10 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { } } fun choosePackage() { - navController.navigate(ApplicationsList(false)) + navController.navigate(ApplicationsList(false, true)) + } + fun chooseSinglePackage() { + navController.navigate(ApplicationsList(false, false)) } fun navigateToAppGroups() { navController.navigate(ManageAppGroups) @@ -336,7 +338,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { DelegatedAdminsScreen(vm.delegatedAdmins, vm::getDelegatedAdmins, ::navigateUp, ::navigate) } composable{ - AddDelegatedAdminScreen(vm.chosenPackage, ::choosePackage, it.toRoute(), + AddDelegatedAdminScreen(vm.chosenPackage, ::chooseSinglePackage, it.toRoute(), vm::setDelegatedAdmin, ::navigateUp) } composable { DeviceInfoScreen(vm, ::navigateUp) } @@ -390,9 +392,11 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { vm::getNsNotificationPolicy, vm::setNsNotificationPolicy, ::navigateUp) } composable { - LockTaskModeScreen(vm.chosenPackage, ::choosePackage, vm.lockTaskPackages, + LockTaskModeScreen( + vm.chosenPackage, ::chooseSinglePackage, ::choosePackage, vm.lockTaskPackages, vm::getLockTaskPackages, vm::setLockTaskPackage, vm::startLockTaskMode, - vm:: getLockTaskFeatures, vm::setLockTaskFeatures, ::navigateUp) + vm:: getLockTaskFeatures, vm::setLockTaskFeatures, ::navigateUp + ) } composable { CaCertScreen(vm.installedCaCerts, vm::getCaCerts, vm.selectedCaCert, vm::selectCaCert, vm::installCaCert, vm::parseCaCert, @@ -440,7 +444,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { WifiSsidPolicyScreen(vm::getSsidPolicy, vm::setSsidPolicy, ::navigateUp) } composable { - NetworkStatsScreen(vm.chosenPackage, ::choosePackage, vm::getPackageUid, + NetworkStatsScreen(vm.chosenPackage, ::chooseSinglePackage, vm::getPackageUid, vm::queryNetworkStats, ::navigateUp) { navController.navigate(NetworkStatsViewer) } } composable { @@ -451,7 +455,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { } composable { AlwaysOnVpnPackageScreen(vm::getAlwaysOnVpnPackage, vm::getAlwaysOnVpnLockdown, - vm::setAlwaysOnVpn, vm.chosenPackage, ::choosePackage, ::navigateUp) + vm::setAlwaysOnVpn, vm.chosenPackage, ::chooseSinglePackage, ::navigateUp) } composable { RecommendedGlobalProxyScreen(vm::setRecommendedGlobalProxy, ::navigateUp) @@ -499,10 +503,10 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { DeleteWorkProfileScreen(vm::wipeData, ::navigateUp) } composable { - val canSwitchView = (it.toRoute() as ApplicationsList).canSwitchView + val params = it.toRoute() AppChooserScreen( - canSwitchView, vm.installedPackages, vm.refreshPackagesProgress, { name -> - if (canSwitchView) { + params, vm.installedPackages, vm.refreshPackagesProgress, { name -> + if (params.canSwitchView) { if (name == null) { navigateUp() } else { @@ -517,12 +521,12 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { navController.navigate(ApplicationsFeatures) { popUpTo(Home) } - }, vm::refreshPackageList) + }, vm::refreshPackageList, vm::setPackageSuspended, vm::setPackageHidden) } composable { ApplicationsFeaturesScreen(::navigateUp, ::navigate) { SP.applicationsListView = true - navController.navigate(ApplicationsList(true)) { + navController.navigate(ApplicationsList(true, true)) { popUpTo(Home) } } @@ -531,52 +535,72 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { ApplicationDetailsScreen(it.toRoute(), vm, ::navigateUp, ::navigate) } composable { - PackageFunctionScreen(R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged, + PackageFunctionScreen( + R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged, vm::setPackageSuspended, ::navigateUp, vm.chosenPackage, ::choosePackage, - ::navigateToAppGroups, vm.appGroups, R.string.info_suspend_app) + ::navigateToAppGroups, vm.appGroups, R.string.info_suspend_app + ) } composable { - PackageFunctionScreen(R.string.hide, vm.hiddenPackages, vm::getHiddenPackages, - vm::setPackageHidden, ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups) + PackageFunctionScreen( + R.string.hide, vm.hiddenPackages, vm::getHiddenPackages, vm::setPackageHidden, + ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups + ) } composable { - PackageFunctionScreenWithoutResult(R.string.block_uninstall, vm.ubPackages, - vm::getUbPackages, vm::setPackageUb, ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups) + PackageFunctionScreen( + R.string.block_uninstall, vm.ubPackages, vm::getUbPackages, vm::setPackageUb, + ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups + ) } composable { - PackageFunctionScreenWithoutResult(R.string.disable_user_control, vm.ucdPackages, - vm::getUcdPackages, vm::setPackageUcd, ::navigateUp, vm.chosenPackage, - ::choosePackage, ::navigateToAppGroups, vm.appGroups, R.string.info_disable_user_control) + PackageFunctionScreen( + R.string.disable_user_control, vm.ucdPackages, vm::getUcdPackages, + vm::setPackageUcd, ::navigateUp, vm.chosenPackage, ::choosePackage, + ::navigateToAppGroups, vm.appGroups, R.string.info_disable_user_control + ) } composable { - PermissionsManagerScreen(vm.packagePermissions, vm::getPackagePermissions, - vm::setPackagePermission, ::navigateUp, it.toRoute(), vm.chosenPackage, ::choosePackage) + PermissionsManagerScreen( + vm.packagePermissions, vm::getPackagePermissions, vm::setPackagePermission, + ::navigateUp, it.toRoute(), vm.chosenPackage, ::chooseSinglePackage + ) } composable { - PackageFunctionScreen(R.string.disable_metered_data, vm.mddPackages, - vm::getMddPackages, vm::setPackageMdd, ::navigateUp, vm.chosenPackage, - ::choosePackage, ::navigateToAppGroups, vm.appGroups) + PackageFunctionScreen( + R.string.disable_metered_data, vm.mddPackages, vm::getMddPackages, + vm::setPackageMdd, ::navigateUp, vm.chosenPackage, ::choosePackage, + ::navigateToAppGroups, vm.appGroups + ) } composable { - ClearAppStorageScreen(vm.chosenPackage, ::choosePackage, vm::clearAppData, ::navigateUp) + ClearAppStorageScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::clearAppData, ::navigateUp + ) } composable { - UninstallAppScreen(vm.chosenPackage, ::choosePackage, vm::uninstallPackage, ::navigateUp) + UninstallAppScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::uninstallPackage, ::navigateUp + ) } composable { - PackageFunctionScreenWithoutResult(R.string.keep_uninstalled_packages, vm.kuPackages, - vm::getKuPackages, vm::setPackageKu, ::navigateUp, vm.chosenPackage, - ::choosePackage, ::navigateToAppGroups, vm.appGroups, - R.string.info_keep_uninstalled_apps) + PackageFunctionScreen( + R.string.keep_uninstalled_packages, vm.kuPackages, vm::getKuPackages, + vm::setPackageKu, ::navigateUp, vm.chosenPackage, ::choosePackage, + ::navigateToAppGroups, vm.appGroups, R.string.info_keep_uninstalled_apps + ) } composable { - InstallExistingAppScreen(vm.chosenPackage, ::choosePackage, - vm::installExistingApp, ::navigateUp) + InstallExistingAppScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::installExistingApp, ::navigateUp + ) } composable { - PackageFunctionScreenWithoutResult(R.string.cross_profile_apps, vm.cpPackages, + PackageFunctionScreen( + R.string.cross_profile_apps, vm.cpPackages, vm::getCpPackages, vm::setPackageCp, ::navigateUp, vm.chosenPackage, - ::choosePackage, ::navigateToAppGroups, vm.appGroups) + ::choosePackage, ::navigateToAppGroups, vm.appGroups + ) } composable { PackageFunctionScreen(R.string.cross_profile_widget, vm.cpwProviders, @@ -584,24 +608,35 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { ::choosePackage, ::navigateToAppGroups, vm.appGroups) } composable { - CredentialManagerPolicyScreen(vm.chosenPackage, ::choosePackage, - vm.cmPackages, vm::getCmPolicy, vm::setCmPackage, vm::setCmPolicy, ::navigateUp) + CredentialManagerPolicyScreen( + vm.chosenPackage, ::choosePackage, vm.cmPackages, vm::getCmPolicy, + vm::setCmPackage, vm::setCmPolicy, ::navigateUp + ) } composable { - PermittedAsAndImPackages(R.string.permitted_accessibility_services, + PermittedAsAndImPackages( + R.string.permitted_accessibility_services, R.string.system_accessibility_always_allowed, vm.chosenPackage, ::choosePackage, - vm.pasPackages, vm::getPasPackages, vm::setPasPackage, vm::setPasPolicy, ::navigateUp) + vm.pasPackages, vm::getPasPackages, vm::setPasPackage, vm::setPasPolicy, + ::navigateUp + ) } composable { - PermittedAsAndImPackages(R.string.permitted_ime, R.string.system_ime_always_allowed, + PermittedAsAndImPackages( + R.string.permitted_ime, R.string.system_ime_always_allowed, vm.chosenPackage, ::choosePackage, vm.pimPackages, vm::getPimPackages, - vm::setPimPackage, vm::setPimPolicy, ::navigateUp) + vm::setPimPackage, vm::setPimPolicy, ::navigateUp + ) } composable { - EnableSystemAppScreen(vm.chosenPackage, ::choosePackage, vm::enableSystemApp, ::navigateUp) + EnableSystemAppScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::enableSystemApp, ::navigateUp + ) } composable { - SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp) + SetDefaultDialerScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::setDefaultDialer, ::navigateUp + ) } composable { ManagedConfigurationScreen( @@ -762,7 +797,10 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) { } if(privilege.device || privilege.profile) { HomePageItem(R.string.applications, R.drawable.apps_fill0) { - onNavigate(if(SP.applicationsListView) ApplicationsList(true) else ApplicationsFeatures) + onNavigate( + if (SP.applicationsListView) ApplicationsList(true, true) + else ApplicationsFeatures + ) } if(VERSION.SDK_INT >= 24) { HomePageItem(R.string.user_restriction, R.drawable.person_off) { onNavigate(UserRestriction) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 2b9debb..496f61e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -224,10 +224,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) { suspendedPackages.value = packages.map { getAppInfo(it) } } @RequiresApi(24) - fun setPackageSuspended(name: String, status: Boolean): Boolean { - val result = DPM.setPackagesSuspended(DAR, arrayOf(name), status) + fun setPackageSuspended(packages: List, status: Boolean) { + DPM.setPackagesSuspended(DAR, packages.toTypedArray(), status) getSuspendedPackaged() - return result.isEmpty() } val hiddenPackages = MutableStateFlow(emptyList()) @@ -236,10 +235,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) { DPM.isApplicationHidden(DAR, it.packageName) }.map { getAppInfo(it) } } - fun setPackageHidden(name: String, status: Boolean): Boolean { - val result = DPM.setApplicationHidden(DAR, name, status) + fun setPackageHidden(packages: List, status: Boolean) { + for (name in packages) { + DPM.setApplicationHidden(DAR, name, status) + } getHiddenPackages() - return result } // Uninstall blocked packages @@ -249,8 +249,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) { DPM.isUninstallBlocked(DAR, it.packageName) }.map { getAppInfo(it) } } - fun setPackageUb(name: String, status: Boolean) { - DPM.setUninstallBlocked(DAR, name, status) + fun setPackageUb(packages: List, status: Boolean) { + for (name in packages) { + DPM.setUninstallBlocked(DAR, name, status) + } getUbPackages() } @@ -263,10 +265,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } } @RequiresApi(30) - fun setPackageUcd(name: String, status: Boolean) { + fun setPackageUcd(packages: List, status: Boolean) { DPM.setUserControlDisabledPackages( DAR, - ucdPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) } + ucdPackages.value.map { it.name }.run { + if (status) plus(packages) else minus(packages) + } ) getUcdPackages() } @@ -296,12 +300,13 @@ class MyViewModel(application: Application): AndroidViewModel(application) { mddPackages.value = DPM.getMeteredDataDisabledPackages(DAR).distinct().map { getAppInfo(it) } } @RequiresApi(28) - fun setPackageMdd(name: String, status: Boolean): Boolean { - val result = DPM.setMeteredDataDisabledPackages( - DAR, mddPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) } + fun setPackageMdd(packages: List, status: Boolean) { + DPM.setMeteredDataDisabledPackages( + DAR, mddPackages.value.map { it.name }.run { + if (status) plus(packages) else minus(packages) + } ) getMddPackages() - return result.isEmpty() } // Keep uninstalled packages @@ -311,9 +316,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) { kuPackages.value = DPM.getKeepUninstalledPackages(DAR)?.distinct()?.map { getAppInfo(it) } ?: emptyList() } @RequiresApi(28) - fun setPackageKu(name: String, status: Boolean) { + fun setPackageKu(packages: List, status: Boolean) { DPM.setKeepUninstalledPackages( - DAR, kuPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) } + DAR, kuPackages.value.map { it.name }.run { + if (status) plus(packages) else minus(packages) + } ) getKuPackages() } @@ -325,10 +332,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) { cpPackages.value = DPM.getCrossProfilePackages(DAR).map { getAppInfo(it) } } @RequiresApi(30) - fun setPackageCp(name: String, status: Boolean) { + fun setPackageCp(packages: List, status: Boolean) { DPM.setCrossProfilePackages( DAR, - cpPackages.value.map { it.name }.toSet().run { if (status) plus(name) else minus(name) } + cpPackages.value.map { it.name }.toSet().run { + if (status) plus(packages) else minus(packages) + } ) getCpPackages() } @@ -338,14 +347,15 @@ class MyViewModel(application: Application): AndroidViewModel(application) { fun getCpwProviders() { cpwProviders.value = DPM.getCrossProfileWidgetProviders(DAR).distinct().map { getAppInfo(it) } } - fun setCpwProvider(name: String, status: Boolean): Boolean { - val result = if (status) { - DPM.addCrossProfileWidgetProvider(DAR, name) - } else { - DPM.removeCrossProfileWidgetProvider(DAR, name) + fun setCpwProvider(packages: List, status: Boolean) { + for (name in packages) { + if (status) { + DPM.addCrossProfileWidgetProvider(DAR, name) + } else { + DPM.removeCrossProfileWidgetProvider(DAR, name) + } } getCpwProviders() - return result } @RequiresApi(28) @@ -401,9 +411,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) { policy.policyType } ?: -1 } - fun setCmPackage(name: String, status: Boolean) { - cmPackages.update { list -> - if (status) list + getAppInfo(name) else list.filter { it.name != name } + fun setCmPackage(packages: List, status: Boolean) { + cmPackages.update { + updateAppInfoList(it, packages, status) } } @RequiresApi(34) @@ -414,6 +424,16 @@ class MyViewModel(application: Application): AndroidViewModel(application) { getCmPolicy() } + fun updateAppInfoList( + origin: List, input: List, status: Boolean + ): List { + return if (status) { + origin + input.map { getAppInfo(it) } + } else { + origin.filter { it.name !in input } + } + } + // Permitted input method val pimPackages = MutableStateFlow(emptyList()) fun getPimPackages(): Boolean { @@ -422,9 +442,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) { packages == null } } - fun setPimPackage(name: String, status: Boolean) { - pimPackages.update { packages -> - if (status) packages + getAppInfo(name) else packages.filter { it.name != name } + fun setPimPackage(packages: List, status: Boolean) { + pimPackages.update { + updateAppInfoList(it, packages, status) } } fun setPimPolicy(allowAll: Boolean): Boolean { @@ -442,9 +462,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) { packages == null } } - fun setPasPackage(name: String, status: Boolean) { - pasPackages.update { packages -> - if (status) packages + getAppInfo(name) else packages.filter { it.name != name } + fun setPasPackage(packages: List, status: Boolean) { + pasPackages.update { + updateAppInfoList(it, packages, status) } } fun setPasPolicy(allowAll: Boolean): Boolean { diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index 271e234..8c58ff1 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -6,7 +6,9 @@ import android.graphics.drawable.Drawable import android.os.Build import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -22,8 +24,14 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator @@ -37,6 +45,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -46,8 +55,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -67,17 +78,20 @@ data class AppInfo( private fun searchInString(query: String, content: String) = query.split(' ').all { content.contains(it, true) } -@Serializable data class ApplicationsList(val canSwitchView: Boolean) +@Serializable data class ApplicationsList(val canSwitchView: Boolean, val multiSelect: Boolean) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun AppChooserScreen( - canSwitchView: Boolean, packageList: MutableStateFlow>, + params: ApplicationsList, packageList: MutableStateFlow>, refreshProgress: MutableStateFlow, onChoosePackage: (String?) -> Unit, - onSwitchView: () -> Unit, onRefresh: () -> Unit + onSwitchView: () -> Unit, onRefresh: () -> Unit, + setPackagesSuspend: (List, Boolean) -> Unit, + setPackagesHidden: (List, Boolean) -> Unit, ) { val packages by packageList.collectAsStateWithLifecycle() val context = LocalContext.current + val hf = LocalHapticFeedback.current val progress by refreshProgress.collectAsStateWithLifecycle() var system by rememberSaveable { mutableStateOf(false) } var query by rememberSaveable { mutableStateOf("") } @@ -86,6 +100,7 @@ fun AppChooserScreen( system == (it.flags and ApplicationInfo.FLAG_SYSTEM != 0) && (query.isEmpty() || (searchInString(query, it.label) || searchInString(query, it.name))) } + val selectedPackages = remember { mutableStateListOf() } val focusMgr = LocalFocusManager.current LaunchedEffect(Unit) { if(packages.size <= 1) onRefresh() @@ -102,18 +117,86 @@ fun AppChooserScreen( system = !system context.popToast(if(system) R.string.show_system_app else R.string.show_user_app) }) { - Icon(painter = painterResource(R.drawable.filter_alt_fill0), contentDescription = null) + Icon(painterResource(R.drawable.filter_alt_fill0), null) } - IconButton(onRefresh, enabled = progress == 1F) { - Icon(painter = painterResource(R.drawable.refresh_fill0), contentDescription = null) + if (selectedPackages.isEmpty()) { + IconButton(onRefresh, enabled = progress == 1F) { + Icon(Icons.Default.Refresh, null) + } + if (params.canSwitchView) IconButton(onSwitchView) { + Icon(Icons.AutoMirrored.Default.List, null) + } } - if (canSwitchView) IconButton(onSwitchView) { - Icon(Icons.AutoMirrored.Default.List, null) + } + if (selectedPackages.isNotEmpty()) { + if (params.canSwitchView) { + var dropdown by remember { mutableStateOf(false) } + Box { + IconButton({ + dropdown = !dropdown + }) { + Icon(Icons.Default.MoreVert, null) + } + DropdownMenu(dropdown, { dropdown = false }) { + if (Build.VERSION.SDK_INT >= 24) { + DropdownMenuItem( + { Text(stringResource(R.string.suspend)) }, + { + setPackagesSuspend(selectedPackages.map { it.name }, true) + dropdown = false + selectedPackages.clear() + }, + leadingIcon = { + Icon(painterResource(R.drawable.block_fill0), null) + } + ) + DropdownMenuItem( + { Text(stringResource(R.string.unsuspend)) }, + { + setPackagesSuspend(selectedPackages.map { it.name }, false) + dropdown = false + selectedPackages.clear() + }, + leadingIcon = { + Icon(painterResource(R.drawable.enable_fill0), null) + } + ) + } + DropdownMenuItem( + { Text(stringResource(R.string.hide)) }, + { + setPackagesHidden(selectedPackages.map { it.name }, true) + dropdown = false + selectedPackages.clear() + }, + leadingIcon = { + Icon(painterResource(R.drawable.visibility_off_fill0), null) + } + ) + DropdownMenuItem( + { Text(stringResource(R.string.unhide)) }, + { + setPackagesHidden(selectedPackages.map { it.name }, false) + dropdown = false + selectedPackages.clear() + }, + leadingIcon = { + Icon(painterResource(R.drawable.visibility_fill0), null) + } + ) + } + } + } else { + FilledIconButton({ + onChoosePackage(selectedPackages.joinToString("\n") { it.name }) + }) { + Icon(Icons.Default.Check, null) + } } } }, title = { - if(searchMode) { + if (searchMode) { val fr = remember { FocusRequester() } LaunchedEffect(Unit) { fr.requestFocus() } OutlinedTextField( @@ -133,6 +216,10 @@ fun AppChooserScreen( textStyle = typography.bodyLarge, modifier = Modifier.fillMaxWidth().focusRequester(fr) ) + } else { + if (selectedPackages.isNotEmpty()) { + Text(selectedPackages.size.toString()) + } } }, navigationIcon = { @@ -154,10 +241,24 @@ fun AppChooserScreen( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickable { - focusMgr.clearFocus() - onChoosePackage(it.name) - } + .combinedClickable(onLongClick = { + if (params.multiSelect) { + selectedPackages += it + hf.performHapticFeedback(HapticFeedbackType.LongPress) + } + }, onClick = { + if (selectedPackages.isEmpty()) { + focusMgr.clearFocus() + onChoosePackage(it.name) + } else { + if (it in selectedPackages) selectedPackages -= it + else selectedPackages += it + } + }) + .background( + if (it in selectedPackages) MaterialTheme.colorScheme.primaryContainer + else MaterialTheme.colorScheme.background + ) .padding(horizontal = 8.dp, vertical = 10.dp) .animateItem() ) { diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 8dd3858..c545970 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -177,3 +177,5 @@ fun registerPackageRemovedReceiver( filter.addDataScheme("package") ctx.registerReceiver(br, filter) } + +fun parsePackageNames(input: String) = input.split('\n').filter { it.isNotEmpty() } 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 235516b..02d1426 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -107,6 +107,7 @@ import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.R import com.bintianqi.owndroid.adaptiveInsets +import com.bintianqi.owndroid.parsePackageNames import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem import com.bintianqi.owndroid.ui.FunctionItem @@ -130,13 +131,18 @@ val String.isValidPackageName @Composable fun LazyItemScope.ApplicationItem(info: AppInfo, onClear: () -> Unit) { Row( - Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp).animateItem(), + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 6.dp) + .animateItem(), Arrangement.SpaceBetween, Alignment.CenterVertically ) { Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) { Image( painter = rememberDrawablePainter(info.icon), contentDescription = null, - modifier = Modifier.padding(start = 12.dp, end = 18.dp).size(30.dp) + modifier = Modifier + .padding(start = 12.dp, end = 18.dp) + .size(30.dp) ) Column { Text(info.label) @@ -156,7 +162,9 @@ fun PackageNameTextField( ) { val fm = LocalFocusManager.current OutlinedTextField( - value, onValueChange, Modifier.fillMaxWidth().then(modifier), + value, onValueChange, Modifier + .fillMaxWidth() + .then(modifier), label = { Text(stringResource(R.string.package_name)) }, trailingIcon = { IconButton(onChoosePackage) { @@ -273,10 +281,14 @@ fun ApplicationDetailsScreen( if (VERSION.SDK_INT >= 23) vm.getAppRestrictions(packageName) } MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) { - Column(Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier + .align(Alignment.CenterHorizontally) + .padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { Image(rememberDrawablePainter(info.icon), null, Modifier.size(50.dp)) Text(info.label, Modifier.padding(top = 4.dp)) - Text(info.name, Modifier.alpha(0.7F).padding(bottom = 8.dp), style = typography.bodyMedium) + Text(info.name, Modifier + .alpha(0.7F) + .padding(bottom = 8.dp), style = typography.bodyMedium) } FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager(packageName)) } if(VERSION.SDK_INT >= 24) SwitchItem( @@ -401,7 +413,7 @@ fun PermissionsManagerScreen( Modifier .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) - .background(if(selected) colorScheme.primaryContainer else Color.Transparent) + .background(if (selected) colorScheme.primaryContainer else Color.Transparent) .clickable { changeState(status) } .padding(vertical = 16.dp, horizontal = 12.dp), Arrangement.SpaceBetween, Alignment.CenterVertically, @@ -609,14 +621,16 @@ fun InstallExistingAppScreen( fun CredentialManagerPolicyScreen( chosenPackage: Channel, onChoosePackage: () -> Unit, cmPackages: MutableStateFlow>, getCmPolicy: () -> Int, - setCmPackage: (String, Boolean) -> Unit, setCmPolicy: (Int) -> Unit, onNavigateUp: () -> Unit + setCmPackage: (List, Boolean) -> Unit, setCmPolicy: (Int) -> Unit, + onNavigateUp: () -> Unit ) { val context = LocalContext.current var policy by rememberSaveable { mutableIntStateOf(getCmPolicy()) } val packages by cmPackages.collectAsStateWithLifecycle() - var packageName by rememberSaveable { mutableStateOf("") } + var input by rememberSaveable { mutableStateOf("") } + val inputPackages = parsePackageNames(input) LaunchedEffect(Unit) { - packageName = chosenPackage.receive() + input = chosenPackage.receive() } MyLazyScaffold(R.string.credential_manager_policy, onNavigateUp) { item { @@ -631,20 +645,20 @@ fun CredentialManagerPolicyScreen( Spacer(Modifier.padding(vertical = 4.dp)) } if (policy != -1) items(packages, { it.name }) { - ApplicationItem(it) { setCmPackage(it.name, false) } + ApplicationItem(it) { setCmPackage(listOf(it.name), false) } } item { Column(Modifier.padding(horizontal = HorizontalPadding)) { if (policy != -1) { - PackageNameTextField(packageName, onChoosePackage, - Modifier.padding(vertical = 8.dp)) { packageName = it } + PackageNameTextField(input, onChoosePackage, + Modifier.padding(vertical = 8.dp)) { input = it } Button( { - setCmPackage(packageName, true) - packageName = "" + setCmPackage(inputPackages, true) + input = "" }, Modifier.fillMaxWidth(), - enabled = packageName.isValidPackageName + inputPackages.all { it.isValidPackageName } ) { Text(stringResource(R.string.add)) } @@ -672,33 +686,37 @@ fun CredentialManagerPolicyScreen( fun PermittedAsAndImPackages( title: Int, note: Int, chosenPackage: Channel, onChoosePackage: () -> Unit, packagesState: MutableStateFlow>, getPackages: () -> Boolean, - setPackage: (String, Boolean) -> Unit, setPolicy: (Boolean) -> Boolean, onNavigateUp: () -> Unit + setPackage: (List, Boolean) -> Unit, setPolicy: (Boolean) -> Boolean, + onNavigateUp: () -> Unit ) { val context = LocalContext.current val packages by packagesState.collectAsStateWithLifecycle() - var packageName by rememberSaveable { mutableStateOf("") } + var input by rememberSaveable { mutableStateOf("") } + val inputPackages = parsePackageNames(input) var allowAll by rememberSaveable { mutableStateOf(getPackages()) } LaunchedEffect(Unit) { - packageName = chosenPackage.receive() + input = chosenPackage.receive() } MyLazyScaffold(title, onNavigateUp) { item { SwitchItem(R.string.allow_all, state = allowAll, onCheckedChange = { allowAll = it }) } if (!allowAll) items(packages, { it.name }) { - ApplicationItem(it) { setPackage(it.name, false) } + ApplicationItem(it) { setPackage(listOf(it.name), false) } } item { if (!allowAll) { - PackageNameTextField(packageName, onChoosePackage, - Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } + PackageNameTextField(input, onChoosePackage, + Modifier.padding(HorizontalPadding, 8.dp)) { input = it } Button( { - setPackage(packageName, true) - packageName = "" + setPackage(inputPackages, true) + input = "" }, - Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), - packageName.isValidPackageName + Modifier + .fillMaxWidth() + .padding(horizontal = HorizontalPadding), + inputPackages.all { it.isValidPackageName } ) { Text(stringResource(R.string.add)) } @@ -707,7 +725,10 @@ fun PermittedAsAndImPackages( { context.showOperationResultToast(setPolicy(allowAll)) }, - Modifier.fillMaxWidth().padding(top = 8.dp).padding(horizontal = HorizontalPadding) + Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .padding(horizontal = HorizontalPadding) ) { Text(stringResource(R.string.apply)) } @@ -777,35 +798,23 @@ fun SetDefaultDialerScreen( } } -@Composable -fun PackageFunctionScreenWithoutResult( - title: Int, packagesState: MutableStateFlow>, onGet: () -> Unit, - onSet: (String, Boolean) -> Unit, onNavigateUp: () -> Unit, - chosenPackage: Channel, onChoosePackage: () -> Unit, - navigateToGroups: () -> Unit, appGroups: StateFlow>, notes: Int? = null -) { - PackageFunctionScreen( - title, packagesState, onGet, { name, status -> onSet(name, status); null }, - onNavigateUp, chosenPackage, onChoosePackage, navigateToGroups, appGroups, notes - ) -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun PackageFunctionScreen( title: Int, packagesState: MutableStateFlow>, onGet: () -> Unit, - onSet: (String, Boolean) -> Boolean?, onNavigateUp: () -> Unit, + onSet: (List, Boolean) -> Unit, onNavigateUp: () -> Unit, chosenPackage: Channel, onChoosePackage: () -> Unit, navigateToGroups: () -> Unit, appGroups: StateFlow>, notes: Int? = null ) { val groups by appGroups.collectAsStateWithLifecycle() val packages by packagesState.collectAsStateWithLifecycle() - var packageName by rememberSaveable { mutableStateOf("") } + var input by rememberSaveable { mutableStateOf("") } + val inputPackages = parsePackageNames(input) var dialog by remember { mutableStateOf(false) } var selectedGroup by remember { mutableStateOf(null) } LaunchedEffect(Unit) { onGet() - packageName = chosenPackage.receive() + input = chosenPackage.receive() } Scaffold( topBar = { @@ -848,21 +857,23 @@ fun PackageFunctionScreen( LazyColumn(Modifier.padding(paddingValues)) { items(packages, { it.name }) { ApplicationItem(it) { - onSet(it.name, false) + onSet(listOf(it.name), false) } } item { - PackageNameTextField(packageName, onChoosePackage, - Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } + PackageNameTextField(input, onChoosePackage, + Modifier.padding(HorizontalPadding, 8.dp)) { input = it } Button( { - if (onSet(packageName, true) != false) { - packageName = "" - } + onSet(inputPackages, true) + input = "" }, - Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 10.dp), - packageName.isValidPackageName && - packages.find { it.name == packageName } == null + Modifier + .fillMaxWidth() + .padding(horizontal = HorizontalPadding) + .padding(bottom = 10.dp), + inputPackages.all { it.isValidPackageName } && + packages.none { it.name in inputPackages } ) { Text(stringResource(R.string.add)) } @@ -875,17 +886,13 @@ fun PackageFunctionScreen( text = { Column { Button({ - selectedGroup!!.apps.forEach { - onSet(it, true) - } + onSet(selectedGroup!!.apps, true) dialog = false }) { Text(stringResource(R.string.add_to_list)) } Button({ - selectedGroup!!.apps.forEach { - onSet(it, false) - } + onSet(selectedGroup!!.apps, false) dialog = false }) { Text(stringResource(R.string.remove_from_list)) @@ -930,9 +937,12 @@ fun ManageAppGroupsScreen( LazyColumn(Modifier.padding(paddingValues)) { items(groups, { it.id }) { Column( - Modifier.fillMaxWidth().clickable { - navigateToEditScreen(it.id, it.name, it.apps) - }.padding(HorizontalPadding, 8.dp) + Modifier + .fillMaxWidth() + .clickable { + navigateToEditScreen(it.id, it.name, it.apps) + } + .padding(HorizontalPadding, 8.dp) ) { Text(it.name) Text( @@ -957,9 +967,10 @@ fun EditAppGroupScreen( var name by rememberSaveable { mutableStateOf(params.name) } val list = rememberSaveable { mutableStateListOf(*params.apps.toTypedArray()) } val appInfoList = list.map { getAppInfo(it) } - var packageName by rememberSaveable { mutableStateOf("") } + var input by rememberSaveable { mutableStateOf("") } + val inputPackages = parsePackageNames(input) LaunchedEffect(Unit) { - packageName = chosenPackage.receive() + input = chosenPackage.receive() } Scaffold( topBar = { @@ -992,7 +1003,9 @@ fun EditAppGroupScreen( LazyColumn(Modifier.padding(paddingValues)) { item { OutlinedTextField( - name, { name = it }, Modifier.fillMaxWidth().padding(HorizontalPadding, 8.dp), + name, { name = it }, Modifier + .fillMaxWidth() + .padding(HorizontalPadding, 8.dp), label = { Text(stringResource(R.string.name)) } ) } @@ -1002,15 +1015,18 @@ fun EditAppGroupScreen( } } item { - PackageNameTextField(packageName, onChoosePackage, - Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } + PackageNameTextField(input, onChoosePackage, + Modifier.padding(HorizontalPadding, 8.dp)) { input = it } Button( { - list += packageName - packageName = "" + list += inputPackages + input = "" }, - Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 10.dp), - packageName.isValidPackageName && packageName !in list + Modifier + .fillMaxWidth() + .padding(horizontal = HorizontalPadding) + .padding(bottom = 10.dp), + inputPackages.all { it.isValidPackageName && it !in list } ) { Text(stringResource(R.string.add)) } @@ -1053,7 +1069,9 @@ fun ManagedConfigurationScreen( } OutlinedTextField( searchKeyword, { searchKeyword = it }, - Modifier.fillMaxWidth().focusRequester(fr), + Modifier + .fillMaxWidth() + .focusRequester(fr), textStyle = typography.bodyLarge, placeholder = { Text(stringResource(R.string.search)) }, trailingIcon = { @@ -1092,9 +1110,12 @@ fun ManagedConfigurationScreen( LazyColumn(Modifier.padding(paddingValues)) { items(displayRestrictions, { it.key }) { entry -> Row( - Modifier.fillMaxWidth().clickable { - dialog = entry - }.padding(HorizontalPadding, 8.dp), + Modifier + .fillMaxWidth() + .clickable { + dialog = entry + } + .padding(HorizontalPadding, 8.dp), verticalAlignment = Alignment.CenterVertically ) { val iconId = when (entry) { @@ -1239,7 +1260,9 @@ fun ManagedConfigurationDialog( } } Row( - Modifier.fillMaxWidth().padding(bottom = 4.dp), + Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), Arrangement.SpaceBetween, Alignment.CenterVertically ) { Text(stringResource(R.string.specify_value)) @@ -1277,9 +1300,12 @@ fun ManagedConfigurationDialog( is AppRestriction.ChoiceItem -> itemsIndexed(restriction.entryValues) { index, value -> val label = restriction.entries.getOrNull(index) Row( - Modifier.fillMaxWidth().clickable { - input = value - }.padding(8.dp, 4.dp) + Modifier + .fillMaxWidth() + .clickable { + input = value + } + .padding(8.dp, 4.dp) ) { RadioButton(input == value, { input = value }) Spacer(Modifier.width(8.dp)) @@ -1298,10 +1324,13 @@ fun ManagedConfigurationDialog( ) { index, entry -> ReorderableItem(reorderableListState, entry.value) { Row( - Modifier.fillMaxWidth().clickable { - val old = multiSelectList[index] - multiSelectList[index] = old.copy(selected = !old.selected) - }.padding(8.dp, 4.dp), + Modifier + .fillMaxWidth() + .clickable { + val old = multiSelectList[index] + multiSelectList[index] = old.copy(selected = !old.selected) + } + .padding(8.dp, 4.dp), Arrangement.SpaceBetween, Alignment.CenterVertically ) { Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) { @@ -1325,7 +1354,9 @@ fun ManagedConfigurationDialog( } } item { - Row(Modifier.fillMaxWidth().padding(top = 4.dp), Arrangement.End) { + Row(Modifier + .fillMaxWidth() + .padding(top = 4.dp), Arrangement.End) { TextButton({ setRestriction(null) }, Modifier.padding(end = 4.dp)) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 64bcbab..c6eaa0c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -1147,7 +1147,7 @@ fun NearbyStreamingPolicyScreen( @RequiresApi(28) @Composable fun LockTaskModeScreen( - chosenPackage: Channel, onChoosePackage: () -> Unit, + chosenPackage: Channel, chooseSinglePackage: () -> Unit, choosePackage: () -> Unit, lockTaskPackages: StateFlow>, getLockTaskPackages: () -> Unit, setLockTaskPackage: (String, Boolean) -> Unit, startLockTaskMode: (String, String, Boolean, Boolean) -> Boolean, @@ -1191,9 +1191,9 @@ fun LockTaskModeScreen( } HorizontalPager(pagerState, verticalAlignment = Alignment.Top) { page -> if(page == 0) { - StartLockTaskMode(startLockTaskMode, chosenPackage, onChoosePackage) + StartLockTaskMode(startLockTaskMode, chosenPackage, chooseSinglePackage) } else if (page == 1) { - LockTaskPackages(chosenPackage, onChoosePackage, lockTaskPackages, setLockTaskPackage) + LockTaskPackages(chosenPackage, choosePackage, lockTaskPackages, setLockTaskPackage) } else { LockTaskFeatures(getLockTaskFeatures, setLockTaskFeature) } @@ -1231,7 +1231,7 @@ private fun StartLockTaskMode( R.string.lock_task_mode_start_clear_task, clearTask ) { clearTask = it } FullWidthCheckBoxItem( - R.string.lock_taso_mode_show_notification, showNotification + R.string.lock_task_mode_show_notification, showNotification ) { showNotification = it } Row( Modifier diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3b95803..1be5e09 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -174,7 +174,7 @@ 在足够安全时启用 锁定任务模式 清除任务(新实例) - 显示通知以退出 + 显示通知以退出 应用未被允许 禁用全部 允许状态栏信息 @@ -344,7 +344,9 @@ 显示用户应用 显示系统应用 挂起 + 取消挂起 隐藏 + 取消隐藏 VPN保持打开 启用锁定 清除当前配置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9add4f7..8ac92ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -202,7 +202,7 @@ Same managed account only Lock task mode Clear task (start fresh) - Show a notification to exit + Show a notification to exit App is not allowed Disable all @@ -378,7 +378,9 @@ Show user apps Show system apps Suspend + Unsuspend Hide + Unhide Always-on VPN Enable lockdown Clear current config diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1090da1..14feb9a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -agp = "8.13.1" -kotlin = "2.2.21" +agp = "8.13.2" +kotlin = "2.3.0" navigation-compose = "2.9.6" -composeBom = "2025.11.00" +composeBom = "2025.12.01" accompanist-drawablepainter = "0.37.3" accompanist-permissions = "0.37.3" shizuku = "13.1.5" @@ -42,4 +42,4 @@ serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.2.21" } +serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.3.0" } From 20a422ba51edcda8d1b2b624150e122641f27b60 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 27 Dec 2025 14:45:09 +0800 Subject: [PATCH 16/20] feat: a snackbar to undo removal in PackageFunctionScreen (#216) Remove redundant SDK version check --- .../owndroid/AppInstallerActivity.kt | 1 + .../owndroid/AppInstallerViewModel.kt | 18 +++--- .../com/bintianqi/owndroid/MainActivity.kt | 4 -- .../com/bintianqi/owndroid/MyViewModel.kt | 62 ++++++------------- .../bintianqi/owndroid/dpm/Applications.kt | 31 ++++++++-- .../com/bintianqi/owndroid/dpm/Network.kt | 5 +- .../com/bintianqi/owndroid/dpm/Password.kt | 10 ++- .../java/com/bintianqi/owndroid/dpm/System.kt | 22 +++---- .../java/com/bintianqi/owndroid/dpm/Users.kt | 39 ++++++------ .../com/bintianqi/owndroid/dpm/WorkProfile.kt | 48 +++++++------- app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 12 files changed, 115 insertions(+), 129 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt index ca6b948..7fbb400 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt @@ -16,6 +16,7 @@ class AppInstallerActivity:FragmentActivity() { super.onCreate(savedInstanceState) val vm by viewModels() vm.initialize(intent) + vm.registerInstallerReceiver(this) val theme = ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme) setContent { OwnDroidTheme(theme) { diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt index db89514..75cc03c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt @@ -36,13 +36,19 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { list += it } intent.getParcelableArrayExtra(Intent.EXTRA_STREAM)?.forEach { list += it as Uri } intent.clipData?.let { clipData -> - for(i in 0..clipData.itemCount - 1) { + for(i in 0..) { uiState.update { it.copy(packages = it.packages.plus(packages).distinct()) @@ -93,17 +99,14 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat uiState.update { it.copy(installing = false, packageWriting = -1) } return } - ContextCompat.registerReceiver( - application, Receiver(), IntentFilter(ACTION), null, - null, ContextCompat.RECEIVER_EXPORTED - ) + val intent = Intent(ACTION).setPackage(application.packageName) val pi = if(Build.VERSION.SDK_INT >= 34) { PendingIntent.getBroadcast( - application, sessionId, Intent(ACTION), + application, sessionId, intent, PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE ).intentSender } else { - PendingIntent.getBroadcast(application, sessionId, Intent(ACTION), PendingIntent.FLAG_MUTABLE).intentSender + PendingIntent.getBroadcast(application, sessionId, intent, PendingIntent.FLAG_MUTABLE).intentSender } session.commit(pi) } @@ -119,7 +122,6 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat ) } else { uiState.update { it.copy(result = intent) } - context.unregisterReceiver(this) } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 0324052..c35d8ca 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -269,10 +269,6 @@ class MainActivity : FragmentActivity() { } } - override fun onResume() { - super.onResume() - } - } @ExperimentalMaterial3Api diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 496f61e..59f4e21 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -276,7 +276,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } val packagePermissions = MutableStateFlow(emptyMap()) - @RequiresApi(23) fun getPackagePermissions(name: String) { if (name.isValidPackageName) { packagePermissions.value = runtimePermissions.associate { @@ -286,7 +285,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { packagePermissions.value = emptyMap() } } - @RequiresApi(23) fun setPackagePermission(name: String, permission: String, status: Int): Boolean { val result = DPM.setPermissionGrantState(DAR, name, permission, status) getPackagePermissions(name) @@ -366,6 +364,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } fun uninstallPackage(packageName: String, onComplete: (String?) -> Unit) { + val action = "com.bintianqi.owndroid.action.PACKAGE_UNINSTALLED" val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999) @@ -383,16 +382,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } } ContextCompat.registerReceiver( - application, receiver, IntentFilter(AppInstallerViewModel.ACTION), null, - null, ContextCompat.RECEIVER_EXPORTED + application, receiver, IntentFilter(action), null, + null, ContextCompat.RECEIVER_NOT_EXPORTED ) + val intent = Intent(action).setPackage(application.packageName) val pi = if(VERSION.SDK_INT >= 34) { PendingIntent.getBroadcast( - application, 0, Intent(AppInstallerViewModel.ACTION), + application, 0, intent, PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE ).intentSender } else { - PendingIntent.getBroadcast(application, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender + PendingIntent.getBroadcast(application, 0, intent, PendingIntent.FLAG_MUTABLE).intentSender } application.getPackageInstaller().uninstall(packageName, pi) } @@ -545,7 +545,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { val appRestrictions = MutableStateFlow(emptyList()) - @RequiresApi(23) fun getAppRestrictions(name: String) { val rm = application.getSystemService(RestrictionsManager::class.java) try { @@ -569,7 +568,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } } - @RequiresApi(23) fun setAppRestrictions(name: String, item: AppRestriction) { viewModelScope.launch(Dispatchers.IO) { val bundle = transformAppRestriction( @@ -580,7 +578,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } } - @RequiresApi(23) fun clearAppRestrictions(name: String) { viewModelScope.launch(Dispatchers.IO) { DPM.setApplicationRestrictions(DAR, name, Bundle()) @@ -699,7 +696,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) { autoTimeRequired = if (VERSION.SDK_INT < 30) DPM.autoTimeRequired else false, masterVolumeMuted = DPM.isMasterVolumeMuted(DAR), backupServiceEnabled = if (VERSION.SDK_INT >= 26) DPM.isBackupServiceEnabled(DAR) else false, - btContactSharingDisabled = if (VERSION.SDK_INT >= 23 && privilege.work) + btContactSharingDisabled = if (privilege.work) DPM.getBluetoothContactSharingDisabled(DAR) else false, commonCriteriaMode = if (VERSION.SDK_INT >= 30 && privilege.run { device || org }) DPM.isCommonCriteriaModeEnabled(DAR) else false, @@ -718,7 +715,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { it.copy(screenCaptureDisabled = DPM.getScreenCaptureDisabled(null)) } } - @RequiresApi(23) fun setStatusBarDisabled(disabled: Boolean) { val result = DPM.setStatusBarDisabled(DAR, disabled) if (result) systemOptionsStatus.update { it.copy(statusBarDisabled = disabled) } @@ -752,7 +748,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { it.copy(backupServiceEnabled = DPM.isBackupServiceEnabled(DAR)) } } - @RequiresApi(23) fun setBtContactSharingDisabled(disabled: Boolean) { DPM.setBluetoothContactSharingDisabled(DAR, disabled) systemOptionsStatus.update { @@ -771,7 +766,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { DPM.isUsbDataSignalingEnabled = enabled systemOptionsStatus.update { it.copy(usbSignalEnabled = DPM.isUsbDataSignalingEnabled) } } - @RequiresApi(23) fun setKeyguardDisabled(disabled: Boolean): Boolean { return DPM.setKeyguardDisabled(DAR, disabled) } @@ -841,11 +835,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) { fun setContentProtectionPolicy(policy: Int) { DPM.setContentProtectionPolicy(DAR, policy) } - @RequiresApi(23) fun getPermissionPolicy(): Int { return DPM.getPermissionPolicy(DAR) } - @RequiresApi(23) fun setPermissionPolicy(policy: Int) { DPM.setPermissionPolicy(DAR, policy) } @@ -1031,14 +1023,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } } } - @RequiresApi(23) fun getSystemUpdatePolicy(): SystemUpdatePolicyInfo { val policy = DPM.systemUpdatePolicy return SystemUpdatePolicyInfo( policy?.policyType ?: -1, policy?.installWindowStart ?: 0, policy?.installWindowEnd ?: 0 ) } - @RequiresApi(23) fun setSystemUpdatePolicy(info: SystemUpdatePolicyInfo) { val policy = when (info.type) { SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC -> SystemUpdatePolicy.createAutomaticInstallPolicy() @@ -1352,28 +1342,19 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } fun createWorkProfile(options: CreateWorkProfileOptions): Intent { val intent = Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE) - if (VERSION.SDK_INT >= 23) { + intent.putExtra( + DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, + MyAdminComponent + ) + intent.putExtra( + DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE, + Account(options.accountName, options.accountType) + ) + if (VERSION.SDK_INT >= 26) { intent.putExtra( - DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, - MyAdminComponent + DevicePolicyManager.EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION, + options.keepAccount ) - } else { - intent.putExtra( - DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME, - application.packageName - ) - } - if (options.migrateAccount && VERSION.SDK_INT >= 22) { - intent.putExtra( - DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE, - Account(options.accountName, options.accountType) - ) - if (VERSION.SDK_INT >= 26) { - intent.putExtra( - DevicePolicyManager.EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION, - options.keepAccount - ) - } } if (VERSION.SDK_INT >= 24) { intent.putExtra( @@ -1440,10 +1421,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) { return UserInformation( if (VERSION.SDK_INT >= 24) UserManager.supportsMultipleUsers() else false, if (VERSION.SDK_INT >= 31) UserManager.isHeadlessSystemUserMode() else false, - if (VERSION.SDK_INT >= 23) UM.isSystemUser else false, + UM.isSystemUser, if (VERSION.SDK_INT >= 34) UM.isAdminUser else false, if (VERSION.SDK_INT >= 25) UM.isDemoUser else false, - if (VERSION.SDK_INT >= 23) UM.getUserCreationTime(uh) else 0, + UM.getUserCreationTime(uh), if (VERSION.SDK_INT >= 28) DPM.isLogoutEnabled else false, if (VERSION.SDK_INT >= 28) DPM.isEphemeralUser(DAR) else false, if (VERSION.SDK_INT >= 28) DPM.isAffiliatedUser else false, @@ -1518,7 +1499,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { fun setProfileName(name: String) { DPM.setProfileName(DAR, name) } - @RequiresApi(23) fun setUserIcon(bitmap: Bitmap) { DPM.setUserIcon(DAR, bitmap) } @@ -1656,7 +1636,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { return PM.getPackageUid(name, 0) } var networkStatsData = emptyList() - @RequiresApi(23) fun readNetworkStats(stats: NetworkStats): List { val list = mutableListOf() while (stats.hasNextBucket()) { @@ -1667,7 +1646,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) { stats.close() return list } - @RequiresApi(23) fun readNetworkStatsBucket(bucket: NetworkStats.Bucket): NetworkStatsData { return NetworkStatsData( bucket.rxBytes, bucket.rxPackets, bucket.txBytes, bucket.txPackets, 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 02d1426..cbae39f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -65,6 +65,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -78,6 +82,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -121,6 +126,7 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState @@ -213,9 +219,7 @@ fun ApplicationsFeaturesScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Un if(VERSION.SDK_INT >= 30 && (privilege.device || (VERSION.SDK_INT >= 33 && privilege.profile))) { FunctionItem(R.string.disable_user_control, icon = R.drawable.do_not_touch_fill0) { onNavigate(DisableUserControl) } } - if(VERSION.SDK_INT >= 23) { - FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager()) } - } + FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager()) } if(VERSION.SDK_INT >= 28) { FunctionItem(R.string.disable_metered_data, icon = R.drawable.money_off_fill0) { onNavigate(DisableMeteredData) } } @@ -278,7 +282,7 @@ fun ApplicationDetailsScreen( val appRestrictions by vm.appRestrictions.collectAsStateWithLifecycle() LaunchedEffect(Unit) { vm.getAppStatus(packageName) - if (VERSION.SDK_INT >= 23) vm.getAppRestrictions(packageName) + vm.getAppRestrictions(packageName) } MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) { Column(Modifier @@ -320,7 +324,7 @@ fun ApplicationDetailsScreen( state = status.keepUninstalled, onCheckedChange = { vm.adSetPackageKu(packageName, it) } ) - if (VERSION.SDK_INT >= 23 && appRestrictions.isNotEmpty()) { + if (appRestrictions.isNotEmpty()) { FunctionItem(R.string.managed_configuration, icon = R.drawable.description_fill0) { onNavigate(ManagedConfiguration(packageName)) } @@ -347,7 +351,6 @@ fun ApplicationDetailsScreen( @Serializable data class PermissionsManager(val packageName: String? = null) -@RequiresApi(23) @Composable fun PermissionsManagerScreen( packagePermissions: MutableStateFlow>, getPackagePermissions: (String) -> Unit, @@ -806,12 +809,15 @@ fun PackageFunctionScreen( chosenPackage: Channel, onChoosePackage: () -> Unit, navigateToGroups: () -> Unit, appGroups: StateFlow>, notes: Int? = null ) { + val context = LocalContext.current val groups by appGroups.collectAsStateWithLifecycle() val packages by packagesState.collectAsStateWithLifecycle() var input by rememberSaveable { mutableStateOf("") } val inputPackages = parsePackageNames(input) var dialog by remember { mutableStateOf(false) } var selectedGroup by remember { mutableStateOf(null) } + val snackbar = remember { SnackbarHostState() } + val coroutine = rememberCoroutineScope() LaunchedEffect(Unit) { onGet() input = chosenPackage.receive() @@ -852,12 +858,25 @@ fun PackageFunctionScreen( } } ) + }, + snackbarHost = { + SnackbarHost(snackbar) } ) { paddingValues -> LazyColumn(Modifier.padding(paddingValues)) { items(packages, { it.name }) { ApplicationItem(it) { onSet(listOf(it.name), false) + coroutine.launch { + val result = snackbar.showSnackbar( + context.getString(R.string.package_removed, it.name), + context.getString(R.string.undo), + true, SnackbarDuration.Short + ) + if (result == SnackbarResult.ActionPerformed) { + onSet(listOf(it.name), true) + } + } } } item { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index 8d6b2da..8723f40 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -152,7 +152,7 @@ fun NetworkScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { if(VERSION.SDK_INT >= 30) { FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { onNavigate(NetworkOptions) } } - if (VERSION.SDK_INT >= 23 && !privilege.dhizuku) + if (!privilege.dhizuku) FunctionItem(R.string.network_stats, icon = R.drawable.query_stats_fill0) { onNavigate(QueryNetworkStats) } if(VERSION.SDK_INT >= 29 && privilege.device) { FunctionItem(R.string.private_dns, icon = R.drawable.dns_fill0) { onNavigate(PrivateDns) } @@ -937,7 +937,6 @@ enum class NetworkStatsState(val id: Int, val text: Int) { Default(NetworkStats.Bucket.STATE_DEFAULT, R.string.default_str), Foreground(NetworkStats.Bucket.STATE_FOREGROUND, R.string.foreground) } -@RequiresApi(23) enum class NetworkStatsUID(val uid: Int, val text: Int) { All(NetworkStats.Bucket.UID_ALL, R.string.all), Removed(NetworkStats.Bucket.UID_REMOVED, R.string.uninstalled), @@ -952,7 +951,6 @@ data class QueryNetworkStatsParams( @Serializable object QueryNetworkStats @OptIn(ExperimentalMaterial3Api::class) -@RequiresApi(23) @Composable fun NetworkStatsScreen( chosenPackage: Channel, onChoosePackage: () -> Unit, getUid: (String) -> Int, @@ -1270,7 +1268,6 @@ data class NetworkStatsData( @Serializable object NetworkStatsViewer -@RequiresApi(23) @Composable fun NetworkStatsViewerScreen( data: List, clearData: () -> Unit, onNavigateUp: () -> Unit diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt index fea9381..1ee9166 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt @@ -333,12 +333,10 @@ fun ResetPasswordScreen(resetPassword: (String, String, Int) -> Boolean, onNavig visualTransformation = PasswordVisualTransformation() ) Spacer(Modifier.padding(vertical = 5.dp)) - if(VERSION.SDK_INT >= 23) { - CheckBoxItem( - R.string.do_not_ask_credentials_on_boot, - flags and RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT != 0 - ) { flags = flags xor RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT } - } + CheckBoxItem( + R.string.do_not_ask_credentials_on_boot, + flags and RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT != 0 + ) { flags = flags xor RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT } CheckBoxItem( R.string.reset_password_require_entry, flags and RESET_PASSWORD_REQUIRE_ENTRY != 0 diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index c6eaa0c..8f0fd09 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -171,9 +171,7 @@ fun SystemManagerScreen( FunctionItem(R.string.key_pairs, icon = R.drawable.key_vertical_fill0) { navCtrl.navigate("KeyPairs") }*/ if(VERSION.SDK_INT >= 35 && (privilege.device || (privilege.profile && privilege.affiliated))) FunctionItem(R.string.content_protection_policy, icon = R.drawable.search_fill0) { onNavigate(ContentProtectionPolicy) } - if(VERSION.SDK_INT >= 23) { - FunctionItem(R.string.permission_policy, icon = R.drawable.key_fill0) { onNavigate(PermissionPolicy) } - } + FunctionItem(R.string.permission_policy, icon = R.drawable.key_fill0) { onNavigate(PermissionPolicy) } if(VERSION.SDK_INT >= 34 && privilege.device) { FunctionItem(R.string.mte_policy, icon = R.drawable.memory_fill0) { onNavigate(MtePolicy) } } @@ -204,7 +202,7 @@ fun SystemManagerScreen( FunctionItem(R.string.support_messages, icon = R.drawable.chat_fill0) { onNavigate(SupportMessage) } } FunctionItem(R.string.disable_account_management, icon = R.drawable.account_circle_fill0) { onNavigate(DisableAccountManagement) } - if(VERSION.SDK_INT >= 23 && (privilege.device || privilege.org)) { + if (privilege.device || privilege.org) { FunctionItem(R.string.system_update_policy, icon = R.drawable.system_update_fill0) { onNavigate(SetSystemUpdatePolicy) } } if(VERSION.SDK_INT >= 29 && (privilege.device || privilege.org)) { @@ -368,7 +366,7 @@ fun SystemOptionsScreen(vm: MyViewModel, onNavigateUp: () -> Unit) { SwitchItem(R.string.enable_usb_signal, status.usbSignalEnabled, vm::setUsbSignalEnabled, R.drawable.usb_fill0) } - if (VERSION.SDK_INT >= 23 && VERSION.SDK_INT < 34) { + if (VERSION.SDK_INT < 34) { Row( Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), verticalAlignment = Alignment.CenterVertically @@ -413,8 +411,8 @@ fun KeyguardScreen( val context = LocalContext.current val privilege by Privilege.status.collectAsStateWithLifecycle() MyScaffold(R.string.keyguard, onNavigateUp) { - if (VERSION.SDK_INT >= 23 && (privilege.device || - (VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated))) { + if (privilege.device || + (VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated)) { Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() @@ -435,7 +433,7 @@ fun KeyguardScreen( Notes(R.string.info_disable_keyguard) Spacer(Modifier.padding(vertical = 12.dp)) } - if(VERSION.SDK_INT >= 23) Text(text = stringResource(R.string.lock_now), style = typography.headlineLarge) + Text(text = stringResource(R.string.lock_now), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 2.dp)) var evictKey by rememberSaveable { mutableStateOf(false) } Button( @@ -1007,7 +1005,6 @@ fun ContentProtectionPolicyScreen( @Serializable object PermissionPolicy -@RequiresApi(23) @Composable fun PermissionPolicyScreen( getPolicy: () -> Int, setPolicy: (Int) -> Unit, onNavigateUp: () -> Unit @@ -1215,7 +1212,7 @@ private fun StartLockTaskMode( var activity by rememberSaveable { mutableStateOf("") } var specifyActivity by rememberSaveable { mutableStateOf(false) } var clearTask by rememberSaveable { mutableStateOf(true) } - var showNotification by rememberSaveable() { mutableStateOf(true) } + var showNotification by rememberSaveable { mutableStateOf(true) } LaunchedEffect(Unit) { packageName = chosenPackage.receive() } @@ -1785,7 +1782,7 @@ fun WipeDataScreen( FullWidthCheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { flag = flag xor WIPE_EXTERNAL_STORAGE } - if(VERSION.SDK_INT >= 22 && privilege.device) FullWidthCheckBoxItem( + if (privilege.device) FullWidthCheckBoxItem( R.string.wipe_reset_protection_data, flag and WIPE_RESET_PROTECTION_DATA != 0) { flag = flag xor WIPE_RESET_PROTECTION_DATA } @@ -1839,7 +1836,7 @@ fun WipeDataScreen( text = { Text( text = stringResource( - if(VERSION.SDK_INT >= 23 && userManager.isSystemUser) R.string.wipe_data_warning + if (userManager.isSystemUser) R.string.wipe_data_warning else R.string.info_wipe_data_in_managed_user ), color = colorScheme.error @@ -1880,7 +1877,6 @@ data class PendingSystemUpdateInfo(val exists: Boolean, val time: Long, val secu @Serializable object SetSystemUpdatePolicy -@RequiresApi(23) @Composable fun SystemUpdatePolicyScreen( getPolicy: () -> SystemUpdatePolicyInfo, setPolicy: (SystemUpdatePolicyInfo) -> Unit, diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt index 2177563..18776a3 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt @@ -30,12 +30,12 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults @@ -105,25 +105,23 @@ fun UsersScreen(vm: MyViewModel, onNavigateUp: () -> Unit, onNavigate: (Any) -> FunctionItem(R.string.create_user, icon = R.drawable.person_add_fill0) { onNavigate(CreateUser) } } FunctionItem(R.string.change_username, icon = R.drawable.edit_fill0) { onNavigate(ChangeUsername) } - if(VERSION.SDK_INT >= 23) { - var changeUserIconDialog by remember { mutableStateOf(false) } - var bitmap: Bitmap? by remember { mutableStateOf(null) } - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { - if(it != null) uriToStream(context, it) { stream -> - bitmap = BitmapFactory.decodeStream(stream) - if(bitmap != null) changeUserIconDialog = true - } + var changeUserIconDialog by remember { mutableStateOf(false) } + var bitmap: Bitmap? by remember { mutableStateOf(null) } + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { + if(it != null) uriToStream(context, it) { stream -> + bitmap = BitmapFactory.decodeStream(stream) + if(bitmap != null) changeUserIconDialog = true } - FunctionItem(R.string.change_user_icon, icon = R.drawable.account_circle_fill0) { - context.popToast(R.string.select_an_image) - launcher.launch("image/*") - } - if (changeUserIconDialog) ChangeUserIconDialog( - bitmap!!, { - vm.setUserIcon(bitmap!!) - changeUserIconDialog = false - }) { changeUserIconDialog = false } } + FunctionItem(R.string.change_user_icon, icon = R.drawable.account_circle_fill0) { + context.popToast(R.string.select_an_image) + launcher.launch("image/*") + } + if (changeUserIconDialog) ChangeUserIconDialog( + bitmap!!, { + vm.setUserIcon(bitmap!!) + changeUserIconDialog = false + }) { changeUserIconDialog = false } if(VERSION.SDK_INT >= 28 && privilege.device) { FunctionItem(R.string.user_session_msg, icon = R.drawable.notifications_fill0) { onNavigate(UserSessionMessage) } } @@ -191,7 +189,7 @@ fun UserInfoScreen(getInfo: () -> UserInformation, onNavigateUp: () -> Unit) { if (VERSION.SDK_INT >= 24) InfoItem(R.string.support_multiuser, info.multiUser.yesOrNo) if (VERSION.SDK_INT >= 31) InfoItem(R.string.headless_system_user_mode, info.headless.yesOrNo, true) { infoDialog = 1 } Spacer(Modifier.height(8.dp)) - if (VERSION.SDK_INT >= 23) InfoItem(R.string.system_user, info.system.yesOrNo) + InfoItem(R.string.system_user, info.system.yesOrNo) if (VERSION.SDK_INT >= 34) InfoItem(R.string.admin_user, info.admin.yesOrNo) if (VERSION.SDK_INT >= 25) InfoItem(R.string.demo_user, info.demo.yesOrNo) if (info.time != 0L) InfoItem(R.string.creation_time, formatDate(info.time)) @@ -264,7 +262,7 @@ fun UserOperationScreen( input, { input = it }, Modifier .fillMaxWidth() - .menuAnchor(MenuAnchorType.PrimaryEditable) + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable) .padding(top = 4.dp, bottom = 8.dp), label = { Text(stringResource(if(useUserId) R.string.user_id else R.string.serial_number)) @@ -586,7 +584,6 @@ fun UserSessionMessageScreen( } } -@RequiresApi(23) @Composable private fun ChangeUserIconDialog(bitmap: Bitmap, onSet: () -> Unit, onClose: () -> Unit) { AlertDialog( diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt index 180deed..beb3cb3 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt @@ -101,31 +101,29 @@ fun CreateWorkProfileScreen( var migrateAccountName by remember { mutableStateOf("") } var migrateAccountType by remember { mutableStateOf("") } var keepAccount by remember { mutableStateOf(true) } - if (VERSION.SDK_INT >= 22) { - FullWidthCheckBoxItem(R.string.migrate_account, migrateAccount) { migrateAccount = it } - AnimatedVisibility(migrateAccount) { - val fr = FocusRequester() - Column(modifier = Modifier.padding(start = 10.dp)) { - OutlinedTextField( - value = migrateAccountName, onValueChange = { migrateAccountName = it }, - label = { Text(stringResource(R.string.account_name)) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - keyboardActions = KeyboardActions { fr.requestFocus() }, - modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding) - ) - OutlinedTextField( - value = migrateAccountType, onValueChange = { migrateAccountType = it }, - label = { Text(stringResource(R.string.account_type)) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions { focusMgr.clearFocus() }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = HorizontalPadding) - .focusRequester(fr) - ) - if(VERSION.SDK_INT >= 26) { - FullWidthCheckBoxItem(R.string.keep_account, keepAccount) { keepAccount = it } - } + FullWidthCheckBoxItem(R.string.migrate_account, migrateAccount) { migrateAccount = it } + AnimatedVisibility(migrateAccount) { + val fr = FocusRequester() + Column(modifier = Modifier.padding(start = 10.dp)) { + OutlinedTextField( + value = migrateAccountName, onValueChange = { migrateAccountName = it }, + label = { Text(stringResource(R.string.account_name)) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions { fr.requestFocus() }, + modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding) + ) + OutlinedTextField( + value = migrateAccountType, onValueChange = { migrateAccountType = it }, + label = { Text(stringResource(R.string.account_type)) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions { focusMgr.clearFocus() }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = HorizontalPadding) + .focusRequester(fr) + ) + if(VERSION.SDK_INT >= 26) { + FullWidthCheckBoxItem(R.string.keep_account, keepAccount) { keepAccount = it } } } } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1be5e09..cf5b18f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -68,6 +68,7 @@ 超时 继续 退出 + 撤销 Profile owner @@ -381,6 +382,7 @@ 托管配置 清除配置 指定值 + 移除了 %1$s 用户限制 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ac92ed..9df041d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,6 +73,7 @@ Timeout Continue Exit + Undo Profile owner @@ -415,6 +416,7 @@ Managed configuration Clear configurations Specify value + Removed %1$s User restriction From 2b58e56bbfa6cec1bbf2c95eee6ca2f40daf729b Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 3 Jan 2026 23:43:15 +0800 Subject: [PATCH 17/20] fix: various bugs (#223, #221, #217) --- .github/workflows/build.yml | 9 +----- .../com/bintianqi/owndroid/MyViewModel.kt | 29 ++++++++++++------- .../com/bintianqi/owndroid/PackageChooser.kt | 2 +- .../com/bintianqi/owndroid/dpm/Password.kt | 4 ++- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06b7c4e..5eb926a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,12 +72,6 @@ jobs: with: path: artifacts - - name: Download telegram-bot-api - run: | - mkdir ./binaries - wget "https://github.com/jakbin/telegram-bot-api-binary/releases/download/latest/telegram-bot-api" -O ./binaries/telegram-bot-api - chmod +x ./binaries/telegram-bot-api - - name: Start API Server & Upload env: COMMIT_MESSAGE: |+ @@ -91,8 +85,7 @@ jobs: mv ./$RELEASE_TEST_PWD/app-release.apk ./$RELEASE_TEST_PWD.apk && rm -rf ./$RELEASE_TEST_PWD export RELEASE_SIGNED_PWD=$(find . -name "*release-signed*") mv ./$RELEASE_SIGNED_PWD/app-release.apk ./$RELEASE_SIGNED_PWD.apk && rm -rf ./$RELEASE_SIGNED_PWD - ../binaries/telegram-bot-api --api-id=${{ secrets.TELEGRAM_API_APP_ID }} --api-hash=${{ secrets.TELEGRAM_API_HASH }} --local 2>&1 > /dev/null & export token=${{ secrets.TELEGRAM_BOT_KEY }} - curl -v "http://127.0.0.1:8081/bot$token/sendMediaGroup?chat_id=-1002203528169&media=%5B%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseTest%22%7D%2C%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseSigned%22%2C%22parse_mode%22%3A%22HTML%22%2C%22caption%22%3A${ESCAPED}%7D%5D" \ + curl -v "http://api.telegram.org/bot$token/sendMediaGroup?chat_id=-1002203528169&media=%5B%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseTest%22%7D%2C%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseSigned%22%2C%22parse_mode%22%3A%22HTML%22%2C%22caption%22%3A${ESCAPED}%7D%5D" \ -F releaseTest="@$RELEASE_TEST_PWD.apk" \ -F releaseSigned="@$RELEASE_SIGNED_PWD.apk" diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 59f4e21..1790e22 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -689,16 +689,16 @@ class MyViewModel(application: Application): AndroidViewModel(application) { statusBarDisabled = if (VERSION.SDK_INT >= 34 && privilege.run { device || (profile && affiliated) }) DPM.isStatusBarDisabled else false, - autoTimeEnabled = if (VERSION.SDK_INT >= 30 && privilege.run { device || org }) + autoTimeEnabled = if (VERSION.SDK_INT >= 30 && (privilege.device || privilege.org)) DPM.getAutoTimeEnabled(DAR) else false, - autoTimeZoneEnabled = if (VERSION.SDK_INT >= 30 && privilege.run { device || org }) + autoTimeZoneEnabled = if (VERSION.SDK_INT >= 30 && (privilege.device || privilege.org)) DPM.getAutoTimeZoneEnabled(DAR) else false, autoTimeRequired = if (VERSION.SDK_INT < 30) DPM.autoTimeRequired else false, masterVolumeMuted = DPM.isMasterVolumeMuted(DAR), backupServiceEnabled = if (VERSION.SDK_INT >= 26) DPM.isBackupServiceEnabled(DAR) else false, btContactSharingDisabled = if (privilege.work) DPM.getBluetoothContactSharingDisabled(DAR) else false, - commonCriteriaMode = if (VERSION.SDK_INT >= 30 && privilege.run { device || org }) + commonCriteriaMode = if (VERSION.SDK_INT >= 30 && (privilege.device || privilege.org)) DPM.isCommonCriteriaModeEnabled(DAR) else false, usbSignalEnabled = if (VERSION.SDK_INT >= 31) DPM.isUsbDataSignalingEnabled else false, canDisableUsbSignal = if (VERSION.SDK_INT >= 31) DPM.canUsbDataSignalingBeDisabled() else false @@ -1346,15 +1346,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) { DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, MyAdminComponent ) - intent.putExtra( - DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE, - Account(options.accountName, options.accountType) - ) - if (VERSION.SDK_INT >= 26) { + if (options.migrateAccount) { intent.putExtra( - DevicePolicyManager.EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION, - options.keepAccount + DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE, + Account(options.accountName, options.accountType) ) + if (VERSION.SDK_INT >= 26) { + intent.putExtra( + DevicePolicyManager.EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION, + options.keepAccount + ) + } } if (VERSION.SDK_INT >= 24) { intent.putExtra( @@ -1927,7 +1929,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } @RequiresApi(26) fun setRpToken(token: String): Boolean { - return DPM.setResetPasswordToken(DAR, token.encodeToByteArray()) + return try { + DPM.setResetPasswordToken(DAR, token.encodeToByteArray()) + } catch (e: Exception) { + e.printStackTrace() + false + } } @RequiresApi(26) fun clearRpToken(): Boolean { diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index 8c58ff1..4f34429 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -242,7 +242,7 @@ fun AppChooserScreen( modifier = Modifier .fillMaxWidth() .combinedClickable(onLongClick = { - if (params.multiSelect) { + if (params.multiSelect && it !in selectedPackages) { selectedPackages += it hf.performHapticFeedback(HapticFeedbackType.LongPress) } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt index 1ee9166..db843e3 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt @@ -97,7 +97,9 @@ fun PasswordScreen(vm: MyViewModel,onNavigateUp: () -> Unit, onNavigate: (Any) - if(privilege.device) { FunctionItem(R.string.max_time_to_lock, icon = R.drawable.schedule_fill0) { dialog = 1 } FunctionItem(R.string.pwd_expiration_timeout, icon = R.drawable.lock_clock_fill0) { dialog = 3 } - FunctionItem(R.string.max_pwd_fail, icon = R.drawable.no_encryption_fill0) { dialog = 4 } + if (SP.displayDangerousFeatures) { + FunctionItem(R.string.max_pwd_fail, icon = R.drawable.no_encryption_fill0) { dialog = 4 } + } } if(VERSION.SDK_INT >= 26) { FunctionItem(R.string.required_strong_auth_timeout, icon = R.drawable.fingerprint_off_fill0) { dialog = 2 } From 3da523051b43aa9a4897ed1c2b7c5fcf29e79d19 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Thu, 15 Jan 2026 13:04:52 +0800 Subject: [PATCH 18/20] feat: app groups importing and exporting (#222) Show group name in app group operation dialog (#224) --- .../com/bintianqi/owndroid/MainActivity.kt | 2 +- .../com/bintianqi/owndroid/MyViewModel.kt | 19 +++++++ .../bintianqi/owndroid/dpm/Applications.kt | 57 ++++++++++++++++++- .../main/res/drawable/file_export_fill0.xml | 9 +++ app/src/main/res/drawable/file_open_fill0.xml | 9 +++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/drawable/file_export_fill0.xml create mode 100644 app/src/main/res/drawable/file_open_fill0.xml diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index c35d8ca..5aedebe 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -642,7 +642,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { } composable { ManageAppGroupsScreen( - vm.appGroups, + vm.appGroups, vm::exportAppGroups, vm::importAppGroups, { id, name, apps -> navController.navigate(EditAppGroup(id, name, apps)) }, ::navigateUp ) diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 1790e22..de923cd 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -65,6 +65,7 @@ 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.BasicAppGroup import com.bintianqi.owndroid.dpm.CaCertInfo import com.bintianqi.owndroid.dpm.CreateUserResult import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions @@ -119,6 +120,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.addJsonObject +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.put import java.net.InetAddress import java.security.MessageDigest import java.security.cert.CertificateException @@ -632,6 +637,20 @@ class MyViewModel(application: Application): AndroidViewModel(application) { group.filter { it.id != id } } } + fun exportAppGroups(uri: Uri) { + application.contentResolver.openOutputStream(uri)!!.use { + val list: List = appGroups.value + it.write(Json.encodeToString(list).encodeToByteArray()) + } + } + fun importAppGroups(uri: Uri) { + application.contentResolver.openInputStream(uri)!!.use { + Json.decodeFromString>(it.readBytes().decodeToString()) + }.forEach { + myRepo.setAppGroup(null, it.name, it.apps) + } + getAppGroups() + } @RequiresApi(24) fun reboot() { 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 cbae39f..d3dc8b4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -5,8 +5,11 @@ import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED import android.app.admin.PackagePolicy import android.content.Intent +import android.net.Uri import android.os.Build.VERSION import android.os.Looper +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -904,6 +907,8 @@ fun PackageFunctionScreen( if (dialog) AlertDialog( text = { Column { + Text(selectedGroup!!.name, style = typography.titleLarge) + Spacer(Modifier.height(6.dp)) Button({ onSet(selectedGroup!!.apps, true) dialog = false @@ -927,22 +932,68 @@ fun PackageFunctionScreen( ) } -class AppGroup(val id: Int, val name: String, val apps: List) +@Serializable +open class BasicAppGroup(open val name: String, open val apps: List) + +class AppGroup( + val id: Int, override val name: String, override val apps: List +) : BasicAppGroup(name, apps) @Serializable object ManageAppGroups @OptIn(ExperimentalMaterial3Api::class) @Composable fun ManageAppGroupsScreen( - appGroups: StateFlow>, + appGroups: StateFlow>, exportData: (Uri) -> Unit, importData: (Uri) -> Unit, navigateToEditScreen: (Int?, String, List) -> Unit, navigateUp: () -> Unit ) { val groups by appGroups.collectAsStateWithLifecycle() + val exportLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("application/json") + ) { + if (it != null) exportData(it) + } + val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { + if (it != null) importData(it) + } Scaffold( topBar = { TopAppBar( { Text(stringResource(R.string.app_group)) }, - navigationIcon = { NavIcon(navigateUp) } + navigationIcon = { NavIcon(navigateUp) }, + actions = { + var dropdown by remember { mutableStateOf(false) } + Box { + IconButton({ + dropdown = true + }) { + Icon(Icons.Default.MoreVert, null) + } + DropdownMenu(dropdown, { dropdown = false }) { + DropdownMenuItem( + { Text(stringResource(R.string.export)) }, + { + exportLauncher.launch("owndroid_app_groups") + dropdown = false + }, + leadingIcon = { + Icon(painterResource(R.drawable.file_export_fill0), null) + } + ) + DropdownMenuItem( + { Text(stringResource(R.string.import_str)) }, + { + importLauncher.launch(arrayOf("application/json")) + dropdown = false + }, + leadingIcon = { + Icon(painterResource(R.drawable.file_open_fill0), null) + } + ) + } + } + + } ) }, floatingActionButton = { diff --git a/app/src/main/res/drawable/file_export_fill0.xml b/app/src/main/res/drawable/file_export_fill0.xml new file mode 100644 index 0000000..a2e4e8b --- /dev/null +++ b/app/src/main/res/drawable/file_export_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/file_open_fill0.xml b/app/src/main/res/drawable/file_open_fill0.xml new file mode 100644 index 0000000..64664a0 --- /dev/null +++ b/app/src/main/res/drawable/file_open_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 cf5b18f..08879fb 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -376,6 +376,7 @@ 搜索 应用组 管理组 + 导入 编辑组 添加到列表 从列表中移除 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9df041d..e8ff66c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -410,6 +410,7 @@ Search App group Manage groups + Import Edit group Add to list Remove from list From 40a1841c8b1405e88c75d693ed982ef47262ca0f Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 18 Jan 2026 15:10:47 +0800 Subject: [PATCH 19/20] fix: managed configuration entry cache (#228) --- app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt | 1 + app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index de923cd..47b1b95 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -570,6 +570,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } ?: emptyList() } catch (e: Exception) { e.printStackTrace() + appRestrictions.value = emptyList() } } 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 d3dc8b4..6d678e4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -1185,7 +1185,8 @@ fun ManagedConfigurationScreen( .clickable { dialog = entry } - .padding(HorizontalPadding, 8.dp), + .padding(HorizontalPadding, 8.dp) + .animateItem(), verticalAlignment = Alignment.CenterVertically ) { val iconId = when (entry) { From ac2a90dbca6ee3718f768c95c91ac976ae5a646c Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 27 Jan 2026 12:56:08 +0800 Subject: [PATCH 20/20] feat: do not enforce package name regex check (#230) Update Readme Bump version number --- Readme-zh_CN.md | 4 ++-- Readme.md | 4 ++-- app/build.gradle.kts | 4 ++-- .../bintianqi/owndroid/dpm/Applications.kt | 23 ++++++++----------- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/Readme-zh_CN.md b/Readme-zh_CN.md index 01e7af0..349d306 100644 --- a/Readme-zh_CN.md +++ b/Readme-zh_CN.md @@ -57,7 +57,7 @@ java.lang.IllegalStateException: Not allowed to set the device owner because the > [!NOTE] > 一些系统有应用克隆、儿童空间等功能,它们通常是用户。 -#### Device owner 已存在 +### Device owner 已存在 ```text java.lang.IllegalStateException: Trying to set the device owner (com.bintianqi.owndroid/.Receiver), but device owner (xxx) is already set. @@ -142,7 +142,7 @@ context.sendBroadcast(intent) [License.md](LICENSE.md) -> Copyright (C) 2024 BinTianqi +> Copyright (C) 2026 BinTianqi > > This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. > diff --git a/Readme.md b/Readme.md index 2a2b9a0..a462f4d 100644 --- a/Readme.md +++ b/Readme.md @@ -57,7 +57,7 @@ Solutions: > [!NOTE] > Some systems have features such as app cloning and children space, which are usually users. -#### Device owner is already set +### Device owner is already set ```text java.lang.IllegalStateException: Trying to set the device owner (com.bintianqi.owndroid/.Receiver), but device owner (xxx) is already set. @@ -144,7 +144,7 @@ You can use Gradle in command line to build OwnDroid. [License.md](LICENSE.md) -> Copyright (C) 2024 BinTianqi +> Copyright (C) 2026 BinTianqi > > This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. > diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ee1a2c..fa9225d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "com.bintianqi.owndroid" minSdk = 23 targetSdk = 36 - versionCode = 41 - versionName = "7.2" + versionCode = 42 + versionName = "7.3" multiDexEnabled = false } 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 6d678e4..1f501dd 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -180,6 +180,7 @@ fun PackageNameTextField( Icon(Icons.AutoMirrored.Default.List, null) } }, + isError = value.isNotEmpty() && !value.isValidPackageName, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), keyboardActions = KeyboardActions { fm.clearFocus() } ) @@ -384,7 +385,7 @@ fun PermissionsManagerScreen( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickable(packageName.isValidPackageName) { + .clickable { selectedPermission = index } .padding(8.dp) @@ -467,8 +468,7 @@ fun ClearAppStorageScreen( Modifier.padding(vertical = 8.dp)) { packageName = it } Button( { dialog = true }, - Modifier.fillMaxWidth(), - packageName.isValidPackageName + Modifier.fillMaxWidth() ) { Text(stringResource(R.string.clear)) } @@ -532,8 +532,7 @@ fun UninstallAppScreen( Modifier.padding(vertical = 8.dp)) { packageName = it } Button( { dialog = true }, - Modifier.fillMaxWidth(), - packageName.isValidPackageName + Modifier.fillMaxWidth() ) { Text(stringResource(R.string.uninstall)) } @@ -607,8 +606,7 @@ fun InstallExistingAppScreen( { context.showOperationResultToast(onInstall(packageName)) }, - Modifier.fillMaxWidth(), - packageName.isValidPackageName + Modifier.fillMaxWidth() ) { Text(stringResource(R.string.install)) } @@ -663,8 +661,7 @@ fun CredentialManagerPolicyScreen( setCmPackage(inputPackages, true) input = "" }, - Modifier.fillMaxWidth(), - inputPackages.all { it.isValidPackageName } + Modifier.fillMaxWidth() ) { Text(stringResource(R.string.add)) } @@ -721,8 +718,7 @@ fun PermittedAsAndImPackages( }, Modifier .fillMaxWidth() - .padding(horizontal = HorizontalPadding), - inputPackages.all { it.isValidPackageName } + .padding(horizontal = HorizontalPadding) ) { Text(stringResource(R.string.add)) } @@ -894,8 +890,7 @@ fun PackageFunctionScreen( .fillMaxWidth() .padding(horizontal = HorizontalPadding) .padding(bottom = 10.dp), - inputPackages.all { it.isValidPackageName } && - packages.none { it.name in inputPackages } + packages.none { it.name in inputPackages } ) { Text(stringResource(R.string.add)) } @@ -1096,7 +1091,7 @@ fun EditAppGroupScreen( .fillMaxWidth() .padding(horizontal = HorizontalPadding) .padding(bottom = 10.dp), - inputPackages.all { it.isValidPackageName && it !in list } + inputPackages.all { it !in list } ) { Text(stringResource(R.string.add)) }