Files
OwnDroid/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt
BinTianqi 65bf0f75d8 Use pager to split Lock task mode into 3 page
Change version name to v6.3
Update workflow file
Fix a typo in Readme.md
2024-12-31 22:28:40 +08:00

581 lines
24 KiB
Kotlin

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<String>() }
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<Bitmap?>(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
}