package com.bintianqi.owndroid.dpm import android.app.admin.DevicePolicyManager import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.os.Binder import android.os.Build.VERSION import android.os.Process import android.os.UserHandle import android.os.UserManager import android.provider.MediaStore import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement 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 import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.IconButton 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.mutableStateListOf 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.draw.clip import androidx.compose.ui.graphics.asImageBitmap 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.parseTimestamp import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CardItem import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.uriToStream import com.bintianqi.owndroid.yesOrNo @Composable fun Users(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner var dialog by remember { mutableIntStateOf(0) } MyScaffold(R.string.users, 0.dp, navCtrl) { FunctionItem(R.string.user_info, icon = R.drawable.person_fill0) { navCtrl.navigate("UserInfo") } if(deviceOwner && VERSION.SDK_INT >= 28) { FunctionItem(R.string.secondary_users, icon = R.drawable.list_fill0) { dialog = 1 } FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("UserOptions") } } if(deviceOwner) { FunctionItem(R.string.user_operation, icon = R.drawable.sync_alt_fill0) { navCtrl.navigate("UserOperation") } } if(VERSION.SDK_INT >= 24 && deviceOwner) { FunctionItem(R.string.create_user, icon = R.drawable.person_add_fill0) { navCtrl.navigate("CreateUser") } } if(VERSION.SDK_INT >= 28 && profileOwner && dpm.isAffiliatedUser) { FunctionItem(R.string.logout_current_user, icon = R.drawable.logout_fill0) { dialog = 2 } } if(deviceOwner || profileOwner) { FunctionItem(R.string.change_username, icon = R.drawable.edit_fill0) { navCtrl.navigate("ChangeUsername") } } if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner)) { FunctionItem(R.string.change_user_icon, icon = R.drawable.account_circle_fill0) { navCtrl.navigate("ChangeUserIcon") } } if(VERSION.SDK_INT >= 28 && deviceOwner) { FunctionItem(R.string.user_session_msg, icon = R.drawable.notifications_fill0) { navCtrl.navigate("UserSessionMessage") } } if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { FunctionItem(R.string.affiliation_id, icon = R.drawable.id_card_fill0) { navCtrl.navigate("AffiliationID") } } } if(dialog != 0 && VERSION.SDK_INT >= 28) AlertDialog( title = { Text(stringResource(if(dialog == 1) R.string.secondary_users else R.string.logout_current_user)) }, text = { if(dialog == 1) { val um = context.getSystemService(Context.USER_SERVICE) as UserManager val list = dpm.getSecondaryUsers(receiver) Column { if(list.isEmpty()) { Text(stringResource(R.string.no_secondary_users)) } else { Text("(" + stringResource(R.string.serial_number) + ")") list.forEach { Text(um.getSerialNumberForUser(it).toString()) } } } } }, confirmButton = { TextButton( onClick = { if(dialog == 2) { val result = dpm.logoutUser(receiver) Toast.makeText(context, userOperationResultCode(result), Toast.LENGTH_SHORT).show() } dialog = 0 } ) { Text(stringResource(R.string.confirm)) } }, dismissButton = { if(dialog != 1) TextButton(onClick = { dialog = 0 }) { Text(stringResource(R.string.cancel)) } }, onDismissRequest = { dialog = 0 } ) } @Composable fun UserOptions(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() MyScaffold(R.string.options, 0.dp, navCtrl) { if(VERSION.SDK_INT >= 28) { SwitchItem(R.string.enable_logout, getState = { dpm.isLogoutEnabled }, onCheckedChange = { dpm.setLogoutEnabled(receiver, it) }) } } } @Composable fun CurrentUserInfo(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val user = Process.myUserHandle() var infoDialog by remember { mutableIntStateOf(0) } MyScaffold(R.string.user_info, 8.dp, navCtrl) { if(VERSION.SDK_INT >= 24) CardItem(R.string.support_multiuser, UserManager.supportsMultipleUsers().yesOrNo) if(VERSION.SDK_INT >= 31) CardItem(R.string.headless_system_user_mode, UserManager.isHeadlessSystemUserMode().yesOrNo) { infoDialog = 1 } Spacer(Modifier.padding(vertical = 8.dp)) if(VERSION.SDK_INT >= 23) CardItem(R.string.system_user, userManager.isSystemUser.yesOrNo) if(VERSION.SDK_INT >= 34) CardItem(R.string.admin_user, userManager.isAdminUser.yesOrNo) if(VERSION.SDK_INT >= 25) CardItem(R.string.demo_user, userManager.isDemoUser.yesOrNo) if(VERSION.SDK_INT >= 26) CardItem(R.string.creation_time, parseTimestamp(userManager.getUserCreationTime(user))) if (VERSION.SDK_INT >= 28) { CardItem(R.string.logout_enabled, dpm.isLogoutEnabled.yesOrNo) if(context.isDeviceOwner || context.isProfileOwner) { CardItem(R.string.ephemeral_user, dpm.isEphemeralUser(receiver).yesOrNo) } CardItem(R.string.affiliated_user, dpm.isAffiliatedUser.yesOrNo) } CardItem(R.string.user_id, (Binder.getCallingUid() / 100000).toString()) CardItem(R.string.user_serial_number, userManager.getSerialNumberForUser(Process.myUserHandle()).toString()) } if(infoDialog != 0) AlertDialog( text = { Text(stringResource(R.string.info_headless_system_user_mode)) }, confirmButton = { TextButton(onClick = { infoDialog = 0 }) { Text(stringResource(R.string.confirm)) } }, onDismissRequest = { infoDialog = 0 } ) } @Composable fun UserOperation(navCtrl: NavHostController) { val context = LocalContext.current val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val dpm = context.getDPM() val receiver = context.getReceiver() var idInput by remember { mutableStateOf("") } var useUid by remember { mutableStateOf(false) } val focusMgr = LocalFocusManager.current fun withUserHandle(operation: (UserHandle) -> Unit) { val userHandle = if(useUid && VERSION.SDK_INT >= 24) { UserHandle.getUserHandleForUid(idInput.toInt()) } else { userManager.getUserForSerialNumber(idInput.toLong()) } if(userHandle == null) { Toast.makeText(context, R.string.user_not_exist, Toast.LENGTH_SHORT).show() } else { operation(userHandle) } } val legalInput = try { idInput.toInt() true } catch(_: Exception) { false } MyScaffold(R.string.user_operation, 8.dp, navCtrl) { OutlinedTextField( value = idInput, onValueChange = { idInput = it }, label = { Text(if(useUid) "UID" else stringResource(R.string.serial_number)) }, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }) ) Spacer(Modifier.padding(vertical = 3.dp)) if(VERSION.SDK_INT >= 24) { CheckBoxItem(text = R.string.use_uid, checked = useUid, operation = { idInput=""; useUid = it }) } if(VERSION.SDK_INT >= 28) { Button( onClick = { focusMgr.clearFocus() withUserHandle { val result = dpm.startUserInBackground(receiver, it) Toast.makeText(context, userOperationResultCode(result), Toast.LENGTH_SHORT).show() } }, enabled = legalInput, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.start_in_background)) } } Button( onClick = { focusMgr.clearFocus() withUserHandle { context.showOperationResultToast(dpm.switchUser(receiver, it)) } }, enabled = legalInput, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.user_operation_switch)) } if(VERSION.SDK_INT >= 28) { Button( onClick = { focusMgr.clearFocus() withUserHandle { val result = dpm.stopUser(receiver, it) Toast.makeText(context, userOperationResultCode(result), Toast.LENGTH_SHORT).show() } }, enabled = legalInput, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.stop)) } } Button( onClick = { focusMgr.clearFocus() withUserHandle { if(dpm.removeUser(receiver, it)) { context.showOperationResultToast(true) idInput = "" } else { Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() } } }, enabled = legalInput, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.delete)) } InfoCard(R.string.info_user_operation) } } @RequiresApi(24) @Composable fun CreateUser(navCtrl: NavHostController) { val context = LocalContext.current val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var userName by remember { mutableStateOf("") } var flag by remember { mutableIntStateOf(0) } MyScaffold(R.string.create_user, 8.dp, navCtrl) { OutlinedTextField( value = userName, onValueChange = { userName= it }, label = { Text(stringResource(R.string.username)) }, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }) ) Spacer(Modifier.padding(vertical = 5.dp)) CheckBoxItem( R.string.create_user_skip_wizard, flag and DevicePolicyManager.SKIP_SETUP_WIZARD != 0 ) { flag = flag xor DevicePolicyManager.SKIP_SETUP_WIZARD } if(VERSION.SDK_INT >= 28) { CheckBoxItem( R.string.create_user_ephemeral_user, flag and DevicePolicyManager.MAKE_USER_EPHEMERAL != 0 ) { flag = flag xor DevicePolicyManager.MAKE_USER_EPHEMERAL } CheckBoxItem( R.string.create_user_enable_all_system_app, flag and DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED != 0 ) { flag = flag xor DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED } } var newUserHandle: UserHandle? by remember { mutableStateOf(null) } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { focusMgr.clearFocus() newUserHandle = dpm.createAndManageUser(receiver, userName, receiver, null, flag) context.showOperationResultToast(newUserHandle != null) }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.create)) } Spacer(Modifier.padding(vertical = 5.dp)) if(newUserHandle != null) { Text(text = stringResource(R.string.serial_number_of_new_user_is, userManager.getSerialNumberForUser(newUserHandle))) } } } @RequiresApi(26) @Composable fun AffiliationID(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var input by remember { mutableStateOf("") } val list = remember { mutableStateListOf() } val refreshIds = { list.clear() list.addAll(dpm.getAffiliationIds(receiver)) } LaunchedEffect(Unit) { refreshIds() } MyScaffold(R.string.affiliation_id, 8.dp, navCtrl) { Column(modifier = Modifier.animateContentSize()) { if(list.isEmpty()) Text(stringResource(R.string.none)) for(i in list) { ListItem(i) { list -= i } } } Spacer(Modifier.padding(vertical = 5.dp)) OutlinedTextField( value = input, onValueChange = { input = it }, label = { Text("ID") }, trailingIcon = { IconButton( onClick = { list += input input = "" } ) { Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.add)) } }, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }) ) Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { list.removeAll(listOf("")) dpm.setAffiliationIds(receiver, list.toSet()) context.showOperationResultToast(true) refreshIds() }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.apply)) } InfoCard(R.string.info_affiliated_id) } } @Composable fun ChangeUsername(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var inputUsername by remember { mutableStateOf("") } MyScaffold(R.string.change_username, 8.dp, navCtrl) { OutlinedTextField( value = inputUsername, onValueChange = { inputUsername= it }, label = { Text(stringResource(R.string.username)) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), modifier = Modifier.fillMaxWidth() ) Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { dpm.setProfileName(receiver, inputUsername) context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.apply)) } Button( onClick = { dpm.setProfileName(receiver,null) }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.reset)) } } } @RequiresApi(28) @Composable fun UserSessionMessage(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var start by remember { mutableStateOf("") } var end by remember { mutableStateOf("") } val refreshMsg = { start = dpm.getStartUserSessionMessage(receiver)?.toString() ?: "" end = dpm.getEndUserSessionMessage(receiver)?.toString() ?: "" } LaunchedEffect(Unit) { refreshMsg() } MyScaffold(R.string.user_session_msg, 8.dp, navCtrl) { OutlinedTextField( value = start, onValueChange = { start= it }, label = { Text(stringResource(R.string.start_user_session_msg)) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }), modifier = Modifier.fillMaxWidth().padding(bottom = 2.dp) ) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Button( onClick = { dpm.setStartUserSessionMessage(receiver,start) refreshMsg() }, modifier = Modifier.fillMaxWidth(0.49F) ) { Text(stringResource(R.string.apply)) } Button( onClick = { dpm.setStartUserSessionMessage(receiver,null) refreshMsg() context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.reset)) } } Spacer(Modifier.padding(vertical = 8.dp)) OutlinedTextField( value = end, onValueChange = { end= it }, label = { Text(stringResource(R.string.end_user_session_msg)) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), modifier = Modifier.fillMaxWidth().padding(bottom = 2.dp) ) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Button( onClick = { dpm.setEndUserSessionMessage(receiver,end) refreshMsg() context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.49F) ) { Text(stringResource(R.string.apply)) } Button( onClick = { dpm.setEndUserSessionMessage(receiver,null) refreshMsg() context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.reset)) } } } } @RequiresApi(23) @Composable fun ChangeUserIcon(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var getContent by remember { mutableStateOf(false) } var bitmap by remember { mutableStateOf(null) } val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { it.data?.data?.let { uriToStream(context, it) { stream -> bitmap = BitmapFactory.decodeStream(stream) } } } MyScaffold(R.string.change_user_icon, 8.dp, navCtrl) { CheckBoxItem(R.string.file_picker_instead_gallery, getContent) { getContent = it } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { val intent = Intent(if(getContent) Intent.ACTION_GET_CONTENT else Intent.ACTION_PICK) if(getContent) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*") getFileLauncher.launch(intent) }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.select_picture)) } AnimatedVisibility(visible = bitmap != null, modifier = Modifier.align(Alignment.CenterHorizontally)) { Card(modifier = Modifier.padding(top = 8.dp)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp)) { Image( bitmap = bitmap!!.asImageBitmap(), contentDescription = "User icon", modifier = Modifier.padding(end = 12.dp).size(80.dp).clip(RoundedCornerShape(50)) ) Button( onClick = { dpm.setUserIcon(receiver, bitmap) context.showOperationResultToast(true) } ) { Text(stringResource(R.string.apply)) } } } } } } @StringRes private fun userOperationResultCode(result:Int): Int = when(result) { UserManager.USER_OPERATION_SUCCESS -> R.string.success UserManager.USER_OPERATION_ERROR_UNKNOWN -> R.string.unknown_error UserManager.USER_OPERATION_ERROR_MANAGED_PROFILE-> R.string.fail_managed_profile UserManager.USER_OPERATION_ERROR_CURRENT_USER-> R.string.fail_current_user else -> R.string.unknown }