ViewModel refactoring: Work profile part

close #169
Improve UI, fix #172
fix #165
This commit is contained in:
BinTianqi
2025-09-27 15:57:45 +08:00
parent 5928dbb657
commit a9452ac14e
11 changed files with 275 additions and 146 deletions

View File

@@ -38,6 +38,7 @@
<action android:name="android.app.action.PROVISION_MANAGED_PROFILE"/>
<action android:name="android.app.action.MANAGED_PROFILE_PROVISIONED"/>
<action android:name="android.app.action.PROFILE_PROVISIONING_COMPLETE"/>
<action android:name="android.app.action.CHECK_POLICY_COMPLIANCE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

View File

@@ -417,11 +417,22 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<AddApnSetting> { AddApnSettingScreen(it.arguments?.getParcelable("setting"), ::navigateUp) }
composable<WorkProfile> { WorkProfileScreen(::navigateUp, ::navigate) }
composable<OrganizationOwnedProfile> { OrganizationOwnedProfileScreen(::navigateUp) }
composable<CreateWorkProfile> { CreateWorkProfileScreen(::navigateUp) }
composable<SuspendPersonalApp> { SuspendPersonalAppScreen(::navigateUp) }
composable<CrossProfileIntentFilter> { CrossProfileIntentFilterScreen(::navigateUp) }
composable<DeleteWorkProfile> { DeleteWorkProfileScreen(::navigateUp) }
composable<OrganizationOwnedProfile> {
OrganizationOwnedProfileScreen(vm::activateOrgProfileByShizuku, ::navigateUp)
}
composable<CreateWorkProfile> {
CreateWorkProfileScreen(vm::createWorkProfile, ::navigateUp)
}
composable<SuspendPersonalApp> {
SuspendPersonalAppScreen(
vm::getPersonalAppsSuspendedReason, vm::setPersonalAppsSuspended,
vm::getProfileMaxTimeOff, vm::setProfileMaxTimeOff, ::navigateUp
)
}
composable<CrossProfileIntentFilter> {
CrossProfileIntentFilterScreen(vm::addCrossProfileIntentFilter, ::navigateUp)
}
composable<DeleteWorkProfile> { DeleteWorkProfileScreen(vm::wipeData, ::navigateUp) }
composable<ApplicationsList> {
val canSwitchView = (it.toRoute() as ApplicationsList).canSwitchView

View File

@@ -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(

View File

@@ -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)

View File

@@ -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))
}
}
}

View File

@@ -1262,6 +1262,7 @@ private fun LockTaskPackages(
Text(stringResource(R.string.add))
}
Notes(R.string.info_lock_task_packages)
Spacer(Modifier.height(40.dp))
}
}
}

View File

