From a9452ac14e1df26eb6e4f1152532188011d470e2 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 27 Sep 2025 15:57:45 +0800 Subject: [PATCH] ViewModel refactoring: Work profile part close #169 Improve UI, fix #172 fix #165 --- app/src/main/AndroidManifest.xml | 1 + .../com/bintianqi/owndroid/MainActivity.kt | 21 +- .../com/bintianqi/owndroid/MyViewModel.kt | 86 +++++- .../com/bintianqi/owndroid/ShizukuService.kt | 4 +- .../bintianqi/owndroid/dpm/Applications.kt | 4 + .../java/com/bintianqi/owndroid/dpm/System.kt | 1 + .../com/bintianqi/owndroid/dpm/WorkProfile.kt | 268 +++++++++++------- app/src/main/res/values-ru/strings.xml | 7 - app/src/main/res/values-tr/strings.xml | 7 - app/src/main/res/values-zh-rCN/strings.xml | 11 +- app/src/main/res/values/strings.xml | 11 +- 11 files changed, 275 insertions(+), 146 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bdbe7d4..c316c33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,7 @@ + diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 80cf791..1443842 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -417,11 +417,22 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { AddApnSettingScreen(it.arguments?.getParcelable("setting"), ::navigateUp) } composable { WorkProfileScreen(::navigateUp, ::navigate) } - composable { OrganizationOwnedProfileScreen(::navigateUp) } - composable { CreateWorkProfileScreen(::navigateUp) } - composable { SuspendPersonalAppScreen(::navigateUp) } - composable { CrossProfileIntentFilterScreen(::navigateUp) } - composable { DeleteWorkProfileScreen(::navigateUp) } + composable { + OrganizationOwnedProfileScreen(vm::activateOrgProfileByShizuku, ::navigateUp) + } + composable { + CreateWorkProfileScreen(vm::createWorkProfile, ::navigateUp) + } + composable { + SuspendPersonalAppScreen( + vm::getPersonalAppsSuspendedReason, vm::setPersonalAppsSuspended, + vm::getProfileMaxTimeOff, vm::setProfileMaxTimeOff, ::navigateUp + ) + } + composable { + CrossProfileIntentFilterScreen(vm::addCrossProfileIntentFilter, ::navigateUp) + } + composable { DeleteWorkProfileScreen(vm::wipeData, ::navigateUp) } composable { val canSwitchView = (it.toRoute() as ApplicationsList).canSwitchView diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 5f5d6ea..c2d58c4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -1,5 +1,6 @@ package com.bintianqi.owndroid +import android.accounts.Account import android.app.ActivityOptions import android.app.Application import android.app.PendingIntent @@ -35,13 +36,17 @@ import com.bintianqi.owndroid.Privilege.DPM import com.bintianqi.owndroid.dpm.ACTIVATE_DEVICE_OWNER_COMMAND import com.bintianqi.owndroid.dpm.AppStatus import com.bintianqi.owndroid.dpm.CaCertInfo +import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions import com.bintianqi.owndroid.dpm.DelegatedAdmin import com.bintianqi.owndroid.dpm.DeviceAdmin import com.bintianqi.owndroid.dpm.FrpPolicyInfo import com.bintianqi.owndroid.dpm.HardwareProperties +import com.bintianqi.owndroid.dpm.IntentFilterDirection +import com.bintianqi.owndroid.dpm.IntentFilterOptions import com.bintianqi.owndroid.dpm.PendingSystemUpdateInfo import com.bintianqi.owndroid.dpm.SystemOptionsStatus import com.bintianqi.owndroid.dpm.SystemUpdatePolicyInfo +import com.bintianqi.owndroid.dpm.activateOrgProfileCommand import com.bintianqi.owndroid.dpm.delegatedScopesList import com.bintianqi.owndroid.dpm.getPackageInstaller import com.bintianqi.owndroid.dpm.handlePrivilegeChange @@ -294,7 +299,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } fun setCmPackage(name: String, status: Boolean) { cmPackages.update { list -> - if (status) list + getAppInfo(name) else list.dropWhile { it.name == name } + if (status) list + getAppInfo(name) else list.filter { it.name != name } } } @RequiresApi(34) @@ -315,7 +320,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } fun setPimPackage(name: String, status: Boolean) { pimPackages.update { packages -> - if (status) packages + getAppInfo(name) else packages.dropWhile { it.name == name } + if (status) packages + getAppInfo(name) else packages.filter { it.name != name } } } fun setPimPolicy(allowAll: Boolean): Boolean { @@ -335,7 +340,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) { } fun setPasPackage(name: String, status: Boolean) { pasPackages.update { packages -> - if (status) packages + getAppInfo(name) else packages.dropWhile { it.name == name } + if (status) packages + getAppInfo(name) else packages.filter { it.name != name } } } fun setPasPolicy(allowAll: Boolean): Boolean { @@ -1022,6 +1027,81 @@ class MyViewModel(application: Application): AndroidViewModel(application) { false } } + 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 + ) + } 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( + DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION, + options.skipEncrypt + ) + } + if (VERSION.SDK_INT >= 33) { + intent.putExtra(DevicePolicyManager.EXTRA_PROVISIONING_ALLOW_OFFLINE, options.offline) + } + return intent + } + fun activateOrgProfileByShizuku(callback: (Boolean) -> Unit) { + viewModelScope.launch { + var succeed = false + useShizuku(application) { service -> + val result = IUserService.Stub.asInterface(service).execute(activateOrgProfileCommand) + succeed = result?.getInt("code", -1) == 0 + callback(succeed) + } + if (succeed) Privilege.updateStatus() + } + } + @RequiresApi(30) + fun getPersonalAppsSuspendedReason(): Int { + return DPM.getPersonalAppsSuspendedReasons(DAR) + } + @RequiresApi(30) + fun setPersonalAppsSuspended(suspended: Boolean) { + DPM.setPersonalAppsSuspended(DAR, suspended) + } + @RequiresApi(30) + fun getProfileMaxTimeOff(): Long { + return DPM.getManagedProfileMaximumTimeOff(DAR) + } + @RequiresApi(30) + fun setProfileMaxTimeOff(time: Long) { + DPM.setManagedProfileMaximumTimeOff(DAR, time) + } + fun addCrossProfileIntentFilter(options: IntentFilterOptions) { + val filter = IntentFilter(options.action) + if (options.category.isNotEmpty()) filter.addCategory(options.category) + if (options.mimeType.isNotEmpty()) filter.addDataType(options.mimeType) + val flags = when(options.direction) { + IntentFilterDirection.ToManaged -> DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED + IntentFilterDirection.ToParent -> DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT + IntentFilterDirection.Both -> DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED or + DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT + } + DPM.addCrossProfileIntentFilter(DAR, filter, flags) + } } data class ThemeSettings( diff --git a/app/src/main/java/com/bintianqi/owndroid/ShizukuService.kt b/app/src/main/java/com/bintianqi/owndroid/ShizukuService.kt index a70eecf..99c5b0d 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ShizukuService.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ShizukuService.kt @@ -57,7 +57,9 @@ fun useShizuku(context: Context, action: (IBinder?) -> Unit) { } else { Sui.init(context.packageName) fun requestPermissionResultListener(requestCode: Int, grantResult: Int) { - if(grantResult != PackageManager.PERMISSION_GRANTED) { + if (grantResult == PackageManager.PERMISSION_GRANTED) { + Shizuku.bindUserService(getShizukuArgs(context), connection) + } else { context.popToast(R.string.permission_denied) } Shizuku.removeRequestPermissionResultListener(::requestPermissionResultListener) 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 e043570..b95f472 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -273,6 +273,7 @@ fun ApplicationDetailsScreen( ) 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(40.dp)) } if(dialog == 1 && VERSION.SDK_INT >= 28) ClearAppStorageDialog(packageName, vm::clearAppData) { dialog = 0 } @@ -604,6 +605,7 @@ fun CredentialManagerPolicyScreen( ) { Text(stringResource(R.string.apply)) } + Spacer(Modifier.height(40.dp)) } } } @@ -658,6 +660,7 @@ fun PermittedAsAndImPackages( } Spacer(Modifier.height(10.dp)) Notes(note, HorizontalPadding) + Spacer(Modifier.height(40.dp)) } } } @@ -767,6 +770,7 @@ fun PackageFunctionScreen( Text(stringResource(R.string.add)) } if (notes != null) Notes(notes, HorizontalPadding) + Spacer(Modifier.height(40.dp)) } } } \ No newline at end of file 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 9138dfd..1016c37 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -1262,6 +1262,7 @@ private fun LockTaskPackages( Text(stringResource(R.string.add)) } Notes(R.string.info_lock_task_packages) + Spacer(Modifier.height(40.dp)) } } } 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 1fac984..642920d 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt @@ -1,22 +1,9 @@ package com.bintianqi.owndroid.dpm -import android.accounts.Account -import android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE -import android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE -import android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ALLOW_OFFLINE -import android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME -import android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME -import android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION -import android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION -import android.app.admin.DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT -import android.app.admin.DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED -import android.app.admin.DevicePolicyManager.PERSONAL_APPS_NOT_SUSPENDED -import android.app.admin.DevicePolicyManager.PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT +import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager.WIPE_EUICC import android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE -import android.content.ActivityNotFoundException import android.content.Intent -import android.content.IntentFilter import android.os.Binder import android.os.Build.VERSION import androidx.activity.compose.rememberLauncherForActivityResult @@ -24,6 +11,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -33,8 +21,13 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -45,6 +38,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -55,18 +49,19 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bintianqi.owndroid.IUserService -import com.bintianqi.owndroid.MyAdminComponent +import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CheckBoxItem +import com.bintianqi.owndroid.ui.CircularProgressDialog +import com.bintianqi.owndroid.ui.ExpandExposedTextFieldIcon +import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.Notes import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.useShizuku +import com.bintianqi.owndroid.yesOrNo import kotlinx.serialization.Serializable @Serializable object WorkProfile @@ -86,22 +81,28 @@ fun WorkProfileScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { } } +data class CreateWorkProfileOptions( + val skipEncrypt: Boolean, val offline: Boolean, val migrateAccount: Boolean, + val accountName: String, val accountType: String, val keepAccount: Boolean +) + @Serializable object CreateWorkProfile @Composable -fun CreateWorkProfileScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current +fun CreateWorkProfileScreen( + createIntent: (CreateWorkProfileOptions) -> Intent, onNavigateUp: () -> Unit +) { val focusMgr = LocalFocusManager.current val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { } - MyScaffold(R.string.create_work_profile, onNavigateUp) { + MyScaffold(R.string.create_work_profile, onNavigateUp, 0.dp) { var skipEncrypt by remember { mutableStateOf(false) } var offlineProvisioning by remember { mutableStateOf(true) } var migrateAccount by remember { mutableStateOf(false) } var migrateAccountName by remember { mutableStateOf("") } var migrateAccountType by remember { mutableStateOf("") } var keepAccount by remember { mutableStateOf(true) } - if(VERSION.SDK_INT >= 22) { - CheckBoxItem(R.string.migrate_account, migrateAccount) { migrateAccount = it } + 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)) { @@ -110,47 +111,40 @@ fun CreateWorkProfileScreen(onNavigateUp: () -> Unit) { label = { Text(stringResource(R.string.account_name)) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), keyboardActions = KeyboardActions { fr.requestFocus() }, - modifier = Modifier.fillMaxWidth() + 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().focusRequester(fr) + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = HorizontalPadding) + .focusRequester(fr) ) if(VERSION.SDK_INT >= 26) { - CheckBoxItem(R.string.keep_account, keepAccount) { keepAccount = it } + FullWidthCheckBoxItem(R.string.keep_account, keepAccount) { keepAccount = it } } } } } - if(VERSION.SDK_INT >= 24) CheckBoxItem(R.string.skip_encryption, skipEncrypt) { skipEncrypt = it } - if(VERSION.SDK_INT >= 33) CheckBoxItem(R.string.offline_provisioning, offlineProvisioning) { offlineProvisioning = it } + if (VERSION.SDK_INT >= 24) FullWidthCheckBoxItem( + R.string.skip_encryption, skipEncrypt + ) { skipEncrypt = it } + if (VERSION.SDK_INT >= 33) FullWidthCheckBoxItem( + R.string.offline_provisioning, offlineProvisioning + ) { offlineProvisioning = it } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { - try { - val intent = Intent(ACTION_PROVISION_MANAGED_PROFILE) - if(VERSION.SDK_INT >= 23) { - intent.putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, MyAdminComponent) - } else { - intent.putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME, context.packageName) - } - if(migrateAccount && VERSION.SDK_INT >= 22) { - intent.putExtra(EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE, Account(migrateAccountName, migrateAccountType)) - if(VERSION.SDK_INT >= 26) { - intent.putExtra(EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION, keepAccount) - } - } - if(VERSION.SDK_INT >= 24) { intent.putExtra(EXTRA_PROVISIONING_SKIP_ENCRYPTION, skipEncrypt) } - if(VERSION.SDK_INT >= 33) { intent.putExtra(EXTRA_PROVISIONING_ALLOW_OFFLINE, offlineProvisioning) } - launcher.launch(intent) - } catch(_: ActivityNotFoundException) { - context.popToast(R.string.unsupported) - } + val intent = createIntent(CreateWorkProfileOptions( + skipEncrypt, offlineProvisioning, migrateAccount, migrateAccountName, + migrateAccountType, keepAccount + )) + launcher.launch(intent) }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding) ) { Text(stringResource(R.string.create)) } @@ -161,18 +155,19 @@ fun CreateWorkProfileScreen(onNavigateUp: () -> Unit) { @RequiresApi(30) @Composable -fun OrganizationOwnedProfileScreen(onNavigateUp: () -> Unit) { +fun OrganizationOwnedProfileScreen( + onActivate: ((Boolean) -> Unit) -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current + var activating by remember { mutableStateOf(false) } var dialog by remember { mutableStateOf(false) } MyScaffold(R.string.org_owned_work_profile, onNavigateUp) { Button({ - useShizuku(context) { service -> - val result = IUserService.Stub.asInterface(service).execute(activateOrgProfileCommand) - if (result?.getInt("code", -1) == 0) { - context.showOperationResultToast(true) - } else { - context.showOperationResultToast(false) - } + activating = true + onActivate { + activating = false + context.showOperationResultToast(it) + if (it) onNavigateUp() } }) { Text(stringResource(R.string.shizuku)) @@ -189,6 +184,7 @@ fun OrganizationOwnedProfileScreen(onNavigateUp: () -> Unit) { }, onDismissRequest = { dialog = false } ) + if (activating) CircularProgressDialog { } } } @@ -199,41 +195,46 @@ val activateOrgProfileCommand = "dpm mark-profile-owner-on-organization-owned-de @RequiresApi(30) @Composable -fun SuspendPersonalAppScreen(onNavigateUp: () -> Unit) { +fun SuspendPersonalAppScreen( + getSuspendedReasons: () -> Int, setSuspended: (Boolean) -> Unit, getMaxTime: () -> Long, + setMaxTime: (Long) -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current val focusMgr = LocalFocusManager.current - var suspend by remember { mutableStateOf(Privilege.DPM.getPersonalAppsSuspendedReasons(Privilege.DAR) != PERSONAL_APPS_NOT_SUSPENDED) } + var reason by remember { mutableIntStateOf(DevicePolicyManager.PERSONAL_APPS_NOT_SUSPENDED) } + var time by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + reason = getSuspendedReasons() + time = getMaxTime().toString() + } MyScaffold(R.string.suspend_personal_app, onNavigateUp) { - SwitchItem(R.string.suspend_personal_app, state = suspend, + SwitchItem(R.string.suspend_personal_app, state = reason != 0, onCheckedChange = { - Privilege.DPM.setPersonalAppsSuspended(Privilege.DAR, it) - suspend = Privilege.DPM.getPersonalAppsSuspendedReasons(Privilege.DAR) != PERSONAL_APPS_NOT_SUSPENDED + setSuspended(it) + reason = if (it) DevicePolicyManager.PERSONAL_APPS_SUSPENDED_EXPLICITLY + else DevicePolicyManager.PERSONAL_APPS_NOT_SUSPENDED }, padding = false ) - var time by remember { mutableStateOf("") } - time = Privilege.DPM.getManagedProfileMaximumTimeOff(Privilege.DAR).toString() Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.profile_max_time_off), style = typography.titleLarge) Text(text = stringResource(R.string.profile_max_time_out_desc)) - Text( - text = stringResource( - R.string.personal_app_suspended_because_timeout, - Privilege.DPM.getPersonalAppsSuspendedReasons(Privilege.DAR) == PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT - ) - ) + Text(stringResource( + R.string.personal_app_suspended_because_timeout, + stringResource((reason == DevicePolicyManager.PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT).yesOrNo) + )) OutlinedTextField( value = time, onValueChange = { time=it }, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), label = { Text(stringResource(R.string.time_unit_ms)) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }) ) - Text(text = stringResource(R.string.cannot_less_than_72_hours)) Button( onClick = { - Privilege.DPM.setManagedProfileMaximumTimeOff(Privilege.DAR, time.toLong()) + setMaxTime(time.toLong()) context.showOperationResultToast(true) }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + enabled = time.toLongOrNull() != null ) { Text(stringResource(R.string.apply)) } @@ -241,14 +242,33 @@ fun SuspendPersonalAppScreen(onNavigateUp: () -> Unit) { } } +data class IntentFilterOptions( + val action: String, val category: String, val mimeType: String, + val direction: IntentFilterDirection +) +enum class IntentFilterDirection(val text: Int) { + ToParent(R.string.work_to_personal), ToManaged(R.string.personal_to_work), + Both(R.string.both_direction) +} + @Serializable object CrossProfileIntentFilter +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun CrossProfileIntentFilterScreen(onNavigateUp: () -> Unit) { +fun CrossProfileIntentFilterScreen( + addFilter: (IntentFilterOptions) -> Unit, + onNavigateUp: () -> Unit +) { val context = LocalContext.current val focusMgr = LocalFocusManager.current + var action by remember { mutableStateOf("") } + var customCategory by remember { mutableStateOf(false) } + var category by remember { mutableStateOf("") } + var customMimeType by remember { mutableStateOf(false) } + var mimeType by remember { mutableStateOf("") } + var dropdown by remember { mutableStateOf(false) } + var direction by remember { mutableStateOf(IntentFilterDirection.Both) } MyScaffold(R.string.intent_filter, onNavigateUp) { - var action by remember { mutableStateOf("") } OutlinedTextField( value = action, onValueChange = { action = it }, label = { Text("Action") }, @@ -256,32 +276,61 @@ fun CrossProfileIntentFilterScreen(onNavigateUp: () -> Unit) { keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }), modifier = Modifier.fillMaxWidth() ) - Spacer(Modifier.padding(vertical = 5.dp)) - Button( - onClick = { - Privilege.DPM.addCrossProfileIntentFilter(Privilege.DAR, IntentFilter(action), FLAG_PARENT_CAN_ACCESS_MANAGED) - context.showOperationResultToast(true) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.add_intent_filter_work_to_personal)) + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Checkbox(customCategory, { + customCategory = it + category = "" + }) + OutlinedTextField( + category, { category = it }, Modifier.fillMaxWidth(), + label = { Text("Category") }, enabled = customCategory + ) + } + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Checkbox(customMimeType, { + customMimeType = it + mimeType = "" + }) + OutlinedTextField( + mimeType, { mimeType = it }, Modifier.fillMaxWidth(), + label = { Text("MIME type") }, enabled = customMimeType + ) + } + ExposedDropdownMenuBox(dropdown, { dropdown = it }, Modifier.padding(vertical = 5.dp)) { + OutlinedTextField( + stringResource(direction.text), {}, + Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth(), + label = { Text(stringResource(R.string.direction)) }, readOnly = true, + trailingIcon = { ExpandExposedTextFieldIcon(dropdown) } + ) + ExposedDropdownMenu(dropdown, { dropdown = false }) { + IntentFilterDirection.entries.forEach { + DropdownMenuItem({ Text(stringResource(it.text)) }, { + direction = it + dropdown = false + }) + } + } } Button( - onClick = { - Privilege.DPM.addCrossProfileIntentFilter(Privilege.DAR, IntentFilter(action), FLAG_MANAGED_CAN_ACCESS_PARENT) + { + addFilter(IntentFilterOptions( + action, category, mimeType, direction + )) context.showOperationResultToast(true) }, - modifier = Modifier.fillMaxWidth() + Modifier.fillMaxWidth(), + enabled = action.isNotBlank() && (!customCategory || category.isNotBlank()) && + (!customMimeType || mimeType.isNotBlank()) ) { - Text(stringResource(R.string.add_intent_filter_personal_to_work)) + Text(stringResource(R.string.add)) } - Spacer(Modifier.padding(vertical = 2.dp)) Button( onClick = { Privilege.DPM.clearCrossProfileIntentFilters(Privilege.DAR) context.showOperationResultToast(true) }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp) ) { Text(stringResource(R.string.clear_cross_profile_filters)) } @@ -292,28 +341,34 @@ fun CrossProfileIntentFilterScreen(onNavigateUp: () -> Unit) { @Serializable object DeleteWorkProfile @Composable -fun DeleteWorkProfileScreen(onNavigateUp: () -> Unit) { +fun DeleteWorkProfileScreen( + deleteProfile: (Boolean, Int, String) -> Unit, onNavigateUp: () -> Unit +) { val focusMgr = LocalFocusManager.current - var flag by remember { mutableIntStateOf(0) } + var flags by remember { mutableIntStateOf(0) } var warning by remember { mutableStateOf(false) } - var silent by remember { mutableStateOf(false) } var reason by remember { mutableStateOf("") } MyScaffold(R.string.delete_work_profile, onNavigateUp) { - CheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { flag = flag xor WIPE_EXTERNAL_STORAGE } - if(VERSION.SDK_INT >= 28) CheckBoxItem(R.string.wipe_euicc, flag and WIPE_EUICC != 0) { flag = flag xor WIPE_EUICC } - CheckBoxItem(R.string.wipe_silently, silent) { silent = it } - AnimatedVisibility(!silent && VERSION.SDK_INT >= 28) { - OutlinedTextField( - value = reason, onValueChange = { reason = it }, - label = { Text(stringResource(R.string.reason)) }, - modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) - ) + CheckBoxItem(R.string.wipe_external_storage, flags and WIPE_EXTERNAL_STORAGE != 0) { + flags = flags xor WIPE_EXTERNAL_STORAGE } + if(VERSION.SDK_INT >= 28) CheckBoxItem(R.string.wipe_euicc, flags and WIPE_EUICC != 0) { + flags = flags xor WIPE_EUICC + } + CheckBoxItem(R.string.wipe_silently, flags and DevicePolicyManager.WIPE_SILENTLY != 0) { + flags = flags xor DevicePolicyManager.WIPE_SILENTLY + reason = "" + } + if (VERSION.SDK_INT >= 28) OutlinedTextField( + value = reason, onValueChange = { reason = it }, + label = { Text(stringResource(R.string.reason)) }, + enabled = flags and DevicePolicyManager.WIPE_SILENTLY == 0, + modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) + ) Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { focusMgr.clearFocus() - silent = reason == "" warning = true }, colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), @@ -322,8 +377,7 @@ fun DeleteWorkProfileScreen(onNavigateUp: () -> Unit) { Text(stringResource(R.string.delete)) } } - if(warning) { - LaunchedEffect(Unit) { silent = reason == "" } + if (warning) { AlertDialog( title = { Text(text = stringResource(R.string.warning), color = colorScheme.error) @@ -335,11 +389,7 @@ fun DeleteWorkProfileScreen(onNavigateUp: () -> Unit) { confirmButton = { TextButton( onClick = { - if(VERSION.SDK_INT >= 28 && !silent) { - Privilege.DPM.wipeData(flag, reason) - } else { - Privilege.DPM.wipeData(flag) - } + deleteProfile(false, flags, reason) }, colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) ) { diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3f7533d..86d42a7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -75,7 +75,6 @@ Владелец профиля Владелец устройства Делегированные администраторы - делегированные возможности Управление ограничениями для приложений Управление сертификатами Выберите сертификат связки ключей @@ -101,7 +100,6 @@ Отключить управление аккаунтами Тип аккаунта Передача прав владения - Имя целевого компонента Информация на экране блокировки Сообщение поддержки Краткое сообщение @@ -212,8 +210,6 @@ Время получения обновления: %1$s Установить системное обновление Выберите OTA-пакет... - Начать установку системного обновления - Установка системного обновления не удалась: Низкий заряд батареи Файл обновления недействителен Неверная версия ОС @@ -339,10 +335,7 @@ Максимальное время отключения Личные приложения будут приостановлены после закрытия рабочего профиля на указанное время. 0 означает отсутствие ограничения. Личное приложение приостановлено по причине: %1$s - Не может быть меньше 72 часов Фильтр намерений - Добавить (из рабочего в личный) - Добавить (из личного в рабочий) Очистить все фильтры Идентификатор организации Длина должна быть от 6 до 64 символов diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c95bc0d..31f9576 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -76,7 +76,6 @@ Profil Sahibi Cihaz Sahibi Yetkilendirilmiş Yöneticiler - Yetkilendirilmiş Kapsam Uygulama Kısıtlamalarını Yönet Sertifikaları Yönet KeyChain Sertifikasını Seç @@ -102,7 +101,6 @@ Hesap Yönetimini Devre Dışı Bırak Hesap Türü Sahipliği Devret - Hedef Bileşen Adı Kilit Ekranı Bilgisi Destek Mesajları Kısa Mesaj @@ -243,8 +241,6 @@ Sistem Güncellemesini Yükle OTA paketini seç... - Sistem güncellemesini yüklemeye başla - "Sistem güncellemesi yüklenemedi: " Pil seviyesi düşük Güncelleme dosyası geçersiz Yanlış işletim sistemi sürümü @@ -365,10 +361,7 @@ Maksimum Kapalı Kalma Süresi İş profili kapatıldıktan sonra kişisel uygulamalar bu süre boyunca askıya alınacak. 0, sınırsız anlamına gelir. Kişisel uygulama şu nedenle askıya alındı: %1$s - 72 saatten az olamaz Niyet Filtresi - Ekle (işten kişisel profile) - Ekle (kişisel profilden işe) Tüm filtreleri temizle Kurum Kimliği Uzunluk 6 ile 64 karakter arasında olmalıdır diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ce5eda5..e7a5914 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -74,7 +74,6 @@ Profile owner Device owner 委托管理员 - 委托作用域 管理应用限制 管理证书 选择密钥链证书 @@ -99,7 +98,6 @@ 禁用账号管理 账号类型 转移所有权 - 目标组件名 锁屏提示信息 提供支持的消息 提供支持的短消息 @@ -221,8 +219,6 @@ 系统更新接收时间: %1$s 安装系统更新 选择OTA包... - 开始安装系统更新 - 安装系统更新失败: 电量低 OTA包无效 系统版本错误 @@ -343,10 +339,11 @@ 资料关闭时间 工作资料处于关闭状态的时间达到该限制后会挂起个人应用,0为无限制 个人应用已经因此挂起:%1$s - 不能少于72小时 Intent过滤器 - 添加(工作到个人) - 添加(个人到工作) + 方向 + 双向 + 工作到个人 + 个人到工作 清除所有过滤器 组织ID 长度应在6~64个字符之间 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da76e17..704a87d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -79,7 +79,6 @@ Profile owner Device owner Delegated admins - delegated scope Manage application restrictions Manage certificates Select KeyChain certificate @@ -105,7 +104,6 @@ Disable account management Account type Transfer Ownership - Target component name Lock screen info Support Messages Short message @@ -249,8 +247,6 @@ Install system update Select OTA package... - Start installing system update - "Install system update failed: " Battery is low Update file is invalid Incorrect OS version @@ -374,10 +370,11 @@ Max time off Personal apps will be suspended after the work profile is closed for this amount of time. 0 means no limit. Personal app suspended because of this: %1$s - Cannot less than 72 hours Intent filter - Add(work to personal) - Add(personal to work) + Direction + Both direction + Work to personal + Personal to work Clear all filters Organization ID The length should be between 6~64 characters