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