Files
OwnDroid/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt
2024-07-22 18:00:42 +08:00

355 lines
16 KiB
Kotlin

package com.bintianqi.owndroid.dpm
import android.annotation.SuppressLint
import android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE
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_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.app.admin.DevicePolicyManager.WIPE_SILENTLY
import android.content.*
import android.os.Binder
import android.os.Build.VERSION
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.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.Scaffold
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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 androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.ui.Animations
import com.bintianqi.owndroid.ui.CheckBoxItem
import com.bintianqi.owndroid.ui.CopyTextButton
import com.bintianqi.owndroid.ui.SubPageItem
import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.ui.TopBar
@Composable
fun ManagedProfile(navCtrl: NavHostController) {
val localNavCtrl = rememberNavController()
val backStackEntry by localNavCtrl.currentBackStackEntryAsState()
Scaffold(
topBar = {
TopBar(backStackEntry, navCtrl, localNavCtrl)
}
) {
NavHost(
navController = localNavCtrl, startDestination = "Home",
enterTransition = Animations.navHostEnterTransition,
exitTransition = Animations.navHostExitTransition,
popEnterTransition = Animations.navHostPopEnterTransition,
popExitTransition = Animations.navHostPopExitTransition,
modifier = Modifier.padding(top = it.calculateTopPadding())
) {
composable(route = "Home") { Home(localNavCtrl) }
composable(route = "OrgOwnedWorkProfile") { OrgOwnedProfile() }
composable(route = "CreateWorkProfile") { CreateWorkProfile() }
composable(route = "SuspendPersonalApp") { SuspendPersonalApp() }
composable(route = "IntentFilter") { IntentFilter() }
composable(route = "DeleteWorkProfile") { DeleteWorkProfile() }
}
}
}
@Composable
private fun Home(navCtrl: NavHostController) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(start = 30.dp, end = 12.dp)
) {
Text(
text = stringResource(R.string.work_profile),
style = typography.headlineLarge,
modifier = Modifier.padding(top = 8.dp, bottom = 5.dp, start = 15.dp)
)
if(VERSION.SDK_INT >= 30 && context.isProfileOwner && dpm.isManagedProfile(receiver)) {
SubPageItem(R.string.org_owned_work_profile, "", R.drawable.corporate_fare_fill0) { navCtrl.navigate("OrgOwnedWorkProfile") }
}
if(VERSION.SDK_INT<24 || (VERSION.SDK_INT>=24 && dpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE))) {
SubPageItem(R.string.create_work_profile, "", R.drawable.work_fill0) { navCtrl.navigate("CreateWorkProfile") }
}
if(dpm.isOrgProfile(receiver)) {
SubPageItem(R.string.suspend_personal_app, "", R.drawable.block_fill0) { navCtrl.navigate("SuspendPersonalApp") }
}
if(context.isProfileOwner && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver)))) {
SubPageItem(R.string.intent_filter, "", R.drawable.filter_alt_fill0) { navCtrl.navigate("IntentFilter") }
}
if(context.isProfileOwner && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver)))) {
SubPageItem(R.string.delete_work_profile, "", R.drawable.delete_forever_fill0) { navCtrl.navigate("DeleteWorkProfile") }
}
Spacer(Modifier.padding(vertical = 30.dp))
}
}
@Composable
private fun CreateWorkProfile() {
val context = LocalContext.current
val receiver = context.getReceiver()
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 10.dp))
Text(text = stringResource(R.string.create_work_profile), style = typography.headlineLarge)
Spacer(Modifier.padding(vertical = 5.dp))
var skipEncrypt by remember { mutableStateOf(false) }
if(VERSION.SDK_INT>=24) {
CheckBoxItem(stringResource(R.string.skip_encryption), skipEncrypt, { skipEncrypt = 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(VERSION.SDK_INT>=24) { intent.putExtra(EXTRA_PROVISIONING_SKIP_ENCRYPTION,skipEncrypt) }
if(VERSION.SDK_INT>=33) { intent.putExtra(EXTRA_PROVISIONING_ALLOW_OFFLINE,true) }
createManagedProfile.launch(intent)
}catch(e:ActivityNotFoundException) {
Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.create))
}
}
}
@SuppressLint("NewApi")
@Composable
private fun OrgOwnedProfile() {
val context = LocalContext.current
val dpm = context.getDPM()
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 10.dp))
Text(text = stringResource(R.string.org_owned_work_profile), style = typography.headlineLarge)
Spacer(Modifier.padding(vertical = 5.dp))
Text(text = stringResource(R.string.is_org_owned_profile,dpm.isOrganizationOwnedDeviceWithManagedProfile))
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))
}
}
}
@SuppressLint("NewApi")
@Composable
private fun SuspendPersonalApp() {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 10.dp))
SwitchItem(
R.string.suspend_personal_app, "", null,
{ dpm.getPersonalAppsSuspendedReasons(receiver)!=PERSONAL_APPS_NOT_SUSPENDED },
{ dpm.setPersonalAppsSuspended(receiver,it) }
)
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())
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show()
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.apply))
}
}
}
@Composable
private fun IntentFilter() {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) {
var action by remember { mutableStateOf("") }
Spacer(Modifier.padding(vertical = 10.dp))
Text(text = stringResource(R.string.intent_filter), style = typography.headlineLarge)
Spacer(Modifier.padding(vertical = 5.dp))
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)
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show()
},
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)
Toast.makeText(context, R.string.success,Toast.LENGTH_SHORT).show()
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.add_intent_filter_personal_to_work))
}
Spacer(Modifier.padding(vertical = 2.dp))
Button(
onClick = {
dpm.clearCrossProfileIntentFilters(receiver)
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show()
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.clear_cross_profile_filters))
}
}
}
@Composable
private fun DeleteWorkProfile() {
val context = LocalContext.current
val dpm = context.getDPM()
val focusMgr = LocalFocusManager.current
var warning by remember { mutableStateOf(false) }
var externalStorage by remember { mutableStateOf(false) }
var euicc by remember { mutableStateOf(false) }
var silent by remember { mutableStateOf(false) }
var reason by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 10.dp))
Text(
text = stringResource(R.string.delete_work_profile),
style = typography.headlineLarge,
modifier = Modifier.padding(6.dp),color = colorScheme.error
)
Spacer(Modifier.padding(vertical = 5.dp))
CheckBoxItem(stringResource(R.string.wipe_external_storage), externalStorage, { externalStorage = it })
if(VERSION.SDK_INT >= 28) { CheckBoxItem(stringResource(R.string.wipe_euicc), euicc, { euicc = it }) }
if(VERSION.SDK_INT >= 29) { CheckBoxItem(stringResource(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()
warning = true
},
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.delete))
}
Spacer(Modifier.padding(vertical = 30.dp))
}
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 = {
var flag = 0
if(externalStorage) { flag += WIPE_EXTERNAL_STORAGE }
if(euicc && VERSION.SDK_INT >= 28) { flag += WIPE_EUICC }
if(silent && VERSION.SDK_INT >= 29) { flag += WIPE_SILENTLY }
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))
}
}
)
}
}