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 ## 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: String)
|--------------------------|------------------------|:-----------------------:| - UNHIDE(package: String)
| `HIDE` | `package` | | - SUSPEND(package: String) (7)
| `UNHIDE` | `package` | | - UNSUSPEND(package: String) (7)
| `SUSPEND` | `package` | 7 | - ADD_USER_RESTRICTION(restriction: Boolean)
| `UNSUSPEND` | `package` | 7 | - CLEAR_USER_RESTRICTION(restriction: Boolean)
| `ADD_USER_RESTRICTION` | `restriction` | | - SET_PERMISSION_DEFAULT(package: String, permission: String) (6)
| `CLEAR_USER_RESTRICTION` | `restriction` | | - SET_PERMISSION_GRANTED(package: String, permission: String) (6)
| `SET_PERMISSION_DEFAULT` | `package` `permission` | 6 | - SET_PERMISSION_DENIED(package: String, permission: String) (6)
| `SET_PERMISSION_GRANTED` | `package` `permission` | 6 | - SET_SCREEN_CAPTURE_DISABLED()
| `SET_PERMISSION_DENIED` | `package` `permission` | 6 | - SET_SCREEN_CAPTURE_ENABLED()
| `SET_CAMERA_DISABLED` | | | - SET_CAMERA_DISABLED()
| `SET_CAMERA_ENABLED` | | | - SET_CAMERA_ENABLED()
| `SET_USB_DISABLED` | | 12 | - SET_USB_DISABLED() (12)
| `SET_USB_ENABLED` | | 12 | - SET_USB_ENABLED() (12)
| `LOCK` | | | - LOCK()
| `REBOOT` | | 7 | - REBOOT() (7)
[Available user restrictions](https://developer.android.com/reference/android/os/UserManager#constants_1)
```shell ```shell
# An example of hiding app in ADB shell # An example of hiding app in ADB shell
@@ -129,6 +127,8 @@ val intent = Intent("com.bintianqi.owndroid.action.HIDE")
context.sendBroadcast(intent) context.sendBroadcast(intent)
``` ```
[Available user restrictions](https://developer.android.com/reference/android/os/UserManager#constants_1)
## Build ## Build
You can use Gradle in command line to build OwnDroid. You can use Gradle in command line to build OwnDroid.

View File

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

View File

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

View File

@@ -547,14 +547,24 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
vm::setUserRestriction, ::navigateUp) vm::setUserRestriction, ::navigateUp)
} }
composable<Users> { UsersScreen(::navigateUp, ::navigate) } composable<Users> { UsersScreen(vm, ::navigateUp, ::navigate) }
composable<UserInfo> { UserInfoScreen(::navigateUp) } composable<UserInfo> { UserInfoScreen(vm::getUserInformation, ::navigateUp) }
composable<UsersOptions> { UsersOptionsScreen(::navigateUp) } composable<UsersOptions> {
composable<UserOperation> { UserOperationScreen(::navigateUp) } UsersOptionsScreen(vm::getLogoutEnabled, vm::setLogoutEnabled, ::navigateUp)
composable<CreateUser> { CreateUserScreen(::navigateUp) } }
composable<ChangeUsername> { ChangeUsernameScreen(::navigateUp) } composable<UserOperation> {
composable<UserSessionMessage> { UserSessionMessageScreen(::navigateUp) } UserOperationScreen(vm::startUser, vm::switchUser, vm::stopUser, vm::deleteUser, ::navigateUp)
composable<AffiliationId> { AffiliationIdScreen(::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<Password> { PasswordScreen(::navigateUp, ::navigate) }
composable<PasswordInfo> { PasswordInfoScreen(::navigateUp) } composable<PasswordInfo> { PasswordInfoScreen(::navigateUp) }

View File

@@ -20,9 +20,13 @@ import android.content.IntentFilter
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Binder
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.HardwarePropertiesManager import android.os.HardwarePropertiesManager
import android.os.UserHandle
import android.os.UserManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb 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.ACTIVATE_DEVICE_OWNER_COMMAND
import com.bintianqi.owndroid.dpm.AppStatus import com.bintianqi.owndroid.dpm.AppStatus
import com.bintianqi.owndroid.dpm.CaCertInfo import com.bintianqi.owndroid.dpm.CaCertInfo
import com.bintianqi.owndroid.dpm.CreateUserResult
import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions
import com.bintianqi.owndroid.dpm.DelegatedAdmin import com.bintianqi.owndroid.dpm.DelegatedAdmin
import com.bintianqi.owndroid.dpm.DeviceAdmin 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.PendingSystemUpdateInfo
import com.bintianqi.owndroid.dpm.SystemOptionsStatus import com.bintianqi.owndroid.dpm.SystemOptionsStatus
import com.bintianqi.owndroid.dpm.SystemUpdatePolicyInfo import com.bintianqi.owndroid.dpm.SystemUpdatePolicyInfo
import com.bintianqi.owndroid.dpm.UserInformation
import com.bintianqi.owndroid.dpm.activateOrgProfileCommand import com.bintianqi.owndroid.dpm.activateOrgProfileCommand
import com.bintianqi.owndroid.dpm.delegatedScopesList import com.bintianqi.owndroid.dpm.delegatedScopesList
import com.bintianqi.owndroid.dpm.getPackageInstaller import com.bintianqi.owndroid.dpm.getPackageInstaller
@@ -68,6 +74,8 @@ import java.security.MessageDigest
import java.security.cert.CertificateException import java.security.cert.CertificateException
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.concurrent.Executors import java.util.concurrent.Executors
class MyViewModel(application: Application): AndroidViewModel(application) { class MyViewModel(application: Application): AndroidViewModel(application) {
@@ -124,12 +132,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
val hiddenPackages = MutableStateFlow(emptyList<AppInfo>()) val hiddenPackages = MutableStateFlow(emptyList<AppInfo>())
fun getHiddenPackages() { fun getHiddenPackages() {
viewModelScope.launch {
hiddenPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter { hiddenPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter {
DPM.isApplicationHidden(DAR, it.packageName) DPM.isApplicationHidden(DAR, it.packageName)
}.map { getAppInfo(it) } }.map { getAppInfo(it) }
} }
}
fun setPackageHidden(name: String, status: Boolean): Boolean { fun setPackageHidden(name: String, status: Boolean): Boolean {
val result = DPM.setApplicationHidden(DAR, name, status) val result = DPM.setApplicationHidden(DAR, name, status)
getHiddenPackages() getHiddenPackages()
@@ -139,12 +145,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
// Uninstall blocked packages // Uninstall blocked packages
val ubPackages = MutableStateFlow(emptyList<AppInfo>()) val ubPackages = MutableStateFlow(emptyList<AppInfo>())
fun getUbPackages() { fun getUbPackages() {
viewModelScope.launch {
ubPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter { ubPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter {
DPM.isUninstallBlocked(DAR, it.packageName) DPM.isUninstallBlocked(DAR, it.packageName)
}.map { getAppInfo(it) } }.map { getAppInfo(it) }
} }
}
fun setPackageUb(name: String, status: Boolean) { fun setPackageUb(name: String, status: Boolean) {
DPM.setUninstallBlocked(DAR, name, status) DPM.setUninstallBlocked(DAR, name, status)
getUbPackages() getUbPackages()
@@ -421,19 +425,33 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
} }
@RequiresApi(24) @RequiresApi(24)
fun requestBugReport(): Boolean { fun requestBugReport(): Boolean {
return DPM.requestBugreport(DAR) return try {
DPM.requestBugreport(DAR)
} catch (e: Exception) {
e.printStackTrace()
false
}
} }
@RequiresApi(24) @RequiresApi(24)
fun getOrgName(): String { fun getOrgName(): String {
return DPM.getOrganizationName(DAR).toString() return try {
DPM.getOrganizationName(DAR)?.toString() ?: ""
} catch (_: Exception) {
""
}
} }
@RequiresApi(24) @RequiresApi(24)
fun setOrgName(name: String) { fun setOrgName(name: String) {
DPM.setOrganizationName(DAR, name) DPM.setOrganizationName(DAR, name)
} }
@RequiresApi(31) @RequiresApi(31)
fun setOrgId(id: String) { fun setOrgId(id: String): Boolean {
return try {
DPM.setOrganizationId(id) DPM.setOrganizationId(id)
true
} catch (_: IllegalStateException) {
false
}
} }
@RequiresApi(31) @RequiresApi(31)
fun getEnrollmentSpecificId(): String { fun getEnrollmentSpecificId(): String {
@@ -557,12 +575,16 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
properties.temperatures.isEmpty()) { properties.temperatures.isEmpty()) {
break break
} }
hardwareProperties.value = properties
delay(hpRefreshInterval) delay(hpRefreshInterval)
} }
} }
@RequiresApi(28) @RequiresApi(28)
fun setTime(time: Long): Boolean { fun setTime(time: Long, useCurrentTz: Boolean): Boolean {
return DPM.setTime(DAR, time) val offset = if (useCurrentTz) {
ZonedDateTime.now(ZoneId.systemDefault()).offset.totalSeconds * 1000L
} else 0L
return DPM.setTime(DAR, time - offset)
} }
@RequiresApi(28) @RequiresApi(28)
fun setTimeZone(tz: String): Boolean { fun setTimeZone(tz: String): Boolean {
@@ -674,10 +696,8 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
} }
val installedCaCerts = MutableStateFlow(emptyList<CaCertInfo>()) val installedCaCerts = MutableStateFlow(emptyList<CaCertInfo>())
fun getCaCerts() { 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? { fun parseCaCert(uri: Uri): CaCertInfo? {
return try { return try {
application.contentResolver.openInputStream(uri)?.use { application.contentResolver.openInputStream(uri)?.use {
@@ -696,7 +716,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
CaCertInfo( CaCertInfo(
hash, cert.serialNumber.toString(16), hash, cert.serialNumber.toString(16),
cert.issuerX500Principal.name, cert.subjectX500Principal.name, cert.issuerX500Principal.name, cert.subjectX500Principal.name,
parseDate(cert.notBefore), parseDate(cert.notAfter), bytes cert.notBefore.time, cert.notAfter.time, bytes
) )
} catch (e: CertificateException) { } catch (e: CertificateException) {
e.printStackTrace() e.printStackTrace()
@@ -809,7 +829,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return DPM.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE) return DPM.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)
} }
fun activateDoByShizuku(callback: (Boolean, String?) -> Unit) { fun activateDoByShizuku(callback: (Boolean, String?) -> Unit) {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
useShizuku(application) { service -> useShizuku(application) { service ->
try { try {
val result = IUserService.Stub.asInterface(service) val result = IUserService.Stub.asInterface(service)
@@ -887,7 +907,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
} }
val dhizukuClients = MutableStateFlow(emptyList<Pair<DhizukuClientInfo, AppInfo>>()) val dhizukuClients = MutableStateFlow(emptyList<Pair<DhizukuClientInfo, AppInfo>>())
fun getDhizukuClients() { fun getDhizukuClients() {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
dhizukuClients.value = myRepo.getDhizukuClients().mapNotNull { dhizukuClients.value = myRepo.getDhizukuClients().mapNotNull {
val packageName = PM.getNameForUid(it.uid) val packageName = PM.getNameForUid(it.uid)
if (packageName == null) { if (packageName == null) {
@@ -985,7 +1005,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
} }
val deviceAdminReceivers = MutableStateFlow(emptyList<DeviceAdmin>()) val deviceAdminReceivers = MutableStateFlow(emptyList<DeviceAdmin>())
fun getDeviceAdminReceivers() { fun getDeviceAdminReceivers() {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
deviceAdminReceivers.value = PM.queryBroadcastReceivers( deviceAdminReceivers.value = PM.queryBroadcastReceivers(
Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED),
PackageManager.GET_META_DATA PackageManager.GET_META_DATA
@@ -1064,7 +1084,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return intent return intent
} }
fun activateOrgProfileByShizuku(callback: (Boolean) -> Unit) { fun activateOrgProfileByShizuku(callback: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
var succeed = false var succeed = false
useShizuku(application) { service -> useShizuku(application) { service ->
val result = IUserService.Stub.asInterface(service).execute(activateOrgProfileCommand) val result = IUserService.Stub.asInterface(service).execute(activateOrgProfileCommand)
@@ -1102,6 +1122,130 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
} }
DPM.addCrossProfileIntentFilter(DAR, filter, flags) 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( data class ThemeSettings(

View File

@@ -9,7 +9,6 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope import androidx.compose.runtime.saveable.SaverScope
@@ -23,9 +22,6 @@ import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -72,20 +68,12 @@ fun formatFileSize(bytes: Long): String {
val Boolean.yesOrNo val Boolean.yesOrNo
@StringRes get() = if(this) R.string.yes else R.string.no @StringRes get() = if(this) R.string.yes else R.string.no
@RequiresApi(26) fun formatTime(ms: Long): String {
fun parseTimestamp(timestamp: Long): String { return SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(ms))
val instant = Instant.ofEpochMilli(timestamp) }
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()) fun formatDate(date: Date): String {
return formatter.format(instant) 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) { fun Context.showOperationResultToast(success: Boolean) {
popToast(if(success) R.string.success else R.string.failed) 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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
@@ -130,9 +131,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.humanReadableDate import com.bintianqi.owndroid.formatTime
import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.CheckBoxItem
@@ -157,9 +157,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.InetAddress import java.net.InetAddress
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmErasure
@Serializable object Network @Serializable object Network
@@ -1036,14 +1033,14 @@ fun NetworkStatsScreen(
} }
} }
OutlinedTextField( 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)) }, label = { Text(stringResource(R.string.start_time)) },
interactionSource = startTimeTextFieldInteractionSource, interactionSource = startTimeTextFieldInteractionSource,
isError = startTime >= endTime, isError = startTime >= endTime,
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)
) )
OutlinedTextField( OutlinedTextField(
value = endTime.humanReadableDate, onValueChange = {}, readOnly = true, value = formatTime(endTime), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.end_time)) }, label = { Text(stringResource(R.string.end_time)) },
interactionSource = endTimeTextFieldInteractionSource, interactionSource = endTimeTextFieldInteractionSource,
isError = startTime >= endTime, isError = startTime >= endTime,
@@ -1315,18 +1312,9 @@ fun NetworkStatsViewerScreen(nsv: NetworkStatsViewer, onNavigateUp: () -> Unit)
HorizontalPager(ps, Modifier.padding(top = 8.dp)) { page -> HorizontalPager(ps, Modifier.padding(top = 8.dp)) { page ->
val data = nsv.stats[page] val data = nsv.stats[page]
Column(Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)) { Column(Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)) {
Row(Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) { Text(formatTime(data.startTime) + "\n~\n" + formatTime(data.endTime),
SimpleDateFormat("", Locale.getDefault()).format(Date(data.startTime)) Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center)
Text( Spacer(Modifier.height(5.dp))
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
)
}
val txBytes = data.txBytes val txBytes = data.txBytes
Text(stringResource(R.string.transmitted), style = typography.titleLarge) Text(stringResource(R.string.transmitted), style = typography.titleLarge)
Column(modifier = Modifier.padding(start = 8.dp, bottom = 4.dp)) { 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.R
import com.bintianqi.owndroid.SP import com.bintianqi.owndroid.SP
import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.humanReadableDate import com.bintianqi.owndroid.formatTime
import com.bintianqi.owndroid.parseTimestamp
import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.CheckBoxItem
@@ -290,7 +289,7 @@ fun SystemManagerScreen(
onClick = { onClick = {
if (dialog == 3 && VERSION.SDK_INT >= 24) vm.setOrgName(input) if (dialog == 3 && VERSION.SDK_INT >= 24) vm.setOrgName(input)
if (dialog == 4 && VERSION.SDK_INT >= 31) { if (dialog == 4 && VERSION.SDK_INT >= 31) {
vm.setOrgId(input) context.showOperationResultToast(vm.setOrgId(input))
enrollmentSpecificId = vm.getEnrollmentSpecificId() enrollmentSpecificId = vm.getEnrollmentSpecificId()
} }
dialog = 0 dialog = 0
@@ -368,6 +367,24 @@ fun SystemOptionsScreen(vm: MyViewModel, onNavigateUp: () -> Unit) {
SwitchItem(R.string.enable_usb_signal, status.usbSignalEnabled, SwitchItem(R.string.enable_usb_signal, status.usbSignalEnabled,
vm::setUsbSignalEnabled, R.drawable.usb_fill0) 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( if(dialog != 0) AlertDialog(
text = { text = {
@@ -520,7 +537,7 @@ fun HardwareMonitorScreen(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(28) @RequiresApi(28)
@Composable @Composable
fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) { fun ChangeTimeScreen(setTime: (Long, Boolean) -> Boolean, onNavigateUp: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val focusMgr = LocalFocusManager.current val focusMgr = LocalFocusManager.current
var tab by remember { mutableIntStateOf(0) } var tab by remember { mutableIntStateOf(0) }
@@ -528,8 +545,9 @@ fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) {
tab = pagerState.currentPage tab = pagerState.currentPage
val coroutine = rememberCoroutineScope() val coroutine = rememberCoroutineScope()
var picker by remember { mutableIntStateOf(0) } //0:None, 1:DatePicker, 2:TimePicker var picker by remember { mutableIntStateOf(0) } //0:None, 1:DatePicker, 2:TimePicker
var useCurrentTz by remember { mutableStateOf(true) }
val datePickerState = rememberDatePickerState() val datePickerState = rememberDatePickerState()
val timePickerState = rememberTimePickerState() val timePickerState = rememberTimePickerState(is24Hour = true)
val dateInteractionSource = remember { MutableInteractionSource() } val dateInteractionSource = remember { MutableInteractionSource() }
val timeInteractionSource = remember { MutableInteractionSource() } val timeInteractionSource = remember { MutableInteractionSource() }
if(dateInteractionSource.collectIsPressedAsState().value) picker = 1 if(dateInteractionSource.collectIsPressedAsState().value) picker = 1
@@ -571,14 +589,15 @@ fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) {
) { ) {
if(page == 0) { if(page == 0) {
OutlinedTextField( OutlinedTextField(
value = datePickerState.selectedDateMillis?.humanReadableDate ?: "", value = datePickerState.selectedDateMillis?.let { formatTime(it) } ?: "",
onValueChange = {}, readOnly = true, onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.date)) }, label = { Text(stringResource(R.string.date)) },
interactionSource = dateInteractionSource, interactionSource = dateInteractionSource,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
OutlinedTextField( OutlinedTextField(
value = timePickerState.hour.toString() + ":" + timePickerState.minute.toString(), value = timePickerState.hour.toString().padStart(2, '0') + ":" +
timePickerState.minute.toString().padStart(2, '0'),
onValueChange = {}, readOnly = true, onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.time)) }, label = { Text(stringResource(R.string.time)) },
interactionSource = timeInteractionSource, interactionSource = timeInteractionSource,
@@ -586,11 +605,14 @@ fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) {
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 4.dp) .padding(vertical = 4.dp)
) )
CheckBoxItem(R.string.use_current_timezone, useCurrentTz) {
useCurrentTz = it
}
Button( Button(
onClick = { onClick = {
val timeMillis = datePickerState.selectedDateMillis!! + val timeMillis = datePickerState.selectedDateMillis!! +
timePickerState.hour * 3600000 + timePickerState.minute * 60000 timePickerState.hour * 3600000 + timePickerState.minute * 60000
context.showOperationResultToast(setTime(timeMillis)) context.showOperationResultToast(setTime(timeMillis, useCurrentTz))
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = datePickerState.selectedDateMillis != null enabled = datePickerState.selectedDateMillis != null
@@ -609,7 +631,7 @@ fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) {
) )
Button( Button(
onClick = { onClick = {
context.showOperationResultToast(setTime(inputTime.toLong())) context.showOperationResultToast(setTime(inputTime.toLong(), false))
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -653,11 +675,14 @@ fun ChangeTimeZoneScreen(setTimeZone: (String) -> Boolean, onNavigateUp: () -> U
val focusMgr = LocalFocusManager.current val focusMgr = LocalFocusManager.current
var inputTimezone by remember { mutableStateOf("") } var inputTimezone by remember { mutableStateOf("") }
var dialog by remember { mutableStateOf(false) } var dialog by remember { mutableStateOf(false) }
val availableIds = TimeZone.getAvailableIDs()
val validInput = inputTimezone in availableIds
MyScaffold(R.string.change_timezone, onNavigateUp) { MyScaffold(R.string.change_timezone, onNavigateUp) {
OutlinedTextField( OutlinedTextField(
value = inputTimezone, value = inputTimezone,
label = { Text(stringResource(R.string.timezone_id)) }, label = { Text(stringResource(R.string.timezone_id)) },
onValueChange = { inputTimezone = it }, onValueChange = { inputTimezone = it },
isError = inputTimezone.isNotEmpty() && !validInput,
trailingIcon = { trailingIcon = {
IconButton(onClick = { dialog = true }) { IconButton(onClick = { dialog = true }) {
Icon(imageVector = Icons.AutoMirrored.Default.List, contentDescription = null) Icon(imageVector = Icons.AutoMirrored.Default.List, contentDescription = null)
@@ -673,7 +698,7 @@ fun ChangeTimeZoneScreen(setTimeZone: (String) -> Boolean, onNavigateUp: () -> U
context.showOperationResultToast(setTimeZone(inputTimezone)) context.showOperationResultToast(setTimeZone(inputTimezone))
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = inputTimezone.isNotEmpty() enabled = inputTimezone.isNotEmpty() && validInput
) { ) {
Text(stringResource(R.string.apply)) Text(stringResource(R.string.apply))
} }
@@ -683,7 +708,7 @@ fun ChangeTimeZoneScreen(setTimeZone: (String) -> Boolean, onNavigateUp: () -> U
if(dialog) AlertDialog( if(dialog) AlertDialog(
text = { text = {
LazyColumn { LazyColumn {
items(TimeZone.getAvailableIDs()) { items(availableIds) {
Text( Text(
text = it, text = it,
modifier = Modifier modifier = Modifier
@@ -1322,8 +1347,8 @@ data class CaCertInfo(
val serialNumber: String, val serialNumber: String,
val issuer: String, val issuer: String,
val subject: String, val subject: String,
val issuedTime: String, val issuedTime: Long,
val expiresTime: String, val expiresTime: Long,
val bytes: ByteArray val bytes: ByteArray
) )
@@ -1373,8 +1398,7 @@ fun CaCertScreen(
}) { }) {
Icon(Icons.Default.Add, stringResource(R.string.install)) Icon(Icons.Default.Add, stringResource(R.string.install))
} }
}, }
contentWindowInsets = WindowInsets.ime
) { paddingValues -> ) { paddingValues ->
LazyColumn( LazyColumn(
Modifier Modifier
@@ -1388,6 +1412,7 @@ fun CaCertScreen(
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
selectedCaCert = cert selectedCaCert = cert
dialog = 2
} }
.animateItem() .animateItem()
.padding(vertical = 10.dp, horizontal = 8.dp) .padding(vertical = 10.dp, horizontal = 8.dp)
@@ -1412,9 +1437,9 @@ fun CaCertScreen(
Text("Issuer", style = typography.labelLarge) Text("Issuer", style = typography.labelLarge)
SelectionContainer { Text(cert.issuer) } SelectionContainer { Text(cert.issuer) }
Text("Issued on", style = typography.labelLarge) Text("Issued on", style = typography.labelLarge)
SelectionContainer { Text(cert.issuedTime) } SelectionContainer { Text(formatTime(cert.issuedTime)) }
Text("Expires on", style = typography.labelLarge) Text("Expires on", style = typography.labelLarge)
SelectionContainer { Text(cert.expiresTime) } SelectionContainer { Text(formatTime(cert.expiresTime)) }
Text("SHA-256 fingerprint", style = typography.labelLarge) Text("SHA-256 fingerprint", style = typography.labelLarge)
SelectionContainer { Text(cert.hash) } SelectionContainer { Text(cert.hash) }
if (dialog == 2) Row( if (dialog == 2) Row(
@@ -1693,7 +1718,6 @@ fun FrpPolicyScreen(
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
}
Button( Button(
onClick = { onClick = {
focusMgr.clearFocus() focusMgr.clearFocus()
@@ -1706,6 +1730,7 @@ fun FrpPolicyScreen(
Text(stringResource(R.string.apply)) Text(stringResource(R.string.apply))
} }
} }
}
Notes(R.string.info_frp_policy, HorizontalPadding) Notes(R.string.info_frp_policy, HorizontalPadding)
} }
} }
@@ -1755,7 +1780,7 @@ fun WipeDataScreen(
dialog = 1 dialog = 1
}, },
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth().padding(HorizontalPadding, 5.dp)
) { ) {
Text("WipeData") Text("WipeData")
} }
@@ -1767,7 +1792,7 @@ fun WipeDataScreen(
dialog = 2 dialog = 2
}, },
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth().padding(HorizontalPadding, 5.dp)
) { ) {
Text("WipeDevice") Text("WipeDevice")
} }
@@ -1905,8 +1930,7 @@ fun SystemUpdatePolicyScreen(
if (VERSION.SDK_INT >= 26) { if (VERSION.SDK_INT >= 26) {
Column(Modifier.padding(HorizontalPadding)) { Column(Modifier.padding(HorizontalPadding)) {
if (pendingUpdate.exists) { if (pendingUpdate.exists) {
Text(stringResource(R.string.update_received_time, Text(stringResource(R.string.update_received_time, formatTime(pendingUpdate.time)))
parseTimestamp(pendingUpdate.time)))
Text(stringResource(R.string.is_security_patch, Text(stringResource(R.string.is_security_patch,
stringResource(pendingUpdate.securityPatch.yesOrNo))) stringResource(pendingUpdate.securityPatch.yesOrNo)))
} else { } else {

View File

@@ -1,18 +1,13 @@
package com.bintianqi.owndroid.dpm package com.bintianqi.owndroid.dpm
import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager
import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Binder import android.os.Binder
import android.os.Build.VERSION 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.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -44,11 +40,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.parseTimestamp import com.bintianqi.owndroid.formatTime
import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CircularProgressDialog 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.ui.SwitchItem
import com.bintianqi.owndroid.uriToStream import com.bintianqi.owndroid.uriToStream
import com.bintianqi.owndroid.yesOrNo import com.bintianqi.owndroid.yesOrNo
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable object Users @Serializable object Users
@Composable @Composable
fun UsersScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { fun UsersScreen(vm: MyViewModel, onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val privilege by Privilege.status.collectAsStateWithLifecycle() val privilege by Privilege.status.collectAsStateWithLifecycle()
/** 1: secondary users, 2: logout*/
var dialog by remember { mutableIntStateOf(0) } var dialog by remember { mutableIntStateOf(0) }
MyScaffold(R.string.users, onNavigateUp, 0.dp) { MyScaffold(R.string.users, onNavigateUp, 0.dp) {
if(VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated) { 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) context.popToast(R.string.select_an_image)
launcher.launch("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) { if(VERSION.SDK_INT >= 28 && privilege.device) {
FunctionItem(R.string.user_session_msg, icon = R.drawable.notifications_fill0) { onNavigate(UserSessionMessage) } 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) } FunctionItem(R.string.affiliation_id, icon = R.drawable.id_card_fill0) { onNavigate(AffiliationId) }
} }
} }
if(dialog != 0 && VERSION.SDK_INT >= 28) AlertDialog( if (VERSION.SDK_INT >= 28 && dialog == 1) AlertDialog(
title = { Text(stringResource(if(dialog == 1) R.string.secondary_users else R.string.logout)) }, title = { Text(stringResource(R.string.secondary_users)) },
text = { text = {
if(dialog == 1) { val list = vm.getSecondaryUsers()
val um = context.getSystemService(Context.USER_SERVICE) as UserManager val text = if (list.isEmpty()) {
val list = Privilege.DPM.getSecondaryUsers(Privilege.DAR) stringResource(R.string.no_secondary_users)
if(list.isEmpty()) {
Text(stringResource(R.string.no_secondary_users))
} else { } else {
Text("(" + stringResource(R.string.serial_number) + ")\n" + list.joinToString("\n") { um.getSerialNumberForUser(it).toString() }) "(" + stringResource(R.string.serial_number) + ")\n" + list.joinToString("\n")
}
} else {
Text(stringResource(R.string.info_logout))
} }
Text(text)
}, },
confirmButton = { confirmButton = {
TextButton( TextButton({ dialog = 0 }) {
onClick = { Text(stringResource(R.string.confirm))
if(dialog == 2) {
val result = Privilege.DPM.logoutUser(Privilege.DAR)
context.popToast(userOperationResultCode(result))
} }
},
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 dialog = 0
} }) {
) {
Text(stringResource(R.string.confirm)) Text(stringResource(R.string.confirm))
} }
}, },
dismissButton = { dismissButton = {
if(dialog != 1) TextButton(onClick = { dialog = 0 }) { TextButton({ dialog = 0 }) {
Text(stringResource(R.string.cancel)) Text(stringResource(R.string.cancel))
} }
}, },
@@ -167,41 +167,53 @@ fun UsersScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
@Serializable object UsersOptions @Serializable object UsersOptions
@Composable @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) { MyScaffold(R.string.options, onNavigateUp, 0.dp) {
if(VERSION.SDK_INT >= 28) { if(VERSION.SDK_INT >= 28) {
SwitchItem(R.string.enable_logout, getState = { Privilege.DPM.isLogoutEnabled }, SwitchItem(R.string.enable_logout, logoutEnabled, {
onCheckedChange = { Privilege.DPM.setLogoutEnabled(Privilege.DAR, it) }) 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 @Serializable object UserInfo
@Composable @Composable
fun UserInfoScreen(onNavigateUp: () -> Unit) { fun UserInfoScreen(getInfo: () -> UserInformation, onNavigateUp: () -> Unit) {
val context = LocalContext.current var info by remember { mutableStateOf(UserInformation()) }
val privilege by Privilege.status.collectAsStateWithLifecycle()
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
val user = Process.myUserHandle()
var infoDialog by remember { mutableIntStateOf(0) } var infoDialog by remember { mutableIntStateOf(0) }
MyScaffold(R.string.user_info, onNavigateUp, 0.dp) { LaunchedEffect(Unit) {
if(VERSION.SDK_INT >= 24) InfoItem(R.string.support_multiuser, UserManager.supportsMultipleUsers().yesOrNo) info = getInfo()
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))
} }
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, formatTime(info.time))
if (VERSION.SDK_INT >= 28) { if (VERSION.SDK_INT >= 28) {
InfoItem(R.string.logout_enabled, Privilege.DPM.isLogoutEnabled.yesOrNo) InfoItem(R.string.logout_enabled, info.logout.yesOrNo)
InfoItem(R.string.ephemeral_user, Privilege.DPM.isEphemeralUser(Privilege.DAR).yesOrNo) InfoItem(R.string.ephemeral_user, info.ephemeral.yesOrNo)
InfoItem(R.string.affiliated_user, privilege.affiliated.yesOrNo) InfoItem(R.string.affiliated_user, info.affiliated.yesOrNo)
} }
InfoItem(R.string.user_id, (Binder.getCallingUid() / 100000).toString()) 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( if(infoDialog != 0) AlertDialog(
text = { Text(stringResource(R.string.info_headless_system_user_mode)) }, text = { Text(stringResource(R.string.info_headless_system_user_mode)) },
@@ -217,24 +229,15 @@ fun UserInfoScreen(onNavigateUp: () -> Unit) {
@Serializable object UserOperation @Serializable object UserOperation
@Composable @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 context = LocalContext.current
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
var input by remember { mutableStateOf("") } var input by remember { mutableStateOf("") }
val focusMgr = LocalFocusManager.current val focusMgr = LocalFocusManager.current
var useUserId by remember { mutableStateOf(false) } var useUserId by remember { mutableStateOf(false) }
fun withUserHandle(operation: (UserHandle) -> Unit) { var dialog by remember { mutableStateOf(false) }
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)
}
}
val legalInput = input.toIntOrNull() != null val legalInput = input.toIntOrNull() != null
MyScaffold(R.string.user_operation, onNavigateUp) { MyScaffold(R.string.user_operation, onNavigateUp) {
if(VERSION.SDK_INT >= 24) SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { if(VERSION.SDK_INT >= 24) SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
@@ -257,10 +260,7 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Button( Button(
onClick = { onClick = {
focusMgr.clearFocus() focusMgr.clearFocus()
withUserHandle { context.popToast(startUser(input.toInt(), useUserId))
val result = Privilege.DPM.startUserInBackground(Privilege.DAR, it)
context.popToast(userOperationResultCode(result))
}
}, },
enabled = legalInput, enabled = legalInput,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -272,7 +272,7 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Button( Button(
onClick = { onClick = {
focusMgr.clearFocus() 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, enabled = legalInput,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -284,10 +284,7 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Button( Button(
onClick = { onClick = {
focusMgr.clearFocus() focusMgr.clearFocus()
withUserHandle { context.popToast(stopUser(input.toInt(), useUserId))
val result = Privilege.DPM.stopUser(Privilege.DAR, it)
context.popToast(userOperationResultCode(result))
}
}, },
enabled = legalInput, enabled = legalInput,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -299,14 +296,7 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Button( Button(
onClick = { onClick = {
focusMgr.clearFocus() focusMgr.clearFocus()
withUserHandle { dialog = true
if(Privilege.DPM.removeUser(Privilege.DAR, it)) {
context.showOperationResultToast(true)
input = ""
} else {
context.showOperationResultToast(false)
}
}
}, },
enabled = legalInput, enabled = legalInput,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -315,21 +305,39 @@ fun UserOperationScreen(onNavigateUp: () -> Unit) {
Text(stringResource(R.string.delete)) 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 @Serializable object CreateUser
@RequiresApi(24) @RequiresApi(24)
@Composable @Composable
fun CreateUserScreen(onNavigateUp: () -> Unit) { fun CreateUserScreen(
val context = LocalContext.current createUser: (String, Int, (CreateUserResult) -> Unit) -> Unit, onNavigateUp: () -> Unit
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager ) {
var result by remember { mutableStateOf<CreateUserResult?>(null) }
val focusMgr = LocalFocusManager.current val focusMgr = LocalFocusManager.current
var userName by remember { mutableStateOf("") } var userName by remember { mutableStateOf("") }
var creating by remember { mutableStateOf(false) } var creating by remember { mutableStateOf(false) }
var createdUserSerialNumber by remember { mutableLongStateOf(-1) } var flags by remember { mutableIntStateOf(0) }
var flag by remember { mutableIntStateOf(0) }
val coroutine = rememberCoroutineScope()
MyScaffold(R.string.create_user, onNavigateUp, 0.dp) { MyScaffold(R.string.create_user, onNavigateUp, 0.dp) {
OutlinedTextField( OutlinedTextField(
userName, { userName= it }, Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), userName, { userName= it }, Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
@@ -340,53 +348,45 @@ fun CreateUserScreen(onNavigateUp: () -> Unit) {
Spacer(Modifier.padding(vertical = 5.dp)) Spacer(Modifier.padding(vertical = 5.dp))
FullWidthCheckBoxItem( FullWidthCheckBoxItem(
R.string.create_user_skip_wizard, R.string.create_user_skip_wizard,
flag and DevicePolicyManager.SKIP_SETUP_WIZARD != 0 flags and DevicePolicyManager.SKIP_SETUP_WIZARD != 0
) { flag = flag xor DevicePolicyManager.SKIP_SETUP_WIZARD } ) { flags = flags xor DevicePolicyManager.SKIP_SETUP_WIZARD }
if(VERSION.SDK_INT >= 28) { if(VERSION.SDK_INT >= 28) {
FullWidthCheckBoxItem( FullWidthCheckBoxItem(
R.string.create_user_ephemeral_user, R.string.create_user_ephemeral_user,
flag and DevicePolicyManager.MAKE_USER_EPHEMERAL != 0 flags and DevicePolicyManager.MAKE_USER_EPHEMERAL != 0
) { flag = flag xor DevicePolicyManager.MAKE_USER_EPHEMERAL } ) { flags = flags xor DevicePolicyManager.MAKE_USER_EPHEMERAL }
FullWidthCheckBoxItem( FullWidthCheckBoxItem(
R.string.create_user_enable_all_system_app, R.string.create_user_enable_all_system_app,
flag and DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED != 0 flags and DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED != 0
) { flag = flag xor DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED } ) { flags = flags xor DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED }
} }
Spacer(Modifier.padding(vertical = 5.dp)) Spacer(Modifier.padding(vertical = 5.dp))
Button( Button(
onClick = { onClick = {
focusMgr.clearFocus() focusMgr.clearFocus()
creating = true creating = true
coroutine.launch(Dispatchers.IO) { createUser(userName, flags) {
try { creating = false
val uh = Privilege.DPM.createAndManageUser(Privilege.DAR, userName, Privilege.DAR, null, flag) result = it
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 }
} }
}, },
modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding) modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)
) { ) {
Text(stringResource(R.string.create)) Text(stringResource(R.string.create))
} }
if(createdUserSerialNumber != -1L) AlertDialog( if (result != null) AlertDialog(
title = { Text(stringResource(R.string.success)) }, text = {
text = { Text(stringResource(R.string.serial_number_of_new_user_is, createdUserSerialNumber)) }, Column {
confirmButton = { Text(stringResource(result!!.message))
TextButton({ createdUserSerialNumber = -1 }) { Text(stringResource(R.string.confirm)) } 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) @RequiresApi(26)
@Composable @Composable
fun AffiliationIdScreen(onNavigateUp: () -> Unit) { fun AffiliationIdScreen(
val context = LocalContext.current affiliationIds: StateFlow<List<String>>, getIds: () -> Unit, setId: (String, Boolean) -> Unit,
onNavigateUp: () -> Unit
) {
val focusMgr = LocalFocusManager.current val focusMgr = LocalFocusManager.current
var input by remember { mutableStateOf("") } var input by remember { mutableStateOf("") }
val list = remember { mutableStateListOf<String>() } val list by affiliationIds.collectAsStateWithLifecycle()
val refreshIds = { LaunchedEffect(Unit) { getIds() }
list.clear()
list.addAll(Privilege.DPM.getAffiliationIds(Privilege.DAR))
}
LaunchedEffect(Unit) { refreshIds() }
MyScaffold(R.string.affiliation_id, onNavigateUp) { MyScaffold(R.string.affiliation_id, onNavigateUp) {
Column(modifier = Modifier.animateContentSize()) { Column(modifier = Modifier.animateContentSize()) {
if (list.isEmpty()) Text(stringResource(R.string.none)) if (list.isEmpty()) Text(stringResource(R.string.none))
for (i in list) { for (i in list) {
ListItem(i) { list -= i } ListItem(i) { setId(i, false) }
} }
} }
Spacer(Modifier.padding(vertical = 5.dp))
OutlinedTextField( OutlinedTextField(
value = input, value = input,
onValueChange = { input = it }, onValueChange = { input = it },
@@ -421,7 +418,7 @@ fun AffiliationIdScreen(onNavigateUp: () -> Unit) {
trailingIcon = { trailingIcon = {
IconButton( IconButton(
onClick = { onClick = {
list += input setId(input, true)
input = "" input = ""
}, },
enabled = input.isNotEmpty() enabled = input.isNotEmpty()
@@ -429,22 +426,10 @@ fun AffiliationIdScreen(onNavigateUp: () -> Unit) {
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.add)) 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), 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) Notes(R.string.info_affiliation_id)
} }
} }
@@ -452,7 +437,7 @@ fun AffiliationIdScreen(onNavigateUp: () -> Unit) {
@Serializable object ChangeUsername @Serializable object ChangeUsername
@Composable @Composable
fun ChangeUsernameScreen(onNavigateUp: () -> Unit) { fun ChangeUsernameScreen(setName: (String) -> Unit, onNavigateUp: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val focusMgr = LocalFocusManager.current val focusMgr = LocalFocusManager.current
var inputUsername by remember { mutableStateOf("") } var inputUsername by remember { mutableStateOf("") }
@@ -468,19 +453,13 @@ fun ChangeUsernameScreen(onNavigateUp: () -> Unit) {
Spacer(Modifier.padding(vertical = 5.dp)) Spacer(Modifier.padding(vertical = 5.dp))
Button( Button(
onClick = { onClick = {
Privilege.DPM.setProfileName(Privilege.DAR, inputUsername) setName(inputUsername)
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text(stringResource(R.string.apply)) 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) @RequiresApi(28)
@Composable @Composable
fun UserSessionMessageScreen(onNavigateUp: () -> Unit) { fun UserSessionMessageScreen(
getMessages: () -> Pair<String, String>, setStartMessage: (String?) -> Unit,
setEndMessage: (String?) -> Unit, onNavigateUp: () -> Unit
) {
val context = LocalContext.current val context = LocalContext.current
val focusMgr = LocalFocusManager.current val focusMgr = LocalFocusManager.current
var start by remember { mutableStateOf("") } var start by remember { mutableStateOf("") }
var end by remember { mutableStateOf("") } var end by remember { mutableStateOf("") }
val refreshMsg = { LaunchedEffect(Unit) {
start = Privilege.DPM.getStartUserSessionMessage(Privilege.DAR)?.toString() ?: "" val messages = getMessages()
end = Privilege.DPM.getEndUserSessionMessage(Privilege.DAR)?.toString() ?: "" start = messages.first
end = messages.second
} }
LaunchedEffect(Unit) { refreshMsg() }
MyScaffold(R.string.user_session_msg, onNavigateUp) { MyScaffold(R.string.user_session_msg, onNavigateUp) {
OutlinedTextField( OutlinedTextField(
value = start, value = start,
@@ -510,8 +492,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button( Button(
onClick = { onClick = {
Privilege.DPM.setStartUserSessionMessage(Privilege.DAR, start) setStartMessage(start)
refreshMsg()
}, },
modifier = Modifier.fillMaxWidth(0.49F) modifier = Modifier.fillMaxWidth(0.49F)
) { ) {
@@ -519,8 +500,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
} }
Button( Button(
onClick = { onClick = {
Privilege.DPM.setStartUserSessionMessage(Privilege.DAR, null) setStartMessage(null)
refreshMsg()
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth(0.96F) modifier = Modifier.fillMaxWidth(0.96F)
@@ -540,8 +520,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button( Button(
onClick = { onClick = {
Privilege.DPM.setEndUserSessionMessage(Privilege.DAR, end) setStartMessage(end)
refreshMsg()
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth(0.49F) modifier = Modifier.fillMaxWidth(0.49F)
@@ -550,8 +529,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
} }
Button( Button(
onClick = { onClick = {
Privilege.DPM.setEndUserSessionMessage(Privilege.DAR, null) setEndMessage(null)
refreshMsg()
context.showOperationResultToast(true) context.showOperationResultToast(true)
}, },
modifier = Modifier.fillMaxWidth(0.96F) modifier = Modifier.fillMaxWidth(0.96F)
@@ -564,8 +542,7 @@ fun UserSessionMessageScreen(onNavigateUp: () -> Unit) {
@RequiresApi(23) @RequiresApi(23)
@Composable @Composable
private fun ChangeUserIconDialog(bitmap: Bitmap, onClose: () -> Unit) { private fun ChangeUserIconDialog(bitmap: Bitmap, onSet: () -> Unit, onClose: () -> Unit) {
val context = LocalContext.current
AlertDialog( AlertDialog(
title = { Text(stringResource(R.string.change_user_icon)) }, title = { Text(stringResource(R.string.change_user_icon)) },
text = { text = {
@@ -577,11 +554,7 @@ private fun ChangeUserIconDialog(bitmap: Bitmap, onClose: () -> Unit) {
} }
}, },
confirmButton = { confirmButton = {
TextButton({ TextButton(onSet) {
Privilege.DPM.setUserIcon(Privilege.DAR, bitmap)
context.showOperationResultToast(true)
onClose()
}) {
Text(stringResource(R.string.confirm)) Text(stringResource(R.string.confirm))
} }
}, },
@@ -593,13 +566,3 @@ private fun ChangeUserIconDialog(bitmap: Bitmap, onClose: () -> Unit) {
onDismissRequest = onClose 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_skip_wizard">Пропустить мастер настройки</string>
<string name="create_user_ephemeral_user">Временный пользователь</string> <string name="create_user_ephemeral_user">Временный пользователь</string>
<string name="create_user_enable_all_system_app">Включить все системные приложения</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="affiliation_id">Аффилированный идентификатор</string>
<string name="change_user_icon">Изменить значок пользователя</string> <string name="change_user_icon">Изменить значок пользователя</string>
<string name="select_an_image">Select an image</string> <!--TODO--> <string name="select_an_image">Select an image</string> <!--TODO-->
@@ -625,7 +624,7 @@
<string name="info_device_id_attestation">Указывает, поддерживает ли устройство проверку идентификаторов устройств в дополнение к проверке ключей.</string> <string name="info_device_id_attestation">Указывает, поддерживает ли устройство проверку идентификаторов устройств в дополнение к проверке ключей.</string>
<string name="info_unique_device_attestation">Да, если реализация StrongBox Keymaster на устройстве была обеспечена индивидуальным сертификатом аттестации и может использовать его для подписи записей аттестации (индивидуальный сертификат аттестации могут использовать только Keymaster с уровнем безопасности StrongBox).</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_enrollment_specific_id">Идентификатор останется неизменным, даже если рабочий профиль будет удален и создан заново (для той же организации), или если устройство будет сброшено до заводских настроек и перерегистрировано</string>
<string name="info_lock_screen_info">Отобразить краткое сообщение на экране блокировки. Переопределяет любую информацию о владельце, установленную пользователем вручную, и предотвращает ее дальнейшее изменение.</string> <string name="info_lock_screen_info">Отобразить краткое сообщение на экране блокировки. Переопределяет любую информацию о владельце, установленную пользователем вручную, и предотвращает ее дальнейшее изменение.</string>
<string name="info_short_support_message">Это будет отображено пользователю на экранах настроек, функциональность которых была отключена администратором. Если длина сообщения превышает 200 символов, оно может быть обрезано.</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_skip_wizard">Sihirbazı Atla</string>
<string name="create_user_ephemeral_user">Geçici Kullanıcı</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="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="affiliation_id">Bağlılık Kimliği</string>
<string name="change_user_icon">Kullanıcı Simgesini Değiştir</string> <string name="change_user_icon">Kullanıcı Simgesini Değiştir</string>
<string name="select_an_image">Bir görüntü seç</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_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_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_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_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> <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="enable_camera">启用相机</string>
<string name="disable_screen_capture">禁止屏幕捕获</string> <string name="disable_screen_capture">禁止屏幕捕获</string>
<string name="disable_status_bar">禁用状态栏</string> <string name="disable_status_bar">禁用状态栏</string>
<string name="status_bar">状态栏</string>
<string name="auto_time">自动设置时间</string> <string name="auto_time">自动设置时间</string>
<string name="auto_timezone">自动设置时区</string> <string name="auto_timezone">自动设置时区</string>
<string name="require_auto_time">要求自动时间</string> <string name="require_auto_time">要求自动时间</string>
@@ -157,6 +158,7 @@
<string name="manually_input">手动输入</string> <string name="manually_input">手动输入</string>
<string name="date">日期</string> <string name="date">日期</string>
<string name="time">时间</string> <string name="time">时间</string>
<string name="use_current_timezone">使用当前时区</string>
<string name="change_timezone">更改时区</string> <string name="change_timezone">更改时区</string>
<string name="timezone_id">时区ID</string> <string name="timezone_id">时区ID</string>
<string name="disable_auto_time_zone_before_set">在设置时区前需要关闭自动时区</string> <string name="disable_auto_time_zone_before_set">在设置时区前需要关闭自动时区</string>
@@ -483,12 +485,13 @@
<string name="logout">登出</string> <string name="logout">登出</string>
<string name="start_in_background">在后台启动</string> <string name="start_in_background">在后台启动</string>
<string name="user_operation_switch">切换</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="create_user">创建用户</string>
<string name="username">用户名</string> <string name="username">用户名</string>
<string name="create_user_skip_wizard">跳过创建用户向导</string> <string name="create_user_skip_wizard">跳过创建用户向导</string>
<string name="create_user_ephemeral_user">临时用户</string> <string name="create_user_ephemeral_user">临时用户</string>
<string name="create_user_enable_all_system_app">启用所有系统应用</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="affiliation_id">附属用户ID</string>
<string name="change_user_icon">更换用户头像</string> <string name="change_user_icon">更换用户头像</string>
<string name="select_an_image">选择一个图片</string> <string name="select_an_image">选择一个图片</string>
@@ -644,7 +647,7 @@
<string name="info_device_id_attestation">指示设备是否除了密钥证明之外还支持设备标识符证明</string> <string name="info_device_id_attestation">指示设备是否除了密钥证明之外还支持设备标识符证明</string>
<string name="info_unique_device_attestation">如果设备上的StrongBox Keymaster可以配置单独的证明证书并且可以使用该证书签署证明记录则返回true只有StrongBox安全级别的Keymaster才能使用单独的证明证书进行证明</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_enrollment_specific_id">不同组织ID的设备注册专用ID不同恢复出厂设置或删除工作资料后不变</string>
<string name="info_lock_screen_info">在锁屏界面上显示的一段简短的消息。将会覆盖用户当前设置的锁屏信息,并且防止用户在系统设置中设置新的锁屏信息</string> <string name="info_lock_screen_info">在锁屏界面上显示的一段简短的消息。将会覆盖用户当前设置的锁屏信息,并且防止用户在系统设置中设置新的锁屏信息</string>
<string name="info_short_support_message">用户试图使用被管理员禁用的功能时会显示此消息。不应多于200字</string> <string name="info_short_support_message">用户试图使用被管理员禁用的功能时会显示此消息。不应多于200字</string>

View File

@@ -133,6 +133,7 @@
<string name="enable_camera">Enable camera</string> <string name="enable_camera">Enable camera</string>
<string name="disable_screen_capture">Disable screen capture</string> <string name="disable_screen_capture">Disable screen capture</string>
<string name="disable_status_bar">Disable status bar</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="auto_time">Auto time</string>
<string name="require_auto_time">Require auto time</string> <string name="require_auto_time">Require auto time</string>
<string name="auto_timezone">Auto timezone</string> <string name="auto_timezone">Auto timezone</string>
@@ -166,6 +167,7 @@
<string name="manually_input">Manually input</string> <string name="manually_input">Manually input</string>
<string name="date">Date</string> <string name="date">Date</string>
<string name="time">Time</string> <string name="time">Time</string>
<string name="use_current_timezone">Use current timezone</string>
<string name="change_timezone">Change timezone</string> <string name="change_timezone">Change timezone</string>
<string name="timezone_id">Timezone ID</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> <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="logout">Logout</string>
<string name="start_in_background">Start in background</string> <string name="start_in_background">Start in background</string>
<string name="user_operation_switch">Switch</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="create_user">Create user</string>
<string name="username">Username</string> <string name="username">Username</string>
<string name="create_user_skip_wizard">Skip wizard</string> <string name="create_user_skip_wizard">Skip wizard</string>
<string name="create_user_ephemeral_user">Ephemeral user</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="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="affiliation_id">Affiliation ID</string>
<string name="change_user_icon">Change user icon</string> <string name="change_user_icon">Change user icon</string>
<string name="select_an_image">Select an image</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_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_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_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_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> <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>