@@ -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)
) {

View File

@@ -75,7 +75,6 @@
<string name="profile_owner">Владелец профиля</string>
<string name="device_owner">Владелец устройства</string>
<string name="delegated_admins">Делегированные администраторы</string>
<string name="delegated_scope">делегированные возможности</string>
<string name="manage_application_restrictions">Управление ограничениями для приложений</string>
<string name="manage_certificates">Управление сертификатами</string>
<string name="select_keychain_certificates">Выберите сертификат связки ключей</string>
@@ -101,7 +100,6 @@
<string name="disable_account_management">Отключить управление аккаунтами</string>
<string name="account_type">Тип аккаунта</string>
<string name="transfer_ownership">Передача прав владения</string>
<string name="target_component_name">Имя целевого компонента</string>
<string name="lock_screen_info">Информация на экране блокировки</string>
<string name="support_messages">Сообщение поддержки</string>
<string name="short_support_msg">Краткое сообщение</string>
@@ -212,8 +210,6 @@
<string name="update_received_time">Время получения обновления: %1$s</string>
<string name="install_system_update">Установить системное обновление</string>
<string name="select_ota_package" tools:ignore="TypographyEllipsis">Выберите OTA-пакет...</string>
<string name="start_install_system_update">Начать установку системного обновления</string>
<string name="install_system_update_failed">Установка системного обновления не удалась: </string>
<string name="battery_low">Низкий заряд батареи</string>
<string name="update_file_invalid">Файл обновления недействителен</string>
<string name="incorrect_os_ver">Неверная версия ОС</string>
@@ -339,10 +335,7 @@
<string name="profile_max_time_off">Максимальное время отключения</string>
<string name="profile_max_time_out_desc">Личные приложения будут приостановлены после закрытия рабочего профиля на указанное время. 0 означает отсутствие ограничения.</string>
<string name="personal_app_suspended_because_timeout">Личное приложение приостановлено по причине: %1$s</string>
<string name="cannot_less_than_72_hours">Не может быть меньше 72 часов</string>
<string name="intent_filter">Фильтр намерений</string>
<string name="add_intent_filter_work_to_personal">Добавить (из рабочего в личный)</string>
<string name="add_intent_filter_personal_to_work">Добавить (из личного в рабочий)</string>
<string name="clear_cross_profile_filters">Очистить все фильтры</string>
<string name="org_id">Идентификатор организации</string>
<string name="length_6_to_64">Длина должна быть от 6 до 64 символов</string>

View File

@@ -76,7 +76,6 @@
<string name="profile_owner">Profil Sahibi</string>
<string name="device_owner">Cihaz Sahibi</string>
<string name="delegated_admins">Yetkilendirilmiş Yöneticiler</string>
<string name="delegated_scope">Yetkilendirilmiş Kapsam</string>
<string name="manage_application_restrictions">Uygulama Kısıtlamalarını Yönet</string>
<string name="manage_certificates">Sertifikaları Yönet</string>
<string name="select_keychain_certificates">KeyChain Sertifikasını Seç</string>
@@ -102,7 +101,6 @@
<string name="disable_account_management">Hesap Yönetimini Devre Dışı Bırak</string>
<string name="account_type">Hesap Türü</string>
<string name="transfer_ownership">Sahipliği Devret</string>
<string name="target_component_name">Hedef Bileşen Adı</string>
<string name="lock_screen_info">Kilit Ekranı Bilgisi</string>
<string name="support_messages">Destek Mesajları</string>
<string name="short_support_msg">Kısa Mesaj</string>
@@ -243,8 +241,6 @@
<string name="install_system_update">Sistem Güncellemesini Yükle</string>
<string name="select_ota_package" tools:ignore="TypographyEllipsis">OTA paketini seç...</string>
<string name="start_install_system_update">Sistem güncellemesini yüklemeye başla</string>
<string name="install_system_update_failed">"Sistem güncellemesi yüklenemedi: "</string>
<string name="battery_low">Pil seviyesi düşük</string>
<string name="update_file_invalid">Güncelleme dosyası geçersiz</string>
<string name="incorrect_os_ver">Yanlış işletim sistemi sürümü</string>
@@ -365,10 +361,7 @@
<string name="profile_max_time_off">Maksimum Kapalı Kalma Süresi</string>
<string name="profile_max_time_out_desc">İş profili kapatıldıktan sonra kişisel uygulamalar bu süre boyunca askıya alınacak. 0, sınırsız anlamına gelir.</string>
<string name="personal_app_suspended_because_timeout">Kişisel uygulama şu nedenle askıya alındı: %1$s</string>
<string name="cannot_less_than_72_hours">72 saatten az olamaz</string>
<string name="intent_filter">Niyet Filtresi</string>
<string name="add_intent_filter_work_to_personal">Ekle (işten kişisel profile)</string>
<string name="add_intent_filter_personal_to_work">Ekle (kişisel profilden işe)</string>
<string name="clear_cross_profile_filters">Tüm filtreleri temizle</string>
<string name="org_id">Kurum Kimliği</string>
<string name="length_6_to_64">Uzunluk 6 ile 64 karakter arasında olmalıdır</string>

View File

@@ -74,7 +74,6 @@
<string name="profile_owner">Profile owner</string>
<string name="device_owner">Device owner</string>
<string name="delegated_admins">委托管理员</string>
<string name="delegated_scope">委托作用域</string>
<string name="manage_application_restrictions">管理应用限制</string>
<string name="manage_certificates">管理证书</string>
<string name="select_keychain_certificates">选择密钥链证书</string>
@@ -99,7 +98,6 @@
<string name="disable_account_management">禁用账号管理</string>
<string name="account_type">账号类型</string>
<string name="transfer_ownership">转移所有权</string>
<string name="target_component_name">目标组件名</string>
<string name="lock_screen_info">锁屏提示信息</string>
<string name="support_messages">提供支持的消息</string>
<string name="short_support_msg">提供支持的短消息</string>
@@ -221,8 +219,6 @@
<string name="update_received_time">系统更新接收时间: %1$s</string>
<string name="install_system_update">安装系统更新</string>
<string name="select_ota_package" tools:ignore="TypographyEllipsis">选择OTA包...</string>
<string name="start_install_system_update">开始安装系统更新</string>
<string name="install_system_update_failed">安装系统更新失败:</string>
<string name="battery_low">电量低</string>
<string name="update_file_invalid">OTA包无效</string>
<string name="incorrect_os_ver">系统版本错误</string>
@@ -343,10 +339,11 @@
<string name="profile_max_time_off">资料关闭时间</string>
<string name="profile_max_time_out_desc">工作资料处于关闭状态的时间达到该限制后会挂起个人应用0为无限制</string>
<string name="personal_app_suspended_because_timeout">个人应用已经因此挂起:%1$s</string>
<string name="cannot_less_than_72_hours">不能少于72小时</string>
<string name="intent_filter">Intent过滤器</string>
<string name="add_intent_filter_work_to_personal">添加(工作到个人)</string>
<string name="add_intent_filter_personal_to_work">添加(个人到工作)</string>
<string name="direction">方向</string>
<string name="both_direction">双向</string>
<string name="work_to_personal">工作到个人</string>
<string name="personal_to_work">个人到工作</string>
<string name="clear_cross_profile_filters">清除所有过滤器</string>
<string name="org_id">组织ID</string>
<string name="length_6_to_64">长度应在6~64个字符之间</string>

View File

@@ -79,7 +79,6 @@
<string name="profile_owner">Profile owner</string>
<string name="device_owner">Device owner</string>
<string name="delegated_admins">Delegated admins</string>
<string name="delegated_scope">delegated scope</string>
<string name="manage_application_restrictions">Manage application restrictions</string>
<string name="manage_certificates">Manage certificates</string>
<string name="select_keychain_certificates">Select KeyChain certificate</string>
@@ -105,7 +104,6 @@
<string name="disable_account_management">Disable account management</string>
<string name="account_type">Account type</string>
<string name="transfer_ownership">Transfer Ownership</string>
<string name="target_component_name">Target component name</string>
<string name="lock_screen_info">Lock screen info</string>
<string name="support_messages">Support Messages</string>
<string name="short_support_msg">Short message</string>
@@ -249,8 +247,6 @@
<string name="install_system_update">Install system update</string>
<string name="select_ota_package" tools:ignore="TypographyEllipsis">Select OTA package...</string>
<string name="start_install_system_update">Start installing system update</string>
<string name="install_system_update_failed">"Install system update failed: "</string>
<string name="battery_low">Battery is low</string>
<string name="update_file_invalid">Update file is invalid</string>
<string name="incorrect_os_ver">Incorrect OS version</string>
@@ -374,10 +370,11 @@
<string name="profile_max_time_off">Max time off</string>
<string name="profile_max_time_out_desc">Personal apps will be suspended after the work profile is closed for this amount of time. 0 means no limit. </string>
<string name="personal_app_suspended_because_timeout">Personal app suspended because of this: %1$s</string>
<string name="cannot_less_than_72_hours">Cannot less than 72 hours</string>
<string name="intent_filter">Intent filter</string>
<string name="add_intent_filter_work_to_personal">Add(work to personal)</string>
<string name="add_intent_filter_personal_to_work">Add(personal to work)</string>
<string name="direction">Direction</string>
<string name="both_direction">Both direction</string>
<string name="work_to_personal">Work to personal</string>
<string name="personal_to_work">Personal to work</string>
<string name="clear_cross_profile_filters">Clear all filters</string>
<string name="org_id">Organization ID</string>
<string name="length_6_to_64">The length should be between 6~64 characters</string>