Files
OwnDroid/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt
BinTianqi a57b3b3a8e User operation shortcuts
Update dependencies
2025-10-20 17:35:58 +08:00

615 lines
24 KiB
Kotlin

package com.bintianqi.owndroid.dpm
import android.app.admin.DevicePolicyManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Binder
import android.os.Build.VERSION
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
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.height
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.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.painterResource
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.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CircularProgressDialog
import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem
import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.InfoItem
import com.bintianqi.owndroid.ui.ListItem
import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.Notes
import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.uriToStream
import com.bintianqi.owndroid.yesOrNo
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable
@Serializable object Users
@Composable
fun UsersScreen(vm: MyViewModel, onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
val context = LocalContext.current
val privilege by Privilege.status.collectAsStateWithLifecycle()
/** 1: logout */
var dialog by rememberSaveable { mutableIntStateOf(0) }
MyScaffold(R.string.users, onNavigateUp, 0.dp) {
if(VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated) {
FunctionItem(R.string.logout, icon = R.drawable.logout_fill0) { dialog = 1 }
}
FunctionItem(R.string.user_info, icon = R.drawable.person_fill0) { onNavigate(UserInfo) }
if(VERSION.SDK_INT >= 28 && privilege.device) {
FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { onNavigate(UsersOptions) }
}
if(privilege.device) {
FunctionItem(R.string.user_operation, icon = R.drawable.sync_alt_fill0) { onNavigate(UserOperation) }
}
if(VERSION.SDK_INT >= 24 && privilege.device) {
FunctionItem(R.string.create_user, icon = R.drawable.person_add_fill0) { onNavigate(CreateUser) }
}
FunctionItem(R.string.change_username, icon = R.drawable.edit_fill0) { onNavigate(ChangeUsername) }
if(VERSION.SDK_INT >= 23) {
var changeUserIconDialog by remember { mutableStateOf(false) }
var bitmap: Bitmap? by remember { mutableStateOf(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
if(it != null) uriToStream(context, it) { stream ->
bitmap = BitmapFactory.decodeStream(stream)
if(bitmap != null) changeUserIconDialog = true
}
}
FunctionItem(R.string.change_user_icon, icon = R.drawable.account_circle_fill0) {
context.popToast(R.string.select_an_image)
launcher.launch("image/*")
}
if (changeUserIconDialog) ChangeUserIconDialog(
bitmap!!, {
vm.setUserIcon(bitmap!!)
changeUserIconDialog = false
}) { changeUserIconDialog = false }
}
if(VERSION.SDK_INT >= 28 && privilege.device) {
FunctionItem(R.string.user_session_msg, icon = R.drawable.notifications_fill0) { onNavigate(UserSessionMessage) }
}
if(VERSION.SDK_INT >= 26) {
FunctionItem(R.string.affiliation_id, icon = R.drawable.id_card_fill0) { onNavigate(AffiliationId) }
}
}
if (VERSION.SDK_INT >= 28 && dialog == 1) AlertDialog(
title = { Text(stringResource(R.string.logout)) },
text = {
Text(stringResource(R.string.info_logout))
},
confirmButton = {
TextButton({
context.popToast(vm.logoutUser())
dialog = 0
}) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton({ dialog = 0 }) {
Text(stringResource(R.string.cancel))
}
},
onDismissRequest = { dialog = 0 }
)
}
@Serializable object UsersOptions
@Composable
fun UsersOptionsScreen(
getLogoutEnabled: () -> Boolean, setLogoutEnabled: (Boolean) -> Unit, onNavigateUp: () -> Unit
) {
var logoutEnabled by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { logoutEnabled = getLogoutEnabled() }
MyScaffold(R.string.options, onNavigateUp, 0.dp) {
if(VERSION.SDK_INT >= 28) {
SwitchItem(R.string.enable_logout, logoutEnabled, {
setLogoutEnabled(it)
logoutEnabled = it
})
}
}
}
data class UserInformation(
val multiUser: Boolean = false, val headless: Boolean = false, val system: Boolean = false,
val admin: Boolean = false, val demo: Boolean = false, val time: Long = 0,
val logout: Boolean = false, val ephemeral: Boolean = false, val affiliated: Boolean = false,
val serial: Long = 0
)
@Serializable object UserInfo
@Composable
fun UserInfoScreen(getInfo: () -> UserInformation, onNavigateUp: () -> Unit) {
var info by remember { mutableStateOf(UserInformation()) }
var infoDialog by rememberSaveable { mutableIntStateOf(0) }
LaunchedEffect(Unit) {
info = getInfo()
}
MyScaffold(R.string.user_info, onNavigateUp, 0.dp) {
if (VERSION.SDK_INT >= 24) InfoItem(R.string.support_multiuser, info.multiUser.yesOrNo)
if (VERSION.SDK_INT >= 31) InfoItem(R.string.headless_system_user_mode, info.headless.yesOrNo, true) { infoDialog = 1 }
Spacer(Modifier.height(8.dp))
if (VERSION.SDK_INT >= 23) InfoItem(R.string.system_user, info.system.yesOrNo)
if (VERSION.SDK_INT >= 34) InfoItem(R.string.admin_user, info.admin.yesOrNo)
if (VERSION.SDK_INT >= 25) InfoItem(R.string.demo_user, info.demo.yesOrNo)
if (info.time != 0L) InfoItem(R.string.creation_time, formatDate(info.time))
if (VERSION.SDK_INT >= 28) {
InfoItem(R.string.logout_enabled, info.logout.yesOrNo)
InfoItem(R.string.ephemeral_user, info.ephemeral.yesOrNo)
InfoItem(R.string.affiliated_user, info.affiliated.yesOrNo)
}
InfoItem(R.string.user_id, (Binder.getCallingUid() / 100000).toString())
InfoItem(R.string.user_serial_number, info.serial.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 }
)
}
class UserIdentifier(val id: Int, val serial: Long)
enum class UserOperationType {
Start, Switch, Stop, Delete
}
@Serializable object UserOperation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UserOperationScreen(
getUsers: () -> List<UserIdentifier>, doOperation: (UserOperationType, Int, Boolean) -> Boolean,
createShortcut: (UserOperationType, Int, Boolean) -> Boolean, onNavigateUp: () -> Unit
) {
val context = LocalContext.current
var input by rememberSaveable { mutableStateOf("") }
val focusMgr = LocalFocusManager.current
var useUserId by rememberSaveable { mutableStateOf(false) }
var dialog by rememberSaveable { mutableStateOf(false) }
var menu by remember { mutableStateOf(false) }
val legalInput = input.toIntOrNull() != null
val identifiers = remember { mutableStateListOf<UserIdentifier>() }
@Composable
fun CreateShortcutIcon(type: UserOperationType) {
FilledTonalIconButton({
if (!createShortcut(type, input.toInt(), useUserId))
context.showOperationResultToast(false)
}, enabled = legalInput) {
Icon(painterResource(R.drawable.open_in_new), null)
}
}
LaunchedEffect(Unit) {
identifiers.addAll(getUsers())
}
MyScaffold(R.string.user_operation, onNavigateUp) {
if (VERSION.SDK_INT >= 24) SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) {
SegmentedButton(!useUserId, { useUserId = false }, SegmentedButtonDefaults.itemShape(0, 2)) {
Text(stringResource(R.string.serial_number))
}
SegmentedButton(useUserId, { useUserId = true }, SegmentedButtonDefaults.itemShape(1, 2)) {
Text(stringResource(R.string.user_id))
}
}
ExposedDropdownMenuBox(menu, { menu = it }) {
OutlinedTextField(
input, { input = it },
Modifier
.fillMaxWidth()
.menuAnchor(MenuAnchorType.PrimaryEditable)
.padding(top = 4.dp, bottom = 8.dp),
label = {
Text(stringResource(if(useUserId) R.string.user_id else R.string.serial_number))
},
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(menu)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done)
)
ExposedDropdownMenu(menu, { menu = false }) {
if (identifiers.isEmpty()) {
DropdownMenuItem(
{ Text(stringResource(R.string.no_secondary_users)) }, {}
)
} else {
identifiers.forEach {
val text = (if (useUserId) it.id else it.serial).toString()
DropdownMenuItem(
{ Text(text) },
{
input = text
menu = false
}
)
}
}
}
}
if (VERSION.SDK_INT >= 28) Row {
Button(
{
focusMgr.clearFocus()
val result = doOperation(UserOperationType.Start, input.toInt(), useUserId)
context.showOperationResultToast(result)
},
Modifier.weight(1F),
legalInput
) {
Icon(Icons.Default.PlayArrow, null, Modifier.padding(end = 4.dp))
Text(stringResource(R.string.start_in_background))
}
CreateShortcutIcon(UserOperationType.Start)
}
Row {
Button(
{
focusMgr.clearFocus()
val result = doOperation(UserOperationType.Switch, input.toInt(), useUserId)
context.showOperationResultToast(result)
},
Modifier.weight(1F),
legalInput
) {
Icon(painterResource(R.drawable.sync_alt_fill0), null, Modifier.padding(end = 4.dp))
Text(stringResource(R.string.user_operation_switch))
}
CreateShortcutIcon(UserOperationType.Switch)
}
if (VERSION.SDK_INT >= 28) Row {
Button(
{
focusMgr.clearFocus()
val result = doOperation(UserOperationType.Stop, input.toInt(), useUserId)
context.showOperationResultToast(result)
},
Modifier.weight(1F),
legalInput
) {
Icon(Icons.Default.Close, null, Modifier.padding(end = 4.dp))
Text(stringResource(R.string.stop))
}
CreateShortcutIcon(UserOperationType.Stop)
}
Button(
onClick = {
focusMgr.clearFocus()
dialog = true
},
enabled = legalInput,
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Delete, null, Modifier.padding(end = 4.dp))
Text(stringResource(R.string.delete))
}
}
if (dialog) AlertDialog(
text = {
Text(stringResource(R.string.delete_user_confirmation, input))
},
confirmButton = {
TextButton({
val result = doOperation(UserOperationType.Delete, input.toInt(), useUserId)
context.showOperationResultToast(result)
dialog = false
}) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton({ dialog = false }) { Text(stringResource(R.string.cancel)) }
},
onDismissRequest = { dialog = false }
)
}
data class CreateUserResult(val message: Int, val serial: Long = -1)
@Serializable object CreateUser
@RequiresApi(24)
@Composable
fun CreateUserScreen(
createUser: (String, Int, (CreateUserResult) -> Unit) -> Unit, onNavigateUp: () -> Unit
) {
var result by remember { mutableStateOf<CreateUserResult?>(null) }
val focusMgr = LocalFocusManager.current
var userName by rememberSaveable { mutableStateOf("") }
var creating by rememberSaveable { mutableStateOf(false) }
var flags by rememberSaveable { mutableIntStateOf(0) }
MyScaffold(R.string.create_user, onNavigateUp, 0.dp) {
OutlinedTextField(
userName, { userName= it }, Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
label = { Text(stringResource(R.string.username)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() })
)
Spacer(Modifier.padding(vertical = 5.dp))
FullWidthCheckBoxItem(
R.string.create_user_skip_wizard,
flags and DevicePolicyManager.SKIP_SETUP_WIZARD != 0
) { flags = flags xor DevicePolicyManager.SKIP_SETUP_WIZARD }
if(VERSION.SDK_INT >= 28) {
FullWidthCheckBoxItem(
R.string.create_user_ephemeral_user,
flags and DevicePolicyManager.MAKE_USER_EPHEMERAL != 0
) { flags = flags xor DevicePolicyManager.MAKE_USER_EPHEMERAL }
FullWidthCheckBoxItem(
R.string.create_user_enable_all_system_app,
flags and DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED != 0
) { flags = flags xor DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED }
}
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
focusMgr.clearFocus()
creating = true
createUser(userName, flags) {
creating = false
result = it
}
},
modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)
) {
Text(stringResource(R.string.create))
}
if (result != null) AlertDialog(
text = {
Column {
Text(stringResource(result!!.message))
if (result?.serial != -1L) {
Text(stringResource(R.string.serial_number) + ": " + result!!.serial)
}
}
},
confirmButton = {
TextButton({ result = null }) { Text(stringResource(R.string.confirm)) }
},
onDismissRequest = { result = null }
)
if (creating) CircularProgressDialog { }
}
}
@Serializable object AffiliationId
@RequiresApi(26)
@Composable
fun AffiliationIdScreen(
affiliationIds: StateFlow<List<String>>, getIds: () -> Unit, setId: (String, Boolean) -> Unit,
onNavigateUp: () -> Unit
) {
val focusMgr = LocalFocusManager.current
var input by rememberSaveable { mutableStateOf("") }
val list by affiliationIds.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { getIds() }
MyScaffold(R.string.affiliation_id, onNavigateUp) {
Column(modifier = Modifier.animateContentSize()) {
if (list.isEmpty()) Text(stringResource(R.string.none))
for (i in list) {
ListItem(i) { setId(i, false) }
}
}
OutlinedTextField(
value = input,
onValueChange = { input = it },
label = { Text("ID") },
trailingIcon = {
IconButton(
onClick = {
setId(input, true)
input = ""
},
enabled = input.isNotEmpty()
) {
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.add))
}
},
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() })
)
Notes(R.string.info_affiliation_id)
}
}
@Serializable object ChangeUsername
@Composable
fun ChangeUsernameScreen(setName: (String) -> Unit, onNavigateUp: () -> Unit) {
val context = LocalContext.current
val focusMgr = LocalFocusManager.current
var inputUsername by rememberSaveable { mutableStateOf("") }
MyScaffold(R.string.change_username, onNavigateUp) {
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 = {
setName(inputUsername)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.apply))
}
}
}
@Serializable object UserSessionMessage
@RequiresApi(28)
@Composable
fun UserSessionMessageScreen(
getMessages: () -> Pair<String, String>, setStartMessage: (String?) -> Unit,
setEndMessage: (String?) -> Unit, onNavigateUp: () -> Unit
) {
val context = LocalContext.current
val focusMgr = LocalFocusManager.current
var start by rememberSaveable { mutableStateOf("") }
var end by rememberSaveable { mutableStateOf("") }
LaunchedEffect(Unit) {
val messages = getMessages()
start = messages.first
end = messages.second
}
MyScaffold(R.string.user_session_msg, onNavigateUp) {
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 = {
setStartMessage(start)
},
modifier = Modifier.fillMaxWidth(0.49F)
) {
Text(stringResource(R.string.apply))
}
Button(
onClick = {
setStartMessage(null)
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 = {
setStartMessage(end)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth(0.49F)
) {
Text(stringResource(R.string.apply))
}
Button(
onClick = {
setEndMessage(null)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth(0.96F)
) {
Text(stringResource(R.string.reset))
}
}
}
}
@RequiresApi(23)
@Composable
private fun ChangeUserIconDialog(bitmap: Bitmap, onSet: () -> Unit, onClose: () -> Unit) {
AlertDialog(
title = { Text(stringResource(R.string.change_user_icon)) },
text = {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Image(
bitmap = bitmap.asImageBitmap(), contentDescription = null,
modifier = Modifier.size(80.dp).clip(RoundedCornerShape(50))
)
}
},
confirmButton = {
TextButton(onSet) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(onClose) {
Text(stringResource(R.string.cancel))
}
},
onDismissRequest = onClose
)
}