diff --git a/Readme-en.md b/Readme-en.md index b367766..ee1cd8f 100644 --- a/Readme-en.md +++ b/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. diff --git a/Readme.md b/Readme.md index 3e4de41..92454c1 100644 --- a/Readme.md +++ b/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 diff --git a/app/src/main/java/com/bintianqi/owndroid/ApiReceiver.kt b/app/src/main/java/com/bintianqi/owndroid/ApiReceiver.kt index 5f12d1d..58b0699 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ApiReceiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ApiReceiver.kt @@ -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 ?: "") diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 1443842..420acd3 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -547,14 +547,24 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { vm::setUserRestriction, ::navigateUp) } - composable { UsersScreen(::navigateUp, ::navigate) } - composable { UserInfoScreen(::navigateUp) } - composable { UsersOptionsScreen(::navigateUp) } - composable { UserOperationScreen(::navigateUp) } - composable { CreateUserScreen(::navigateUp) } - composable { ChangeUsernameScreen(::navigateUp) } - composable { UserSessionMessageScreen(::navigateUp) } - composable { AffiliationIdScreen(::navigateUp) } + composable { UsersScreen(vm, ::navigateUp, ::navigate) } + composable { UserInfoScreen(vm::getUserInformation, ::navigateUp) } + composable { + UsersOptionsScreen(vm::getLogoutEnabled, vm::setLogoutEnabled, ::navigateUp) + } + composable { + UserOperationScreen(vm::startUser, vm::switchUser, vm::stopUser, vm::deleteUser, ::navigateUp) + } + composable { CreateUserScreen(vm::createUser, ::navigateUp) } + composable { ChangeUsernameScreen(vm::setProfileName, ::navigateUp) } + composable { + UserSessionMessageScreen(vm::getUserSessionMessages, vm::setStartUserSessionMessage, + vm::setEndUserSessionMessage, ::navigateUp) + } + composable { + AffiliationIdScreen(vm.affiliationIds, vm::getAffiliationIds, vm::setAffiliationId, + ::navigateUp) + } composable { PasswordScreen(::navigateUp, ::navigate) } composable { PasswordInfoScreen(::navigateUp) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index c2d58c4..41c6ce6 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -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()) 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()) 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()) 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>()) 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()) 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()) + @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 { + return DPM.getSecondaryUsers(DAR).map { UM.getSerialNumberForUser(it) } + } + @RequiresApi(28) + fun getUserSessionMessages(): Pair { + 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( diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 14fdfff..f6a9947 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -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) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index 65da686..aef2ab6 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -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)) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 1016c37..7f0aa33 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -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 { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt index 88689fa..958c48a 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt @@ -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(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>, getIds: () -> Unit, setId: (String, Boolean) -> Unit, + onNavigateUp: () -> Unit +) { val focusMgr = LocalFocusManager.current var input by remember { mutableStateOf("") } - val list = remember { mutableStateListOf() } - 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, 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 - } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 86d42a7..68a7695 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -482,7 +482,6 @@ Пропустить мастер настройки Временный пользователь Включить все системные приложения - Серийный номер этого пользователя: %1$d Аффилированный идентификатор Изменить значок пользователя Select an image @@ -625,7 +624,7 @@ Указывает, поддерживает ли устройство проверку идентификаторов устройств в дополнение к проверке ключей. Да, если реализация StrongBox Keymaster на устройстве была обеспечена индивидуальным сертификатом аттестации и может использовать его для подписи записей аттестации (индивидуальный сертификат аттестации могут использовать только Keymaster с уровнем безопасности StrongBox). - Устанавливает идентификатор предприятия (Enterprise ID). Это необходимо для создания идентификатора устройства, специфичного для регистрации. + Устанавливает идентификатор предприятия (Enterprise ID). Это необходимо для создания идентификатора устройства, специфичного для регистрации. Идентификатор останется неизменным, даже если рабочий профиль будет удален и создан заново (для той же организации), или если устройство будет сброшено до заводских настроек и перерегистрировано Отобразить краткое сообщение на экране блокировки. Переопределяет любую информацию о владельце, установленную пользователем вручную, и предотвращает ее дальнейшее изменение. Это будет отображено пользователю на экранах настроек, функциональность которых была отключена администратором. Если длина сообщения превышает 200 символов, оно может быть обрезано. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 31f9576..2f06a5a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -507,7 +507,6 @@ Sihirbazı Atla Geçici Kullanıcı Tüm Sistem Uygulamalarını Etkinleştir - Bu kullanıcının seri numarası: %1$d Bağlılık Kimliği Kullanıcı Simgesini Değiştir Bir görüntü seç @@ -664,7 +663,7 @@ Cihazın, anahtar doğrulamasına ek olarak cihaz kimlik doğrulamalarını destekleyip desteklemediğini belirtir. 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). - Kurumsal Kimliği ayarlar. Bu, cihaz için kayıt özel bir kimlik oluşturmak için bir gerekliliktir. + Kurumsal Kimliği ayarlar. Bu, cihaz için kayıt özel bir kimlik oluşturmak için bir gerekliliktir. 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. 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. 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. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e7a5914..35adb2d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -124,6 +124,7 @@ 启用相机 禁止屏幕捕获 禁用状态栏 + 状态栏 自动设置时间 自动设置时区 要求自动时间 @@ -157,6 +158,7 @@ 手动输入 日期 时间 + 使用当前时区 更改时区 时区ID 在设置时区前需要关闭自动时区 @@ -483,12 +485,13 @@ 登出 在后台启动 切换 + 已达到上限 + 你确定要删除用户%1$s吗? 创建用户 用户名 跳过创建用户向导 临时用户 启用所有系统应用 - 新用户的序列号:%1$d 附属用户ID 更换用户头像 选择一个图片 @@ -644,7 +647,7 @@ 指示设备是否除了密钥证明之外还支持设备标识符证明 如果设备上的StrongBox Keymaster可以配置单独的证明证书并且可以使用该证书签署证明记录,则返回true(只有StrongBox安全级别的Keymaster才能使用单独的证明证书进行证明) - 设置组织ID后才能获取设备注册专用ID + 设置组织ID后才能获取设备注册专用ID。ID只能设置一次。 不同组织ID的设备注册专用ID不同,恢复出厂设置或删除工作资料后不变 在锁屏界面上显示的一段简短的消息。将会覆盖用户当前设置的锁屏信息,并且防止用户在系统设置中设置新的锁屏信息 用户试图使用被管理员禁用的功能时会显示此消息。不应多于200字 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 704a87d..1d1236b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -133,6 +133,7 @@ Enable camera Disable screen capture Disable status bar + Status bar Auto time Require auto time Auto timezone @@ -166,6 +167,7 @@ Manually input Date Time + Use current timezone Change timezone Timezone ID Auto timezone should be disabled before set a custom timezone. @@ -516,12 +518,13 @@ Logout Start in background Switch + Limit reached + Are you sure you want to delete user %1$s ? Create user Username Skip wizard Ephemeral user Enable all system app - Serial number of this user: %1$d Affiliation ID Change user icon Select an image @@ -678,7 +681,7 @@ Indicates if the device supports attestation of device identifiers in addition to key 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). - Sets the Enterprise ID. This is a requirement for generating an enrollment-specific ID for the device. + Sets the Enterprise ID. This is a requirement for generating an enrollment-specific ID for the device. The ID can only be set once. 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. 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. 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