Files
OwnDroid/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt
BinTianqi 6893ef58aa Content protection policy
Change targetSdk and compileSdk to 35
Fix GitHub action build.yml
Upgrade Compose BOM
2025-01-24 17:58:50 +08:00

342 lines
16 KiB
Kotlin

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.WIPE_EUICC
import android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE
import android.content.*
import android.os.Binder
import android.os.Build.VERSION
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
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.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CardItem
import com.bintianqi.owndroid.ui.CheckBoxItem
import com.bintianqi.owndroid.ui.CopyTextButton
import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.InfoCard
import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.yesOrNo
@Composable
fun WorkProfile(navCtrl: NavHostController) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val profileOwner = context.isProfileOwner
MyScaffold(R.string.work_profile, 0.dp, navCtrl) {
if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) {
FunctionItem(R.string.org_owned_work_profile, icon = R.drawable.corporate_fare_fill0) { navCtrl.navigate("OrgOwnedWorkProfile") }
}
if(VERSION.SDK_INT < 24 || dpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE)) {
FunctionItem(R.string.create_work_profile, icon = R.drawable.work_fill0) { navCtrl.navigate("CreateWorkProfile") }
}
if(dpm.isOrgProfile(receiver)) {
FunctionItem(R.string.suspend_personal_app, icon = R.drawable.block_fill0) { navCtrl.navigate("SuspendPersonalApp") }
}
if(profileOwner && (VERSION.SDK_INT < 24 || dpm.isManagedProfile(receiver))) {
FunctionItem(R.string.intent_filter, icon = R.drawable.filter_alt_fill0) { navCtrl.navigate("IntentFilter") }
}
if(profileOwner && (VERSION.SDK_INT < 24 || dpm.isManagedProfile(receiver))) {
FunctionItem(R.string.delete_work_profile, icon = R.drawable.delete_forever_fill0) { navCtrl.navigate("DeleteWorkProfile") }
}
}
}
@Composable
fun CreateWorkProfile(navCtrl: NavHostController) {
val context = LocalContext.current
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { }
MyScaffold(R.string.create_work_profile, 8.dp, navCtrl) {
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 }
AnimatedVisibility(migrateAccount) {
val fr = FocusRequester()
Column(modifier = Modifier.padding(start = 10.dp)) {
OutlinedTextField(
value = migrateAccountName, onValueChange = { migrateAccountName = it },
label = { Text(stringResource(R.string.account_name)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions { fr.requestFocus() },
modifier = Modifier.fillMaxWidth()
)
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)
)
if(VERSION.SDK_INT >= 26) {
CheckBoxItem(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 }
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,receiver)
} 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) {
Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.create))
}
}
}
@RequiresApi(30)
@Composable
fun OrgOwnedProfile(navCtrl: NavHostController) {
val context = LocalContext.current
val dpm = context.getDPM()
MyScaffold(R.string.org_owned_work_profile, 8.dp, navCtrl, false) {
CardItem(R.string.org_owned_work_profile, dpm.isOrganizationOwnedDeviceWithManagedProfile.yesOrNo)
Spacer(Modifier.padding(vertical = 5.dp))
if(!dpm.isOrganizationOwnedDeviceWithManagedProfile) {
SelectionContainer {
Text(
text = stringResource(R.string.activate_org_profile_command, Binder.getCallingUid()/100000),
color = colorScheme.onTertiaryContainer
)
}
CopyTextButton(R.string.copy_command, stringResource(R.string.activate_org_profile_command, Binder.getCallingUid()/100000))
}
}
}
@RequiresApi(30)
@Composable
fun SuspendPersonalApp(navCtrl: NavHostController) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
var suspend by remember { mutableStateOf(dpm.getPersonalAppsSuspendedReasons(receiver) != PERSONAL_APPS_NOT_SUSPENDED) }
MyScaffold(R.string.suspend_personal_app, 8.dp, navCtrl) {
SwitchItem(R.string.suspend_personal_app, state = suspend,
onCheckedChange = {
dpm.setPersonalAppsSuspended(receiver,it)
suspend = dpm.getPersonalAppsSuspendedReasons(receiver) != PERSONAL_APPS_NOT_SUSPENDED
}, padding = false
)
var time by remember { mutableStateOf("") }
time = dpm.getManagedProfileMaximumTimeOff(receiver).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,
dpm.getPersonalAppsSuspendedReasons(receiver) == PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT
)
)
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 = {
dpm.setManagedProfileMaximumTimeOff(receiver,time.toLong())
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.apply))
}
InfoCard(R.string.info_profile_maximum_time_off)
}
}
@Composable
fun IntentFilter(navCtrl: NavHostController) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
MyScaffold(R.string.intent_filter, 8.dp, navCtrl) {
var action by remember { mutableStateOf("") }
OutlinedTextField(
value = action, onValueChange = { action = it },
label = { Text("Action") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }),
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
dpm.addCrossProfileIntentFilter(receiver, IntentFilter(action), FLAG_PARENT_CAN_ACCESS_MANAGED)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.add_intent_filter_work_to_personal))
}
Button(
onClick = {
dpm.addCrossProfileIntentFilter(receiver, IntentFilter(action), FLAG_MANAGED_CAN_ACCESS_PARENT)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.add_intent_filter_personal_to_work))
}
Spacer(Modifier.padding(vertical = 2.dp))
Button(
onClick = {
dpm.clearCrossProfileIntentFilters(receiver)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.clear_cross_profile_filters))
}
InfoCard(R.string.info_cross_profile_intent_filter)
}
}
@Composable
fun DeleteWorkProfile(navCtrl: NavHostController) {
val context = LocalContext.current
val dpm = context.getDPM()
val focusMgr = LocalFocusManager.current
var flag 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, 8.dp, navCtrl) {
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)
)
}
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
focusMgr.clearFocus()
silent = reason == ""
warning = true
},
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.delete))
}
}
if(warning) {
LaunchedEffect(Unit) { silent = reason == "" }
AlertDialog(
title = {
Text(text = stringResource(R.string.warning), color = colorScheme.error)
},
text = {
Text(text = stringResource(R.string.wipe_work_profile_warning), color = colorScheme.error)
},
onDismissRequest = { warning = false },
confirmButton = {
TextButton(
onClick = {
if(VERSION.SDK_INT >= 28 && !silent) {
dpm.wipeData(flag, reason)
} else {
dpm.wipeData(flag)
}
},
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = { warning = false }) {
Text(stringResource(R.string.cancel))
}
}
)
}
}