mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 11:05:59 +00:00
ViewModel refactoring: Users part
Bugfix and improvement (#166, #174, #177, #178)
This commit is contained in:
40
Readme-en.md
40
Readme-en.md
@@ -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.
|
||||
|
||||
40
Readme.md
40
Readme.md
@@ -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
|
||||
|
||||
@@ -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 ?: "")
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user