ViewModel refactoring: Users part

Bugfix and improvement (#166, #174, #177, #178)
This commit is contained in:
BinTianqi
2025-09-30 21:21:12 +08:00
parent a9452ac14e
commit 43b1314e3a
13 changed files with 471 additions and 349 deletions

View File

@@ -93,27 +93,25 @@ Samsung restricts Android's multiple users feature. There is currently no soluti
## API
OwnDroid provides an API based on Intent and BroadcastReceiver.
OwnDroid provides an Intent-based API. You need to set the API key in settings and enable the API. The numbers in brackets represent the minimum Android version required.
| ID | Extras | Minimum Android version |
|--------------------------|------------------------|:-----------------------:|
| `HIDE` | `package` | |
| `UNHIDE` | `package` | |
| `SUSPEND` | `package` | 7 |
| `UNSUSPEND` | `package` | 7 |
| `ADD_USER_RESTRICTION` | `restriction` | |
| `CLEAR_USER_RESTRICTION` | `restriction` | |
| `SET_PERMISSION_DEFAULT` | `package` `permission` | 6 |
| `SET_PERMISSION_GRANTED` | `package` `permission` | 6 |
| `SET_PERMISSION_DENIED` | `package` `permission` | 6 |
| `SET_CAMERA_DISABLED` | | |
| `SET_CAMERA_ENABLED` | | |
| `SET_USB_DISABLED` | | 12 |
| `SET_USB_ENABLED` | | 12 |
| `LOCK` | | |
| `REBOOT` | | 7 |
[Available user restrictions](https://developer.android.com/reference/android/os/UserManager#constants_1)
- HIDE(package: String)
- UNHIDE(package: String)
- SUSPEND(package: String) (7)
- UNSUSPEND(package: String) (7)
- ADD_USER_RESTRICTION(restriction: Boolean)
- CLEAR_USER_RESTRICTION(restriction: Boolean)
- SET_PERMISSION_DEFAULT(package: String, permission: String) (6)
- SET_PERMISSION_GRANTED(package: String, permission: String) (6)
- SET_PERMISSION_DENIED(package: String, permission: String) (6)
- SET_SCREEN_CAPTURE_DISABLED()
- SET_SCREEN_CAPTURE_ENABLED()
- SET_CAMERA_DISABLED()
- SET_CAMERA_ENABLED()
- SET_USB_DISABLED() (12)
- SET_USB_ENABLED() (12)
- LOCK()
- REBOOT() (7)
```shell
# An example of hiding app in ADB shell
@@ -129,6 +127,8 @@ val intent = Intent("com.bintianqi.owndroid.action.HIDE")
context.sendBroadcast(intent)
```
[Available user restrictions](https://developer.android.com/reference/android/os/UserManager#constants_1)
## Build
You can use Gradle in command line to build OwnDroid.

View File

@@ -91,27 +91,25 @@ user limit reached
## API
OwnDroid提供了一个基于Intent和BroadcastReceiver的API
OwnDroid提供了一个基于Intent的API。你需要在设置中设置密钥并启用API。括号中的数字是最小的安卓版本
| ID | Extra | 最小安卓版本 |
|--------------------------|------------------------|:------:|
| `HIDE` | `package` | |
| `UNHIDE` | `package` | |
| `SUSPEND` | `package` | 7 |
| `UNSUSPEND` | `package` | 7 |
| `ADD_USER_RESTRICTION` | `restriction` | |
| `CLEAR_USER_RESTRICTION` | `restriction` | |
| `SET_PERMISSION_DEFAULT` | `package` `permission` | 6 |
| `SET_PERMISSION_GRANTED` | `package` `permission` | 6 |
| `SET_PERMISSION_DENIED` | `package` `permission` | 6 |
| `SET_CAMERA_DISABLED` | | |
| `SET_CAMERA_ENABLED` | | |
| `SET_USB_DISABLED` | | 12 |
| `SET_USB_ENABLED` | | 12 |
| `LOCK` | | |
| `REBOOT` | | 7 |
[可用的用户限制](https://developer.android.google.cn/reference/android/os/UserManager#constants_1)
- HIDE(package: String)
- UNHIDE(package: String)
- SUSPEND(package: String) (7)
- UNSUSPEND(package: String) (7)
- ADD_USER_RESTRICTION(restriction: Boolean)
- CLEAR_USER_RESTRICTION(restriction: Boolean)
- SET_PERMISSION_DEFAULT(package: String, permission: String) (6)
- SET_PERMISSION_GRANTED(package: String, permission: String) (6)
- SET_PERMISSION_DENIED(package: String, permission: String) (6)
- SET_SCREEN_CAPTURE_DISABLED()
- SET_SCREEN_CAPTURE_ENABLED()
- SET_CAMERA_DISABLED()
- SET_CAMERA_ENABLED()
- SET_USB_DISABLED() (12)
- SET_USB_ENABLED() (12)
- LOCK()
- REBOOT() (7)
```shell
# 一个在ADB shell中隐藏app的示例
@@ -127,6 +125,8 @@ val intent = Intent("com.bintianqi.owndroid.action.HIDE")
context.sendBroadcast(intent)
```
[可用的用户限制](https://developer.android.google.cn/reference/android/os/UserManager#constants_1)
## 构建
你可以在命令行中使用Gradle以构建OwnDroid

View File

@@ -20,13 +20,13 @@ class ApiReceiver: BroadcastReceiver() {
if (!permission.isNullOrEmpty()) log += "\npermission: $permission"
try {
@SuppressWarnings("NewApi")
val ok = when(intent.action?.removePrefix("com.bintianqi.owndroid.action.")) {
when(intent.action?.removePrefix("com.bintianqi.owndroid.action.")) {
"HIDE" -> Privilege.DPM.setApplicationHidden(Privilege.DAR, app, true)
"UNHIDE" -> Privilege.DPM.setApplicationHidden(Privilege.DAR, app, false)
"SUSPEND" -> Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(app), true).isEmpty()
"UNSUSPEND" -> Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(app), false).isEmpty()
"ADD_USER_RESTRICTION" -> { Privilege.DPM.addUserRestriction(Privilege.DAR, restriction); true }
"CLEAR_USER_RESTRICTION" -> { Privilege.DPM.clearUserRestriction(Privilege.DAR, restriction); true }
"SUSPEND" -> Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(app), true)
"UNSUSPEND" -> Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(app), false)
"ADD_USER_RESTRICTION" -> { Privilege.DPM.addUserRestriction(Privilege.DAR, restriction) }
"CLEAR_USER_RESTRICTION" -> { Privilege.DPM.clearUserRestriction(Privilege.DAR, restriction) }
"SET_PERMISSION_DEFAULT" -> {
Privilege.DPM.setPermissionGrantState(
Privilege.DAR, app!!, permission!!,
@@ -45,30 +45,31 @@ class ApiReceiver: BroadcastReceiver() {
DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED
)
}
"LOCK" -> { Privilege.DPM.lockNow(); true }
"REBOOT" -> { Privilege.DPM.reboot(Privilege.DAR); true }
"LOCK" -> { Privilege.DPM.lockNow() }
"REBOOT" -> { Privilege.DPM.reboot(Privilege.DAR) }
"SET_CAMERA_DISABLED" -> {
Privilege.DPM.setCameraDisabled(Privilege.DAR, true)
true
}
"SET_CAMERA_ENABLED" -> {
Privilege.DPM.setCameraDisabled(Privilege.DAR, false)
true
}
"SET_USB_DISABLED" -> {
Privilege.DPM.isUsbDataSignalingEnabled = false
true
}
"SET_USB_ENABLED" -> {
Privilege.DPM.isUsbDataSignalingEnabled = true
true
}
"SET_SCREEN_CAPTURE_DISABLED" -> {
Privilege.DPM.setScreenCaptureDisabled(Privilege.DAR, true)
}
"SET_SCREEN_CAPTURE_ENABLED" -> {
Privilege.DPM.setScreenCaptureDisabled(Privilege.DAR, false)
}
else -> {
log += "\nInvalid action"
false
}
}
log += "\nsuccess: $ok"
} catch(e: Exception) {
e.printStackTrace()
val message = (e::class.qualifiedName ?: "Exception") + ": " + (e.message ?: "")

View File

@@ -547,14 +547,24 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
vm::setUserRestriction, ::navigateUp)
}
composable<Users> { UsersScreen(::navigateUp, ::navigate) }
composable<UserInfo> { UserInfoScreen(::navigateUp) }
composable<UsersOptions> { UsersOptionsScreen(::navigateUp) }
composable<UserOperation> { UserOperationScreen(::navigateUp) }
composable<CreateUser> { CreateUserScreen(::navigateUp) }
composable<ChangeUsername> { ChangeUsernameScreen(::navigateUp) }
composable<UserSessionMessage> { UserSessionMessageScreen(::navigateUp) }
composable<AffiliationId> { AffiliationIdScreen(::navigateUp) }
composable<Users> { UsersScreen(vm, ::navigateUp, ::navigate) }
composable<UserInfo> { UserInfoScreen(vm::getUserInformation, ::navigateUp) }
composable<UsersOptions> {
UsersOptionsScreen(vm::getLogoutEnabled, vm::setLogoutEnabled, ::navigateUp)
}
composable<UserOperation> {
UserOperationScreen(vm::startUser, vm::switchUser, vm::stopUser, vm::deleteUser, ::navigateUp)
}
composable<CreateUser> { CreateUserScreen(vm::createUser, ::navigateUp) }
composable<ChangeUsername> { ChangeUsernameScreen(vm::setProfileName, ::navigateUp) }
composable<UserSessionMessage> {
UserSessionMessageScreen(vm::getUserSessionMessages, vm::setStartUserSessionMessage,
vm::setEndUserSessionMessage, ::navigateUp)
}
composable<AffiliationId> {
AffiliationIdScreen(vm.affiliationIds, vm::getAffiliationIds, vm::setAffiliationId,
::navigateUp)
}
composable<Password> { PasswordScreen(::navigateUp, ::navigate) }
composable<PasswordInfo> { PasswordInfoScreen(::navigateUp) }

View File

@@ -20,9 +20,13 @@ import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.os.Binder
import android.os.Build.VERSION
import android.os.HardwarePropertiesManager
import android.os.UserHandle
import android.os.UserManager
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
@@ -36,6 +40,7 @@ import com.bintianqi.owndroid.Privilege.DPM
import com.bintianqi.owndroid.dpm.ACTIVATE_DEVICE_OWNER_COMMAND
import com.bintianqi.owndroid.dpm.AppStatus
import com.bintianqi.owndroid.dpm.CaCertInfo
import com.bintianqi.owndroid.dpm.CreateUserResult
import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions
import com.bintianqi.owndroid.dpm.DelegatedAdmin
import com.bintianqi.owndroid.dpm.DeviceAdmin
@@ -46,6 +51,7 @@ import com.bintianqi.owndroid.dpm.IntentFilterOptions
import com.bintianqi.owndroid.dpm.PendingSystemUpdateInfo
import com.bintianqi.owndroid.dpm.SystemOptionsStatus
import com.bintianqi.owndroid.dpm.SystemUpdatePolicyInfo
import com.bintianqi.owndroid.dpm.UserInformation
import com.bintianqi.owndroid.dpm.activateOrgProfileCommand
import com.bintianqi.owndroid.dpm.delegatedScopesList
import com.bintianqi.owndroid.dpm.getPackageInstaller
@@ -68,6 +74,8 @@ import java.security.MessageDigest
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.concurrent.Executors
class MyViewModel(application: Application): AndroidViewModel(application) {
@@ -124,11 +132,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
val hiddenPackages = MutableStateFlow(emptyList<AppInfo>())
fun getHiddenPackages() {
viewModelScope.launch {
hiddenPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter {
DPM.isApplicationHidden(DAR, it.packageName)
}.map { getAppInfo(it) }
}
hiddenPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter {
DPM.isApplicationHidden(DAR, it.packageName)
}.map { getAppInfo(it) }
}
fun setPackageHidden(name: String, status: Boolean): Boolean {
val result = DPM.setApplicationHidden(DAR, name, status)
@@ -139,11 +145,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
// Uninstall blocked packages
val ubPackages = MutableStateFlow(emptyList<AppInfo>())
fun getUbPackages() {
viewModelScope.launch {
ubPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter {
DPM.isUninstallBlocked(DAR, it.packageName)
}.map { getAppInfo(it) }
}
ubPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter {
DPM.isUninstallBlocked(DAR, it.packageName)
}.map { getAppInfo(it) }
}
fun setPackageUb(name: String, status: Boolean) {
DPM.setUninstallBlocked(DAR, name, status)
@@ -421,19 +425,33 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
@RequiresApi(24)
fun requestBugReport(): Boolean {
return DPM.requestBugreport(DAR)
return try {
DPM.requestBugreport(DAR)
} catch (e: Exception) {
e.printStackTrace()
false
}
}
@RequiresApi(24)
fun getOrgName(): String {
return DPM.getOrganizationName(DAR).toString()
return try {
DPM.getOrganizationName(DAR)?.toString() ?: ""
} catch (_: Exception) {
""
}
}
@RequiresApi(24)
fun setOrgName(name: String) {
DPM.setOrganizationName(DAR, name)
}
@RequiresApi(31)
fun setOrgId(id: String) {
DPM.setOrganizationId(id)
fun setOrgId(id: String): Boolean {
return try {
DPM.setOrganizationId(id)
true
} catch (_: IllegalStateException) {
false
}
}
@RequiresApi(31)
fun getEnrollmentSpecificId(): String {
@@ -557,12 +575,16 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
properties.temperatures.isEmpty()) {
break
}
hardwareProperties.value = properties
delay(hpRefreshInterval)
}
}
@RequiresApi(28)
fun setTime(time: Long): Boolean {
return DPM.setTime(DAR, time)
fun setTime(time: Long, useCurrentTz: Boolean): Boolean {
val offset = if (useCurrentTz) {
ZonedDateTime.now(ZoneId.systemDefault()).offset.totalSeconds * 1000L
} else 0L
return DPM.setTime(DAR, time - offset)
}
@RequiresApi(28)
fun setTimeZone(tz: String): Boolean {
@@ -674,9 +696,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
val installedCaCerts = MutableStateFlow(emptyList<CaCertInfo>())
fun getCaCerts() {
viewModelScope.launch {
installedCaCerts.value = DPM.getInstalledCaCerts(DAR).mapNotNull { parseCaCert(it) }
}
installedCaCerts.value = DPM.getInstalledCaCerts(DAR).mapNotNull { parseCaCert(it) }
}
fun parseCaCert(uri: Uri): CaCertInfo? {
return try {
@@ -696,7 +716,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
CaCertInfo(
hash, cert.serialNumber.toString(16),
cert.issuerX500Principal.name, cert.subjectX500Principal.name,
parseDate(cert.notBefore), parseDate(cert.notAfter), bytes
cert.notBefore.time, cert.notAfter.time, bytes
)
} catch (e: CertificateException) {
e.printStackTrace()
@@ -809,7 +829,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return DPM.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)
}
fun activateDoByShizuku(callback: (Boolean, String?) -> Unit) {
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
useShizuku(application) { service ->
try {
val result = IUserService.Stub.asInterface(service)
@@ -887,7 +907,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
val dhizukuClients = MutableStateFlow(emptyList<Pair<DhizukuClientInfo, AppInfo>>())
fun getDhizukuClients() {
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
dhizukuClients.value = myRepo.getDhizukuClients().mapNotNull {
val packageName = PM.getNameForUid(it.uid)
if (packageName == null) {
@@ -985,7 +1005,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
val deviceAdminReceivers = MutableStateFlow(emptyList<DeviceAdmin>())
fun getDeviceAdminReceivers() {
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
deviceAdminReceivers.value = PM.queryBroadcastReceivers(
Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED),
PackageManager.GET_META_DATA
@@ -1064,7 +1084,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return intent
}
fun activateOrgProfileByShizuku(callback: (Boolean) -> Unit) {
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
var succeed = false
useShizuku(application) { service ->
val result = IUserService.Stub.asInterface(service).execute(activateOrgProfileCommand)
@@ -1102,6 +1122,130 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
DPM.addCrossProfileIntentFilter(DAR, filter, flags)
}
val UM = application.getSystemService(Context.USER_SERVICE) as UserManager
@RequiresApi(28)
fun getLogoutEnabled(): Boolean {
return DPM.isLogoutEnabled
}
@RequiresApi(28)
fun setLogoutEnabled(enabled: Boolean) {
DPM.setLogoutEnabled(DAR, enabled)
}
fun getUserInformation(): UserInformation {
val uh = Binder.getCallingUserHandle()
return UserInformation(
if (VERSION.SDK_INT >= 24) UserManager.supportsMultipleUsers() else false,
if (VERSION.SDK_INT >= 31) UserManager.isHeadlessSystemUserMode() else false,
if (VERSION.SDK_INT >= 23) UM.isSystemUser else false,
if (VERSION.SDK_INT >= 34) UM.isAdminUser else false,
if (VERSION.SDK_INT >= 25) UM.isDemoUser else false,
if (VERSION.SDK_INT >= 23) UM.getUserCreationTime(uh) else 0,
if (VERSION.SDK_INT >= 28) DPM.isLogoutEnabled else false,
if (VERSION.SDK_INT >= 28) DPM.isEphemeralUser(DAR) else false,
if (VERSION.SDK_INT >= 28) DPM.isAffiliatedUser else false,
UM.getSerialNumberForUser(uh)
)
}
@RequiresApi(28)
fun startUser(id: Int, isUserId: Boolean): Int {
val uh = getUserHandle(id, isUserId)
if (uh == null) return R.string.user_not_exist
return getUserOperationResultText(DPM.startUserInBackground(DAR, uh))
}
fun switchUser(id: Int, isUserId: Boolean): Boolean {
val uh = getUserHandle(id, isUserId)
if (uh == null) return false
DPM.switchUser(DAR, uh)
return true
}
@RequiresApi(28)
fun stopUser(id: Int, isUserId: Boolean): Int {
val uh = getUserHandle(id, isUserId)
if (uh == null) return R.string.user_not_exist
return getUserOperationResultText(DPM.stopUser(DAR, uh))
}
fun deleteUser(id: Int, isUserId: Boolean): Boolean {
val uh = getUserHandle(id, isUserId)
if (uh == null) return false
return DPM.removeUser(DAR, uh)
}
fun getUserHandle(id: Int, isUserId: Boolean): UserHandle? {
return if (isUserId && VERSION.SDK_INT >= 24) {
UserHandle.getUserHandleForUid(id * 100000)
} else {
UM.getUserForSerialNumber(id.toLong())
}
}
fun getUserOperationResultText(code: Int): Int {
return when (code) {
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_MAX_RUNNING_USERS -> R.string.limit_reached
UserManager.USER_OPERATION_ERROR_CURRENT_USER -> R.string.fail_current_user
else -> R.string.unknown
}
}
@RequiresApi(24)
fun createUser(name: String, flags: Int, callback: (CreateUserResult) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
val uh = DPM.createAndManageUser(DAR, name, DAR, null, flags)
if (uh == null) {
callback(CreateUserResult(R.string.failed))
} else {
callback(CreateUserResult(R.string.succeeded, UM.getSerialNumberForUser(uh)))
}
} catch (e: Exception) {
e.printStackTrace()
if (VERSION.SDK_INT >= 28 && e is UserManager.UserOperationException) {
callback(CreateUserResult(getUserOperationResultText(e.userOperationResult)))
} else {
callback(CreateUserResult(R.string.error))
}
}
}
}
val affiliationIds = MutableStateFlow(emptyList<String>())
@RequiresApi(26)
fun getAffiliationIds() {
affiliationIds.value = DPM.getAffiliationIds(DAR).toList()
}
@RequiresApi(26)
fun setAffiliationId(id: String, state: Boolean) {
val newList = affiliationIds.value.run { if (state) plus(id) else minus(id) }
DPM.setAffiliationIds(DAR, newList.toSet())
affiliationIds.value = newList
}
fun setProfileName(name: String) {
DPM.setProfileName(DAR, name)
}
@RequiresApi(23)
fun setUserIcon(bitmap: Bitmap) {
DPM.setUserIcon(DAR, bitmap)
}
@RequiresApi(28)
fun getSecondaryUsers(): List<Long> {
return DPM.getSecondaryUsers(DAR).map { UM.getSerialNumberForUser(it) }
}
@RequiresApi(28)
fun getUserSessionMessages(): Pair<String, String> {
return (DPM.getStartUserSessionMessage(DAR)?.toString() ?: "") to
(DPM.getEndUserSessionMessage(DAR)?.toString() ?: "")
}
@RequiresApi(28)
fun setStartUserSessionMessage(message: String?) {
DPM.setStartUserSessionMessage(DAR, message)
}
@RequiresApi(28)
fun setEndUserSessionMessage(message: String?) {
DPM.setEndUserSessionMessage(DAR, message)
}
@RequiresApi(28)
fun logoutUser(): Int {
return getUserOperationResultText(DPM.logoutUser(DAR))
}
}
data class ThemeSettings(

View File

@@ -9,7 +9,6 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
@@ -23,9 +22,6 @@ import java.io.IOException
import java.io.InputStream
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
@@ -72,20 +68,12 @@ fun formatFileSize(bytes: Long): String {
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)
fun formatTime(ms: Long): String {
return SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(ms))
}
fun formatDate(date: Date): String {
return SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(date)
}
fun parseDate(date: Date): String = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(date)
val Long.humanReadableDate: String
get() = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(Date(this))
fun formatDate(pattern: String, value: Long): String
= SimpleDateFormat(pattern, Locale.getDefault()).format(Date(value))
fun Context.showOperationResultToast(success: Boolean) {
popToast(if(success) R.string.success else R.string.failed)

View File

@@ -53,6 +53,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
@@ -130,9 +131,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.humanReadableDate
import com.bintianqi.owndroid.formatTime
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem
@@ -157,9 +157,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import java.net.InetAddress
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.reflect.jvm.jvmErasure
@Serializable object Network
@@ -1036,14 +1033,14 @@ fun NetworkStatsScreen(
}
}
OutlinedTextField(
value = startTime.let { if(it == -1L) "" else it.humanReadableDate }, onValueChange = {}, readOnly = true,
value = startTime.let { if(it == -1L) "" else formatTime(it) }, onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.start_time)) },
interactionSource = startTimeTextFieldInteractionSource,
isError = startTime >= endTime,
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)
)
OutlinedTextField(
value = endTime.humanReadableDate, onValueChange = {}, readOnly = true,
value = formatTime(endTime), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.end_time)) },
interactionSource = endTimeTextFieldInteractionSource,
isError = startTime >= endTime,
@@ -1315,18 +1312,9 @@ fun NetworkStatsViewerScreen(nsv: NetworkStatsViewer, onNavigateUp: () -> Unit)
HorizontalPager(ps, Modifier.padding(top = 8.dp)) { page ->
val data = nsv.stats[page]
Column(Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)) {
Row(Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
SimpleDateFormat("", Locale.getDefault()).format(Date(data.startTime))
Text(
formatDate("yyyy/MM/dd", data.startTime) + "\n" + formatDate("HH:mm:ss", data.startTime),
textAlign = TextAlign.Center
)
Text("~", Modifier.padding(horizontal = 8.dp))
Text(
formatDate("yyyy/MM/dd", data.endTime) + "\n" + formatDate("HH:mm:ss", data.endTime),
textAlign = TextAlign.Center
)
}
Text(formatTime(data.startTime) + "\n~\n" + formatTime(data.endTime),
Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center)
Spacer(Modifier.height(5.dp))
val txBytes = data.txBytes
Text(stringResource(R.string.transmitted), style = typography.titleLarge)
Column(modifier = Modifier.padding(start = 8.dp, bottom = 4.dp)) {

View File

@@ -112,8 +112,7 @@ import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SP
import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.humanReadableDate
import com.bintianqi.owndroid.parseTimestamp
import com.bintianqi.owndroid.formatTime
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem
@@ -290,7 +289,7 @@ fun SystemManagerScreen(
onClick = {
if (dialog == 3 && VERSION.SDK_INT >= 24) vm.setOrgName(input)
if (dialog == 4 && VERSION.SDK_INT >= 31) {
vm.setOrgId(input)
context.showOperationResultToast(vm.setOrgId(input))
enrollmentSpecificId = vm.getEnrollmentSpecificId()
}
dialog = 0
@@ -368,6 +367,24 @@ fun SystemOptionsScreen(vm: MyViewModel, onNavigateUp: () -> Unit) {
SwitchItem(R.string.enable_usb_signal, status.usbSignalEnabled,
vm::setUsbSignalEnabled, R.drawable.usb_fill0)
}
if (VERSION.SDK_INT >= 23 && VERSION.SDK_INT < 34) {
Row(
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(R.string.status_bar), style = typography.titleMedium)
Button({
vm.setStatusBarDisabled(true)
}, Modifier.padding(horizontal = 4.dp)) {
Text(stringResource(R.string.disable))
}
Button({
vm.setStatusBarDisabled(false)
}) {
Text(stringResource(R.string.enable))
}
}
}
}
if(dialog != 0) AlertDialog(
text = {
@@ -520,7 +537,7 @@ fun HardwareMonitorScreen(
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(28)
@Composable
fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) {
fun ChangeTimeScreen(setTime: (Long, Boolean) -> Boolean, onNavigateUp: () -> Unit) {
val context = LocalContext.current
val focusMgr = LocalFocusManager.current
var tab by remember { mutableIntStateOf(0) }
@@ -528,8 +545,9 @@ fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) {
tab = pagerState.currentPage
val coroutine = rememberCoroutineScope()
var picker by remember { mutableIntStateOf(0) } //0:None, 1:DatePicker, 2:TimePicker
var useCurrentTz by remember { mutableStateOf(true) }
val datePickerState = rememberDatePickerState()
val timePickerState = rememberTimePickerState()
val timePickerState = rememberTimePickerState(is24Hour = true)
val dateInteractionSource = remember { MutableInteractionSource() }
val timeInteractionSource = remember { MutableInteractionSource() }
if(dateInteractionSource.collectIsPressedAsState().value) picker = 1
@@ -571,14 +589,15 @@ fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) {
) {
if(page == 0) {
OutlinedTextField(
value = datePickerState.selectedDateMillis?.humanReadableDate ?: "",
value = datePickerState.selectedDateMillis?.let { formatTime(it) } ?: "",
onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.date)) },
interactionSource = dateInteractionSource,
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = timePickerState.hour.toString() + ":" + timePickerState.minute.toString(),
value = timePickerState.hour.toString().padStart(2, '0') + ":" +
timePickerState.minute.toString().padStart(2, '0'),
onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.time)) },
interactionSource = timeInteractionSource,
@@ -586,11 +605,14 @@ fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) {
.fillMaxWidth()
.padding(vertical = 4.dp)
)
CheckBoxItem(R.string.use_current_timezone, useCurrentTz) {
useCurrentTz = it
}
Button(
onClick = {
val timeMillis = datePickerState.selectedDateMillis!! +
timePickerState.hour * 3600000 + timePickerState.minute * 60000
context.showOperationResultToast(setTime(timeMillis))
context.showOperationResultToast(setTime(timeMillis, useCurrentTz))
},
modifier = Modifier.fillMaxWidth(),
enabled = datePickerState.selectedDateMillis != null
@@ -609,7 +631,7 @@ fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) {
)
Button(
onClick = {
context.showOperationResultToast(setTime(inputTime.toLong()))
context.showOperationResultToast(setTime(inputTime.toLong(), false))
},
modifier = Modifier
.fillMaxWidth()
@@ -653,11 +675,14 @@ fun ChangeTimeZoneScreen(setTimeZone: (String) -> Boolean, onNavigateUp: () -> U
val focusMgr = LocalFocusManager.current
var inputTimezone by remember { mutableStateOf("") }
var dialog by remember { mutableStateOf(false) }
MyScaffold(R.string.change_timezone, onNavigateUp) {
val availableIds = TimeZone.getAvailableIDs()
val validInput = inputTimezone in availableIds
MyScaffold(R.string.change_timezone, onNavigateUp) {
OutlinedTextField(
value = inputTimezone,
label = { Text(stringResource(R.string.timezone_id)) },
onValueChange = { inputTimezone = it },
isError = inputTimezone.isNotEmpty() && !validInput,
trailingIcon = {
IconButton(onClick = { dialog = true }) {
Icon(imageVector = Icons.AutoMirrored.Default.List, contentDescription = null)
@@ -673,7 +698,7 @@ fun ChangeTimeZoneScreen(setTimeZone: (String) -> Boolean, onNavigateUp: () -> U
context.showOperationResultToast(setTimeZone(inputTimezone))
},
modifier = Modifier.fillMaxWidth(),
enabled = inputTimezone.isNotEmpty()
enabled = inputTimezone.isNotEmpty() && validInput
) {
Text(stringResource(R.string.apply))
}
@@ -683,7 +708,7 @@ fun ChangeTimeZoneScreen(setTimeZone: (String) -> Boolean, onNavigateUp: () -> U
if(dialog) AlertDialog(
text = {
LazyColumn {
items(TimeZone.getAvailableIDs()) {
items(availableIds) {
Text(
text = it,
modifier = Modifier
@@ -1322,8 +1347,8 @@ data class CaCertInfo(
val serialNumber: String,
val issuer: String,
val subject: String,
val issuedTime: String,
val expiresTime: String,
val issuedTime: Long,
val expiresTime: Long,
val bytes: ByteArray
)
@@ -1373,8 +1398,7 @@ fun CaCertScreen(
}) {
Icon(Icons.Default.Add, stringResource(R.string.install))
}
},
contentWindowInsets = WindowInsets.ime
}
) { paddingValues ->
LazyColumn(
Modifier
@@ -1388,6 +1412,7 @@ fun CaCertScreen(
.fillMaxWidth()
.clickable {
selectedCaCert = cert
dialog = 2
}
.animateItem()
.padding(vertical = 10.dp, horizontal = 8.dp)
@@ -1412,9 +1437,9 @@ fun CaCertScreen(
Text("Issuer", style = typography.labelLarge)
SelectionContainer { Text(cert.issuer) }
Text("Issued on", style = typography.labelLarge)
SelectionContainer { Text(cert.issuedTime) }
SelectionContainer { Text(formatTime(cert.issuedTime)) }
Text("Expires on", style = typography.labelLarge)
SelectionContainer { Text(cert.expiresTime) }
SelectionContainer { Text(formatTime(cert.expiresTime)) }
Text("SHA-256 fingerprint", style = typography.labelLarge)
SelectionContainer { Text(cert.hash) }
if (dialog == 2) Row(
@@ -1693,17 +1718,17 @@ fun FrpPolicyScreen(
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
modifier = Modifier.fillMaxWidth()
)
}
Button(
onClick = {
focusMgr.clearFocus()
setFrpPolicy(FrpPolicyInfo(true, usePolicy, enabled, accountList))
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Text(stringResource(R.string.apply))
Button(
onClick = {
focusMgr.clearFocus()
setFrpPolicy(FrpPolicyInfo(true, usePolicy, enabled, accountList))
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Text(stringResource(R.string.apply))
}
}
}
Notes(R.string.info_frp_policy, HorizontalPadding)
@@ -1755,7 +1780,7 @@ fun WipeDataScreen(
dialog = 1
},
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth().padding(HorizontalPadding, 5.dp)
) {
Text("WipeData")
}
@@ -1767,7 +1792,7 @@ fun WipeDataScreen(
dialog = 2
},
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth().padding(HorizontalPadding, 5.dp)
) {
Text("WipeDevice")
}
@@ -1905,8 +1930,7 @@ fun SystemUpdatePolicyScreen(
if (VERSION.SDK_INT >= 26) {
Column(Modifier.padding(HorizontalPadding)) {
if (pendingUpdate.exists) {
Text(stringResource(R.string.update_received_time,
parseTimestamp(pendingUpdate.time)))
Text(stringResource(R.string.update_received_time, formatTime(pendingUpdate.time)))
Text(stringResource(R.string.is_security_patch,
stringResource(pendingUpdate.securityPatch.yesOrNo)))
} else {

View File

@@ -1,18 +1,13 @@
package com.bintianqi.owndroid.dpm
import android.app.admin.DevicePolicyManager
import android.content.Context
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 androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
@@ -20,6 +15,7 @@ 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
@@ -44,11 +40,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -62,9 +55,10 @@ 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.parseTimestamp
import com.bintianqi.owndroid.formatTime
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CircularProgressDialog
@@ -77,17 +71,16 @@ 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.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable
@Serializable object Users
@Composable
fun UsersScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
fun UsersScreen(vm: MyViewModel, onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
val context = LocalContext.current
val privilege by Privilege.status.collectAsStateWithLifecycle()
/** 1: secondary users, 2: logout*/
var dialog by remember { mutableIntStateOf(0) }
MyScaffold(R.string.users, onNavigateUp, 0.dp) {
if(VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated) {
@@ -118,7 +111,11 @@ fun UsersScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
context.popToast(R.string.select_an_image)
launcher.launch("image/*")
}
if(changeUserIconDialog) ChangeUserIconDialog(bitmap!!) { changeUserIconDialog = false }
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) }
@@ -127,36 +124,39 @@ fun UsersScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
FunctionItem(R.string.affiliation_id, icon = R.drawable.id_card_fill0) { onNavigate(AffiliationId) }
}
}
if(dialog != 0 && VERSION.SDK_INT >= 28) AlertDialog(
title = { Text(stringResource(if(dialog == 1) R.string.secondary_users else R.string.logout)) },
if (VERSION.SDK_INT >= 28 && dialog == 1) AlertDialog(
title = { Text(stringResource(R.string.secondary_users)) },
text = {
if(dialog == 1) {
val um = context.getSystemService(Context.USER_SERVICE) as UserManager
val list = Privilege.DPM.getSecondaryUsers(Privilege.DAR)
if(list.isEmpty()) {
Text(stringResource(R.string.no_secondary_users))
} else {
Text("(" + stringResource(R.string.serial_number) + ")\n" + list.joinToString("\n") { um.getSerialNumberForUser(it).toString() })
}
val list = vm.getSecondaryUsers()
val text = if (list.isEmpty()) {
stringResource(R.string.no_secondary_users)
} else {
Text(stringResource(R.string.info_logout))
"(" + stringResource(R.string.serial_number) + ")\n" + list.joinToString("\n")
}
Text(text)
},
confirmButton = {
TextButton(
onClick = {
if(dialog == 2) {
val result = Privilege.DPM.logoutUser(Privilege.DAR)
context.popToast(userOperationResultCode(result))
}
dialog = 0
}
) {
TextButton({ dialog = 0 }) {
Text(stringResource(R.string.confirm))
}
},
onDismissRequest = { dialog = 0 }
)
if (VERSION.SDK_INT >= 28 && dialog == 2) 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 = {
if(dialog != 1) TextButton(onClick = { dialog = 0 }) {
TextButton({ dialog = 0 }) {
Text(stringResource(R.string.cancel))
}
},
@@ -167,41 +167,53 @@ fun UsersScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
@Serializable object UsersOptions
@Composable
fun UsersOptionsScreen(onNavigateUp: () -> Unit) {
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, getState = { Privilege.DPM.isLogoutEnabled },
onCheckedChange = { Privilege.DPM.setLogoutEnabled(Privilege.DAR, it) })
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(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val privilege by Privilege.status.collectAsStateWithLifecycle()
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
val user = Process.myUserHandle()
fun UserInfoScreen(getInfo: () -> UserInformation, onNavigateUp: () -> Unit) {
var info by remember { mutableStateOf(UserInformation()) }
var infoDialog by remember { mutableIntStateOf(0) }
LaunchedEffect(Unit) {
info = getInfo()
}
MyScaffold(R.string.user_info, onNavigateUp, 0.dp) {
if(VERSION.SDK_INT >= 24) InfoItem(R.string.support_multiuser, UserManager.supportsMultipleUsers().yesOrNo)
if(VERSION.SDK_INT >= 31) InfoItem(R.string.headless_system_user_mode, UserManager.isHeadlessSystemUserMode().yesOrNo, true) { infoDialog = 1 }
Spacer(Modifier.padding(vertical = 8.dp))
if(VERSION.SDK_INT >= 23) InfoItem(R.string.system_user, userManager.isSystemUser.yesOrNo)
if(VERSION.SDK_INT >= 34) InfoItem(R.string.admin_user, userManager.isAdminUser.yesOrNo)
if(VERSION.SDK_INT >= 25) InfoItem(R.string.demo_user, userManager.isDemoUser.yesOrNo)
if(VERSION.SDK_INT >= 26) userManager.getUserCreationTime(user).let {
if(it != 0L) InfoItem(R.string.creation_time, parseTimestamp(it))
}
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, formatTime(info.time))
if (VERSION.SDK_INT >= 28) {
InfoItem(R.string.logout_enabled, Privilege.DPM.isLogoutEnabled.yesOrNo)
InfoItem(R.string.ephemeral_user, Privilege.DPM.isEphemeralUser(Privilege.DAR).yesOrNo)
InfoItem(R.string.affiliated_user, privilege.affiliated.yesOrNo)
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, userManager.getSerialNumberForUser(Process.myUserHandle()).toString())
InfoItem(R.string.user_serial_number, info.serial.toString())
}
if(infoDialog != 0) AlertDialog(
text = { Text(stringResource(R.string.info_headless_system_user_mode)) },
@@ -217,24 +229,15 @@ fun UserInfoScreen(onNavigateUp: () -> Unit) {
@Serializable object UserOperation
@Composable
fun UserOperationScreen(onNavigateUp: () -> Unit) {
fun UserOperationScreen(
startUser: (Int, Boolean) -> Int, switchUser: (Int, Boolean) -> Boolean,
stopUser: (Int, Boolean) -> Int, deleteUser: (Int, Boolean) -> Boolean, onNavigateUp: () -> Unit
) {
val context = LocalContext.current
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
var input by remember { mutableStateOf("") }
val focusMgr = LocalFocusManager.current
var useUserId by remember { mutableStateOf(false) }
fun withUserHandle(operation: (UserHandle) -> Unit) {
val userHandle = if(useUserId && VERSION.SDK_INT >= 24) {
UserHandle.getUserHandleForUid(input.toInt() * 100000)
} else {
userManager.getUserForSerialNumber(input.toLong())
}
if(userHandle == null) {
context.popToast(R.string.user_not_exist)
} else {
operation(userHandle)
}
}
var dialog by remember { mutableStateOf(false) }
val legalInput = input.toIntOrNull() != null
MyScaffold(R.string.user_operation, onNavigateUp) {
if(VERSION.SDK_INT >= 24) SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
@@ -257,10 +260,7 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Button(
onClick = {
focusMgr.clearFocus()
withUserHandle {
val result = Privilege.DPM.startUserInBackground(Privilege.DAR, it)
context.popToast(userOperationResultCode(result))
}
context.popToast(startUser(input.toInt(), useUserId))
},
enabled = legalInput,
modifier = Modifier.fillMaxWidth()
@@ -272,7 +272,7 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Button(
onClick = {
focusMgr.clearFocus()
withUserHandle { context.showOperationResultToast(Privilege.DPM.switchUser(Privilege.DAR, it)) }
if (switchUser(input.toInt(), useUserId)) context.popToast(R.string.user_not_exist)
},
enabled = legalInput,
modifier = Modifier.fillMaxWidth()
@@ -284,10 +284,7 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Button(
onClick = {
focusMgr.clearFocus()
withUserHandle {
val result = Privilege.DPM.stopUser(Privilege.DAR, it)
context.popToast(userOperationResultCode(result))
}
context.popToast(stopUser(input.toInt(), useUserId))
},
enabled = legalInput,
modifier = Modifier.fillMaxWidth()
@@ -299,14 +296,7 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Button(
onClick = {
focusMgr.clearFocus()
withUserHandle {
if(Privilege.DPM.removeUser(Privilege.DAR, it)) {
context.showOperationResultToast(true)
input = ""
} else {
context.showOperationResultToast(false)
}
}
dialog = true
},
enabled = legalInput,
modifier = Modifier.fillMaxWidth()
@@ -315,21 +305,39 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Text(stringResource(R.string.delete))
}
}
if (dialog) AlertDialog(
text = {
Text(stringResource(R.string.delete_user_confirmation, input))
},
confirmButton = {
TextButton({
context.showOperationResultToast(deleteUser(input.toInt(), useUserId))
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(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
fun CreateUserScreen(
createUser: (String, Int, (CreateUserResult) -> Unit) -> Unit, onNavigateUp: () -> Unit
) {
var result by remember { mutableStateOf<CreateUserResult?>(null) }
val focusMgr = LocalFocusManager.current
var userName by remember { mutableStateOf("") }
var creating by remember { mutableStateOf(false) }
var createdUserSerialNumber by remember { mutableLongStateOf(-1) }
var flag by remember { mutableIntStateOf(0) }
val coroutine = rememberCoroutineScope()
var flags by remember { mutableIntStateOf(0) }
MyScaffold(R.string.create_user, onNavigateUp, 0.dp) {
OutlinedTextField(
userName, { userName= it }, Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
@@ -340,55 +348,47 @@ fun CreateUserScreen(onNavigateUp: () -> Unit) {
Spacer(Modifier.padding(vertical = 5.dp))
FullWidthCheckBoxItem(
R.string.create_user_skip_wizard,
flag and DevicePolicyManager.SKIP_SETUP_WIZARD != 0
) { flag = flag xor DevicePolicyManager.SKIP_SETUP_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,
flag and DevicePolicyManager.MAKE_USER_EPHEMERAL != 0
) { flag = flag xor DevicePolicyManager.MAKE_USER_EPHEMERAL }
flags and DevicePolicyManager.MAKE_USER_EPHEMERAL != 0
) { flags = flags xor DevicePolicyManager.MAKE_USER_EPHEMERAL }
FullWidthCheckBoxItem(
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 }
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
coroutine.launch(Dispatchers.IO) {
try {
val uh = Privilege.DPM.createAndManageUser(Privilege.DAR, userName, Privilege.DAR, null, flag)
withContext(Dispatchers.Main) {
createdUserSerialNumber = userManager.getSerialNumberForUser(uh)
}
} catch(e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
if (VERSION.SDK_INT >= 28 && e is UserManager.UserOperationException) {
context.popToast(e.message ?: context.getString(R.string.error))
} else {
context.showOperationResultToast(false)
}
}
}
withContext(Dispatchers.Main) { creating = false }
createUser(userName, flags) {
creating = false
result = it
}
},
modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)
) {
Text(stringResource(R.string.create))
}
if(createdUserSerialNumber != -1L) AlertDialog(
title = { Text(stringResource(R.string.success)) },
text = { Text(stringResource(R.string.serial_number_of_new_user_is, createdUserSerialNumber)) },
confirmButton = {
TextButton({ createdUserSerialNumber = -1 }) { Text(stringResource(R.string.confirm)) }
if (result != null) AlertDialog(
text = {
Column {
Text(stringResource(result!!.message))
if (result?.serial != -1L) {
Text(stringResource(R.string.serial_number) + ": " + result!!.serial)
}
}
},
onDismissRequest = { createdUserSerialNumber = -1 }
confirmButton = {
TextButton({ result = null }) { Text(stringResource(R.string.confirm)) }
},
onDismissRequest = { result = null }
)
if(creating) CircularProgressDialog { }
if (creating) CircularProgressDialog { }
}
}
@@ -396,24 +396,21 @@ fun CreateUserScreen(onNavigateUp: () -> Unit) {
@RequiresApi(26)
@Composable
fun AffiliationIdScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
fun AffiliationIdScreen(
affiliationIds: StateFlow<List<String>>, getIds: () -> Unit, setId: (String, Boolean) -> Unit,
onNavigateUp: () -> Unit
) {
val focusMgr = LocalFocusManager.current
var input by remember { mutableStateOf("") }
val list = remember { mutableStateListOf<String>() }
val refreshIds = {
list.clear()
list.addAll(Privilege.DPM.getAffiliationIds(Privilege.DAR))
}
LaunchedEffect(Unit) { refreshIds() }
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) { list -= i }
if (list.isEmpty()) Text(stringResource(R.string.none))
for (i in list) {
ListItem(i) { setId(i, false) }
}
}
Spacer(Modifier.padding(vertical = 5.dp))
OutlinedTextField(
value = input,
onValueChange = { input = it },
@@ -421,7 +418,7 @@ fun AffiliationIdScreen(onNavigateUp: () -> Unit) {
trailingIcon = {
IconButton(
onClick = {
list += input
setId(input, true)
input = ""
},
enabled = input.isNotEmpty()
@@ -429,22 +426,10 @@ fun AffiliationIdScreen(onNavigateUp: () -> Unit) {
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.add))
}
},
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() })
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() })
)
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
list.removeAll(setOf(""))
Privilege.DPM.setAffiliationIds(Privilege.DAR, list.toSet())
context.showOperationResultToast(true)
refreshIds()
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.apply))
}
Notes(R.string.info_affiliation_id)
}
}
@@ -452,7 +437,7 @@ fun AffiliationIdScreen(onNavigateUp: () -> Unit) {
@Serializable object ChangeUsername
@Composable
fun ChangeUsernameScreen(onNavigateUp: () -> Unit) {
fun ChangeUsernameScreen(setName: (String) -> Unit, onNavigateUp: () -> Unit) {
val context = LocalContext.current
val focusMgr = LocalFocusManager.current
var inputUsername by remember { mutableStateOf("") }
@@ -468,19 +453,13 @@ fun ChangeUsernameScreen(onNavigateUp: () -> Unit) {
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
Privilege.DPM.setProfileName(Privilege.DAR, inputUsername)
setName(inputUsername)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.apply))
}
Button(
onClick = { Privilege.DPM.setProfileName(Privilege.DAR, null) },
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.reset))
}
}
}
@@ -488,16 +467,19 @@ fun ChangeUsernameScreen(onNavigateUp: () -> Unit) {
@RequiresApi(28)
@Composable
fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
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 remember { mutableStateOf("") }
var end by remember { mutableStateOf("") }
val refreshMsg = {
start = Privilege.DPM.getStartUserSessionMessage(Privilege.DAR)?.toString() ?: ""
end = Privilege.DPM.getEndUserSessionMessage(Privilege.DAR)?.toString() ?: ""
LaunchedEffect(Unit) {
val messages = getMessages()
start = messages.first
end = messages.second
}
LaunchedEffect(Unit) { refreshMsg() }
MyScaffold(R.string.user_session_msg, onNavigateUp) {
OutlinedTextField(
value = start,
@@ -510,8 +492,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button(
onClick = {
Privilege.DPM.setStartUserSessionMessage(Privilege.DAR, start)
refreshMsg()
setStartMessage(start)
},
modifier = Modifier.fillMaxWidth(0.49F)
) {
@@ -519,8 +500,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
}
Button(
onClick = {
Privilege.DPM.setStartUserSessionMessage(Privilege.DAR, null)
refreshMsg()
setStartMessage(null)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth(0.96F)
@@ -540,8 +520,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button(
onClick = {
Privilege.DPM.setEndUserSessionMessage(Privilege.DAR, end)
refreshMsg()
setStartMessage(end)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth(0.49F)
@@ -550,8 +529,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
}
Button(
onClick = {
Privilege.DPM.setEndUserSessionMessage(Privilege.DAR, null)
refreshMsg()
setEndMessage(null)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth(0.96F)
@@ -564,8 +542,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
@RequiresApi(23)
@Composable
private fun ChangeUserIconDialog(bitmap: Bitmap, onClose: () -> Unit) {
val context = LocalContext.current
private fun ChangeUserIconDialog(bitmap: Bitmap, onSet: () -> Unit, onClose: () -> Unit) {
AlertDialog(
title = { Text(stringResource(R.string.change_user_icon)) },
text = {
@@ -577,11 +554,7 @@ private fun ChangeUserIconDialog(bitmap: Bitmap, onClose: () -> Unit) {
}
},
confirmButton = {
TextButton({
Privilege.DPM.setUserIcon(Privilege.DAR, bitmap)
context.showOperationResultToast(true)
onClose()
}) {
TextButton(onSet) {
Text(stringResource(R.string.confirm))
}
},
@@ -593,13 +566,3 @@ private fun ChangeUserIconDialog(bitmap: Bitmap, onClose: () -> Unit) {
onDismissRequest = onClose
)
}
@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
}

View File

@@ -482,7 +482,6 @@
<string name="create_user_skip_wizard">Пропустить мастер настройки</string>
<string name="create_user_ephemeral_user">Временный пользователь</string>
<string name="create_user_enable_all_system_app">Включить все системные приложения</string>
<string name="serial_number_of_new_user_is">Серийный номер этого пользователя: %1$d</string>
<string name="affiliation_id">Аффилированный идентификатор</string>
<string name="change_user_icon">Изменить значок пользователя</string>
<string name="select_an_image">Select an image</string> <!--TODO-->
@@ -625,7 +624,7 @@
<string name="info_device_id_attestation">Указывает, поддерживает ли устройство проверку идентификаторов устройств в дополнение к проверке ключей.</string>
<string name="info_unique_device_attestation">Да, если реализация StrongBox Keymaster на устройстве была обеспечена индивидуальным сертификатом аттестации и может использовать его для подписи записей аттестации (индивидуальный сертификат аттестации могут использовать только Keymaster с уровнем безопасности StrongBox).</string>
<string name="info_org_id">Устанавливает идентификатор предприятия (Enterprise ID). Это необходимо для создания идентификатора устройства, специфичного для регистрации.</string>
<!--TODO--><string name="info_org_id">Устанавливает идентификатор предприятия (Enterprise ID). Это необходимо для создания идентификатора устройства, специфичного для регистрации.</string>
<string name="info_enrollment_specific_id">Идентификатор останется неизменным, даже если рабочий профиль будет удален и создан заново (для той же организации), или если устройство будет сброшено до заводских настроек и перерегистрировано</string>
<string name="info_lock_screen_info">Отобразить краткое сообщение на экране блокировки. Переопределяет любую информацию о владельце, установленную пользователем вручную, и предотвращает ее дальнейшее изменение.</string>
<string name="info_short_support_message">Это будет отображено пользователю на экранах настроек, функциональность которых была отключена администратором. Если длина сообщения превышает 200 символов, оно может быть обрезано.</string>

View File

@@ -507,7 +507,6 @@
<string name="create_user_skip_wizard">Sihirbazı Atla</string>
<string name="create_user_ephemeral_user">Geçici Kullanıcı</string>
<string name="create_user_enable_all_system_app">Tüm Sistem Uygulamalarını Etkinleştir</string>
<string name="serial_number_of_new_user_is">Bu kullanıcının seri numarası: %1$d</string>
<string name="affiliation_id">Bağlılık Kimliği</string>
<string name="change_user_icon">Kullanıcı Simgesini Değiştir</string>
<string name="select_an_image">Bir görüntü seç</string>
@@ -664,7 +663,7 @@
<string name="info_device_id_attestation">Cihazın, anahtar doğrulamasına ek olarak cihaz kimlik doğrulamalarını destekleyip desteklemediğini belirtir.</string>
<string name="info_unique_device_attestation">Evet, eğer cihazdaki StrongBox Keymaster uygulaması bireysel bir doğrulama sertifikasıyla sağlanmışsa ve bunu kullanarak doğrulama kayıtlarını imzalayabiliyorsa (yalnızca StrongBox güvenlik seviyesine sahip Keymaster bireysel doğrulama sertifikası kullanabilir).</string>
<string name="info_org_id">Kurumsal Kimliği ayarlar. Bu, cihaz için kayıt özel bir kimlik oluşturmak için bir gerekliliktir.</string>
<!--TODO--><string name="info_org_id">Kurumsal Kimliği ayarlar. Bu, cihaz için kayıt özel bir kimlik oluşturmak için bir gerekliliktir.</string>
<string name="info_enrollment_specific_id">Kimlik, iş profili kaldırılsa ve aynı Kurum Kimliği ile yeniden oluşturulsa veya cihaz fabrika ayarlarına sıfırlanıp yeniden kaydedilse bile tutarlı kalır.</string>
<string name="info_lock_screen_info">Kilit ekranında kısa bir mesaj gösterir.\nKullanıcı tarafından manuel olarak ayarlanan herhangi bir sahip bilgisini geçersiz kılar ve kullanıcının bunu daha fazla değiştirmesini engeller.</string>
<string name="info_short_support_message">Bu, yönetici tarafından devre dışı bırakılan işlevlerin bulunduğu ayar ekranlarında kullanıcıya gösterilecektir. Mesaj 200 karakterden uzunsa kesilebilir.</string>

View File

@@ -124,6 +124,7 @@
<string name="enable_camera">启用相机</string>
<string name="disable_screen_capture">禁止屏幕捕获</string>
<string name="disable_status_bar">禁用状态栏</string>
<string name="status_bar">状态栏</string>
<string name="auto_time">自动设置时间</string>
<string name="auto_timezone">自动设置时区</string>
<string name="require_auto_time">要求自动时间</string>
@@ -157,6 +158,7 @@
<string name="manually_input">手动输入</string>
<string name="date">日期</string>
<string name="time">时间</string>
<string name="use_current_timezone">使用当前时区</string>
<string name="change_timezone">更改时区</string>
<string name="timezone_id">时区ID</string>
<string name="disable_auto_time_zone_before_set">在设置时区前需要关闭自动时区</string>
@@ -483,12 +485,13 @@
<string name="logout">登出</string>
<string name="start_in_background">在后台启动</string>
<string name="user_operation_switch">切换</string>
<string name="limit_reached">已达到上限</string>
<string name="delete_user_confirmation">你确定要删除用户%1$s吗</string>
<string name="create_user">创建用户</string>
<string name="username">用户名</string>
<string name="create_user_skip_wizard">跳过创建用户向导</string>
<string name="create_user_ephemeral_user">临时用户</string>
<string name="create_user_enable_all_system_app">启用所有系统应用</string>
<string name="serial_number_of_new_user_is">新用户的序列号:%1$d</string>
<string name="affiliation_id">附属用户ID</string>
<string name="change_user_icon">更换用户头像</string>
<string name="select_an_image">选择一个图片</string>
@@ -644,7 +647,7 @@
<string name="info_device_id_attestation">指示设备是否除了密钥证明之外还支持设备标识符证明</string>
<string name="info_unique_device_attestation">如果设备上的StrongBox Keymaster可以配置单独的证明证书并且可以使用该证书签署证明记录则返回true只有StrongBox安全级别的Keymaster才能使用单独的证明证书进行证明</string>
<string name="info_org_id">设置组织ID后才能获取设备注册专用ID</string>
<string name="info_org_id">设置组织ID后才能获取设备注册专用ID。ID只能设置一次。</string>
<string name="info_enrollment_specific_id">不同组织ID的设备注册专用ID不同恢复出厂设置或删除工作资料后不变</string>
<string name="info_lock_screen_info">在锁屏界面上显示的一段简短的消息。将会覆盖用户当前设置的锁屏信息,并且防止用户在系统设置中设置新的锁屏信息</string>
<string name="info_short_support_message">用户试图使用被管理员禁用的功能时会显示此消息。不应多于200字</string>

View File

@@ -133,6 +133,7 @@
<string name="enable_camera">Enable camera</string>
<string name="disable_screen_capture">Disable screen capture</string>
<string name="disable_status_bar">Disable status bar</string>
<string name="status_bar">Status bar</string>
<string name="auto_time">Auto time</string>
<string name="require_auto_time">Require auto time</string>
<string name="auto_timezone">Auto timezone</string>
@@ -166,6 +167,7 @@
<string name="manually_input">Manually input</string>
<string name="date">Date</string>
<string name="time">Time</string>
<string name="use_current_timezone">Use current timezone</string>
<string name="change_timezone">Change timezone</string>
<string name="timezone_id">Timezone ID</string>
<string name="disable_auto_time_zone_before_set">Auto timezone should be disabled before set a custom timezone. </string>
@@ -516,12 +518,13 @@
<string name="logout">Logout</string>
<string name="start_in_background">Start in background</string>
<string name="user_operation_switch">Switch</string>
<string name="limit_reached">Limit reached</string>
<string name="delete_user_confirmation">Are you sure you want to delete user %1$s ?</string>
<string name="create_user">Create user</string>
<string name="username">Username</string>
<string name="create_user_skip_wizard">Skip wizard</string>
<string name="create_user_ephemeral_user">Ephemeral user</string>
<string name="create_user_enable_all_system_app">Enable all system app</string>
<string name="serial_number_of_new_user_is">Serial number of this user: %1$d</string>
<string name="affiliation_id">Affiliation ID</string>
<string name="change_user_icon">Change user icon</string>
<string name="select_an_image">Select an image</string>
@@ -678,7 +681,7 @@
<string name="info_device_id_attestation">Indicates if the device supports attestation of device identifiers in addition to key attestation.</string>
<string name="info_unique_device_attestation">Yes if the StrongBox Keymaster implementation on the device was provisioned with an individual attestation certificate and can sign attestation records using it (only Keymaster with StrongBox security level can use an individual attestation certificate).</string>
<string name="info_org_id">Sets the Enterprise ID. This is a requirement for generating an enrollment-specific ID for the device.</string>
<string name="info_org_id">Sets the Enterprise ID. This is a requirement for generating an enrollment-specific ID for the device. The ID can only be set once.</string>
<string name="info_enrollment_specific_id">The identifier would be consistent even if the work profile is removed and create again (to the same Organization ID), or the device is factory reset and re-enrolled.</string>
<string name="info_lock_screen_info">Show a brief message on your lock screen.\nOverrides any owner information manually set by the user and prevents the user from further changing it.</string>
<string name="info_short_support_message">This will be displayed to the user in settings screens where functionality has been disabled by the admin. If the message is longer than 200 characters it may be truncated</string>