From c40b94b5871a1bdb28225402fa406b8bf4470606 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 24 Nov 2024 11:22:07 +0800 Subject: [PATCH] Secondary user list Fix crash in User operation when UID/serial number is empty Logout current user in secondary user More user info --- .../main/java/com/bintianqi/owndroid/Utils.kt | 15 +- .../com/bintianqi/owndroid/dpm/Password.kt | 4 +- .../com/bintianqi/owndroid/dpm/Permissions.kt | 6 +- .../com/bintianqi/owndroid/dpm/UserManager.kt | 170 ++++++++++++------ app/src/main/res/drawable/list_fill0.xml | 9 + app/src/main/res/drawable/logout_fill0.xml | 9 + app/src/main/res/values-ru/strings.xml | 7 +- app/src/main/res/values-tr/strings.xml | 7 +- app/src/main/res/values-zh-rCN/strings.xml | 8 +- app/src/main/res/values/strings.xml | 8 +- 10 files changed, 178 insertions(+), 65 deletions(-) create mode 100644 app/src/main/res/drawable/list_fill0.xml create mode 100644 app/src/main/res/drawable/logout_fill0.xml diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index b7b98bd..f2ec0b5 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -14,6 +14,7 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.annotation.StringRes import com.bintianqi.owndroid.dpm.addDeviceAdmin import com.bintianqi.owndroid.dpm.createManagedProfile @@ -22,6 +23,9 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter import java.util.Locale lateinit var getFile: ActivityResultLauncher @@ -123,7 +127,12 @@ fun formatFileSize(bytes: Long): String { } } -@StringRes -fun Boolean.yesOrNo(): Int { - return if(this) R.string.yes else R.string.no +val Boolean.yesOrNo + @StringRes get() = if(this) R.string.yes else R.string.no + +@RequiresApi(26) +fun parseTimestamp(timestamp: Long): String { + val instant = Instant.ofEpochMilli(timestamp) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()) + return formatter.format(instant) } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt index 26bc8bd..69a291d 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt @@ -283,10 +283,10 @@ private fun PasswordInfo() { CardItem(R.string.current_password_complexity, passwordComplexity[dpm.passwordComplexity] ?: R.string.unknown) } if(deviceOwner || profileOwner) { - CardItem(R.string.password_sufficient, dpm.isActivePasswordSufficient.yesOrNo()) + CardItem(R.string.password_sufficient, dpm.isActivePasswordSufficient.yesOrNo) } if(VERSION.SDK_INT >= 28 && profileOwner && dpm.isManagedProfile(receiver)) { - CardItem(R.string.unified_password, dpm.isUsingUnifiedPassword(receiver).yesOrNo()) + CardItem(R.string.unified_password, dpm.isUsingUnifiedPassword(receiver).yesOrNo) } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index da68326..a5677ad 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -496,7 +496,7 @@ fun DeviceInfo() { Text(text = stringResource(R.string.device_info), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 5.dp)) if(VERSION.SDK_INT>=34 && (context.isDeviceOwner || dpm.isOrgProfile(receiver))) { - CardItem(R.string.financed_device, dpm.isDeviceFinanced.yesOrNo()) + CardItem(R.string.financed_device, dpm.isDeviceFinanced.yesOrNo) } if(VERSION.SDK_INT >= 33) { val dpmRole = dpm.devicePolicyManagementRoleHolderPackage @@ -511,10 +511,10 @@ fun DeviceInfo() { if(VERSION.SDK_INT >= 24) { encryptionStatus[DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER] = R.string.es_active_per_user } CardItem(R.string.encryption_status, encryptionStatus[dpm.storageEncryptionStatus] ?: R.string.unknown) if(VERSION.SDK_INT >= 28) { - CardItem(R.string.support_device_id_attestation, dpm.isDeviceIdAttestationSupported.yesOrNo()) { dialog = 1 } + CardItem(R.string.support_device_id_attestation, dpm.isDeviceIdAttestationSupported.yesOrNo) { dialog = 1 } } if (VERSION.SDK_INT >= 30) { - CardItem(R.string.support_unique_device_attestation, dpm.isUniqueDeviceAttestationSupported.yesOrNo()) { dialog = 2 } + CardItem(R.string.support_unique_device_attestation, dpm.isUniqueDeviceAttestationSupported.yesOrNo) { dialog = 2 } } val adminList = dpm.activeAdmins if(adminList != null) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/UserManager.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/UserManager.kt index 38953b2..9c43e24 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserManager.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserManager.kt @@ -14,6 +14,7 @@ import android.os.UserHandle import android.os.UserManager import android.provider.MediaStore import android.widget.Toast +import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image @@ -33,6 +34,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll 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 @@ -41,10 +43,12 @@ 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.collectAsState 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 @@ -68,6 +72,7 @@ import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.fileUriFlow import com.bintianqi.owndroid.getFile +import com.bintianqi.owndroid.parseTimestamp import com.bintianqi.owndroid.toggle import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.CardItem @@ -121,8 +126,12 @@ fun UserManage(navCtrl: NavHostController) { @Composable private fun Home(navCtrl: NavHostController,scrollState: ScrollState) { val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner + //var logoutDialog by remember { mutableStateOf(false) } + var dialog by remember { mutableIntStateOf(0) } Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) { Text( text = stringResource(R.string.user_manager), @@ -131,6 +140,7 @@ private fun Home(navCtrl: NavHostController,scrollState: ScrollState) { ) SubPageItem(R.string.user_info, "", R.drawable.person_fill0) { navCtrl.navigate("UserInfo") } if(deviceOwner && VERSION.SDK_INT >= 28) { + SubPageItem(R.string.secondary_users, "", R.drawable.list_fill0) { dialog = 1 } SubPageItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("Options") } } if(deviceOwner) { @@ -139,6 +149,9 @@ private fun Home(navCtrl: NavHostController,scrollState: ScrollState) { if(VERSION.SDK_INT >= 24 && deviceOwner) { SubPageItem(R.string.create_user, "", R.drawable.person_add_fill0) { navCtrl.navigate("CreateUser") } } + if(VERSION.SDK_INT >= 28 && profileOwner && dpm.isAffiliatedUser) { + SubPageItem(R.string.logout_current_user, "", R.drawable.logout_fill0) { dialog = 2 } + } if(deviceOwner || profileOwner) { SubPageItem(R.string.edit_username, "", R.drawable.edit_fill0) { navCtrl.navigate("EditUsername") } } @@ -154,6 +167,40 @@ private fun Home(navCtrl: NavHostController,scrollState: ScrollState) { Spacer(Modifier.padding(vertical = 30.dp)) LaunchedEffect(Unit) { fileUriFlow.value = Uri.parse("") } } + 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 { + 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 @@ -174,25 +221,39 @@ private fun CurrentUserInfo() { 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) } Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.user_info), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 5.dp)) - if(VERSION.SDK_INT >= 24) CardItem(R.string.support_multiuser, UserManager.supportsMultipleUsers().yesOrNo()) - 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 >= 31) CardItem(R.string.headless_system_user, UserManager.isHeadlessSystemUserMode().yesOrNo()) + 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()) + 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.ephemeral_user, dpm.isEphemeralUser(receiver).yesOrNo) } - CardItem(R.string.affiliated_user, dpm.isAffiliatedUser.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()) Spacer(Modifier.padding(vertical = 30.dp)) } + 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 @@ -201,26 +262,35 @@ private fun UserOperation() { 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 + } Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.user_operation), style = typography.headlineLarge) - var idInput by remember { mutableStateOf("") } - var userHandleById: UserHandle by remember { mutableStateOf(Process.myUserHandle()) } - var useUid by remember { mutableStateOf(false) } Spacer(Modifier.padding(vertical = 5.dp)) OutlinedTextField( value = idInput, onValueChange = { idInput = it - if(useUid) { - if(idInput != "" && VERSION.SDK_INT >= 24) { - userHandleById = UserHandle.getUserHandleForUid(idInput.toInt()) - } - }else{ - val userHandleBySerial = userManager.getUserForSerialNumber(idInput.toLong()) - userHandleById = userHandleBySerial ?: Process.myUserHandle() - } }, label = { Text(if(useUid) "UID" else stringResource(R.string.serial_number)) }, modifier = Modifier.fillMaxWidth(), @@ -231,27 +301,16 @@ private fun UserOperation() { if(VERSION.SDK_INT >= 24) { CheckBoxItem(text = R.string.use_uid, checked = useUid, operation = { idInput=""; useUid = it }) } - Spacer(Modifier.padding(vertical = 5.dp)) - if(VERSION.SDK_INT > 28) { - if(context.isProfileOwner && dpm.isAffiliatedUser) { - Button( - onClick = { - val result = dpm.logoutUser(receiver) - Toast.makeText(context, userOperationResultCode(result, context), Toast.LENGTH_SHORT).show() - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.logout_current_user)) - } - } - } if(VERSION.SDK_INT >= 28) { Button( onClick = { focusMgr.clearFocus() - val result = dpm.startUserInBackground(receiver, userHandleById) - Toast.makeText(context, userOperationResultCode(result, context), Toast.LENGTH_SHORT).show() + 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)) @@ -260,8 +319,11 @@ private fun UserOperation() { Button( onClick = { focusMgr.clearFocus() - Toast.makeText(context, if(dpm.switchUser(receiver,userHandleById)) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + withUserHandle { + Toast.makeText(context, if(dpm.switchUser(receiver, it)) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + } }, + enabled = legalInput, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.user_operation_switch)) @@ -270,13 +332,12 @@ private fun UserOperation() { Button( onClick = { focusMgr.clearFocus() - try{ - val result = dpm.stopUser(receiver,userHandleById) - Toast.makeText(context, userOperationResultCode(result,context), Toast.LENGTH_SHORT).show() - }catch(_: IllegalArgumentException) { - Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() + 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)) @@ -285,13 +346,16 @@ private fun UserOperation() { Button( onClick = { focusMgr.clearFocus() - if(dpm.removeUser(receiver,userHandleById)) { - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - idInput = "" - }else{ - Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() + withUserHandle { + if(dpm.removeUser(receiver, it)) { + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + idInput = "" + } else { + Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() + } } }, + enabled = legalInput, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.delete)) @@ -593,12 +657,12 @@ private fun UserIcon() { } } -private fun userOperationResultCode(result:Int, context: Context): String { - return when(result) { - UserManager.USER_OPERATION_SUCCESS->context.getString(R.string.success) - UserManager.USER_OPERATION_ERROR_UNKNOWN-> context.getString(R.string.unknown_result) - UserManager.USER_OPERATION_ERROR_MANAGED_PROFILE-> context.getString(R.string.fail_managed_profile) - UserManager.USER_OPERATION_ERROR_CURRENT_USER-> context.getString(R.string.fail_current_user) - else->context.getString(R.string.unknown) +@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 } -} diff --git a/app/src/main/res/drawable/list_fill0.xml b/app/src/main/res/drawable/list_fill0.xml new file mode 100644 index 0000000..b877c00 --- /dev/null +++ b/app/src/main/res/drawable/list_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/logout_fill0.xml b/app/src/main/res/drawable/logout_fill0.xml new file mode 100644 index 0000000..ada51d0 --- /dev/null +++ b/app/src/main/res/drawable/logout_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d37e435..bc8b708 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -61,6 +61,7 @@ On Off Alias + Unknown error @@ -451,14 +452,18 @@ Поддержка нескольких пользователей Системный пользователь Пользователь-администратор - Фоновый системный пользователь + Demo user + Headless system user mode + Creation time Выход из системы разрешен Временный пользователь Связанный пользователь Идентификатор пользователя Серийный номер пользователя Операция с пользователем + User does not exist Серийный номер + Secondary users Использовать UID Выйти из текущего пользователя Запустить в фоновом режиме diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 872b062..bf7e7d1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -62,6 +62,7 @@ On Off Alias + Unknown error Etkinleştirmek İçin Tıklayın @@ -449,13 +450,17 @@ Çoklu kullanıcı desteği Sistem kullanıcısı Yönetici kullanıcısı - Başsız sistem kullanıcısı + Demo user + Headless system user mode + Creation time Logout enabled Geçici kullanıcı Bağlı kullanıcı Kullanıcı ID Kullanıcı seri numarası + Secondary users Kullanıcı işlemi + User does not exist Seri numarası UID kullan Mevcut kullanıcıyı çıkış yap diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7fbbc7c..4a862eb 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -59,6 +59,7 @@ 开启 关闭 别名 + 未知错误 点击以激活 @@ -441,13 +442,17 @@ 支持多用户 系统用户 管理员用户 - 无头系统用户 + 演示用户 + 无头系统用户模式 + 创建时间 用户可登出 临时用户 附属用户 当前UserID 当前用户序列号 + 次要用户 用户操作 + 用户不存在 序列号 使用UID 登出当前用户 @@ -626,6 +631,7 @@ 挂起的应用无法被打开,通知会被隐藏,不会在最近任务中显示,不能弹窗,不能发送Toast。\n有些应用无法被挂起,比如Device admin、启动器和默认拨号应用。 用户无法清除这些应用的存储空间,也无法强制停止应用 这个列表中的应用的APK将会一直保留,即使没有任何用户安装这个应用 + 无头系统用户模式意味着系统用户运行系统服务和一些系统UI,但它不与任何真实的人相关联,必须创建额外的用户才能与真实的人相关联。 推荐使用用户序列号来标识用户,如果要使用UID,UID可以是运行在目标用户中任意应用的UID 当Device owner创建并管理用户时,新的用户不是附属用户。Device owner设置和受管理用户完全相同的附属用户ID后,受管理用户成为附属于Device owner的用户 设置一个新的密码,密码的长度需要4位或以上,不输入密码将会清除现有的密码。长度在6位或以下的纯数字密码将会设置为PIN码。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9a77f6..4e81c94 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,6 +62,7 @@ On Off Alias + Unknown error Click to activate @@ -455,13 +456,17 @@ Support multiuser System user Admin user - Headless system user + Demo user + Headless system user mode + Creation time Logout enabled Ephemeral user Affiliated user UserID User serial number + Secondary users User operation + User does not exist Serial number Use UID Logout current user @@ -640,6 +645,7 @@ A suspended package will not be able to start activities. Its notifications will be hidden, it will not show up in recent activities, will not be able to show toasts or dialogs or ring the device.\nSome apps cannot be suspended, such as device admins, the active launcher and the default dialer. User will not be able to clear app data or force-stop packages. Set a list of apps to keep around as APKs even if no user has currently installed it. + Headless system user mode means the system user runs system services and some system UI, but it is not associated with any real person and additional users must be created to be associated with real persons. It is recommended to specify a user with serial number, you can also use UID, the UID should be any of the apps in the target user. When Device owner create a managed user, the managed user isn\'t affiliated. In order to make the managed user affiliated with the Device owner, you should set same affiliated IDs in main user and managed user Set a new lockscreen password. The length of this password must be at least 4 digits. Keep it empty to remove password.\nIf you set a numeric password that length is 6 or lower, it will set as PIN