From 26c956a2cf93c4afe0f82747cae1fd3789568f02 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Mon, 22 Sep 2025 22:46:37 +0800 Subject: [PATCH] ViewModel refactoring: System part fix #165 --- .../com/bintianqi/owndroid/MainActivity.kt | 77 +- .../com/bintianqi/owndroid/MyViewModel.kt | 410 ++++++ .../bintianqi/owndroid/dpm/Applications.kt | 34 +- .../java/com/bintianqi/owndroid/dpm/System.kt | 1289 ++++++++--------- .../com/bintianqi/owndroid/ui/Components.kt | 16 + app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 9 files changed, 1128 insertions(+), 706 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 9384f24..d4d58bd 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -316,29 +316,64 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { } } - composable { SystemManagerScreen(::navigateUp, ::navigate) } - composable { SystemOptionsScreen(::navigateUp) } - composable { KeyguardScreen(::navigateUp) } - composable { HardwareMonitorScreen(::navigateUp) } - composable { ChangeTimeScreen(::navigateUp) } - composable { ChangeTimeZoneScreen(::navigateUp) } - composable { AutoTimePolicyScreen(::navigateUp) } - composable { AutoTimeZonePolicyScreen(::navigateUp) } - //composable<> { KeyPairs(::navigateUp) } - composable { ContentProtectionPolicyScreen(::navigateUp) } - composable { PermissionPolicyScreen(::navigateUp) } - composable { MtePolicyScreen(::navigateUp) } - composable { NearbyStreamingPolicyScreen(::navigateUp) } - composable { - LockTaskModeScreen(vm.chosenPackage, ::choosePackage, ::navigateUp) + composable { SystemManagerScreen(vm, ::navigateUp, ::navigate) } + composable { SystemOptionsScreen(vm, ::navigateUp) } + composable { + KeyguardScreen(vm::setKeyguardDisabled, vm::lockScreen, ::navigateUp) + } + composable { + HardwareMonitorScreen(vm.hardwareProperties, vm::getHardwareProperties, + vm::setHpRefreshInterval, ::navigateUp) + } + composable { ChangeTimeScreen(vm::setTime, ::navigateUp) } + composable { ChangeTimeZoneScreen(vm::setTimeZone, ::navigateUp) } + composable { + AutoTimePolicyScreen(vm::getAutoTimePolicy, vm::setAutoTimePolicy, ::navigateUp) + } + composable { + AutoTimeZonePolicyScreen(vm::getAutoTimeZonePolicy, vm::setAutoTimeZonePolicy, + ::navigateUp) + } + //composable<> { KeyPairs(::navigateUp) } + composable { + ContentProtectionPolicyScreen(vm::getContentProtectionPolicy, + vm::setContentProtectionPolicy, ::navigateUp) + } + composable { + PermissionPolicyScreen(vm::getPermissionPolicy, vm::setPermissionPolicy, ::navigateUp) + } + composable { + MtePolicyScreen(vm::getMtePolicy, vm::setMtePolicy, ::navigateUp) + } + composable { + NearbyStreamingPolicyScreen(vm::getNsAppPolicy, vm::setNsAppPolicy, + vm::getNsNotificationPolicy, vm::setNsNotificationPolicy, ::navigateUp) + } + composable { + LockTaskModeScreen(vm.chosenPackage, ::choosePackage, vm.lockTaskPackages, + vm::getLockTaskPackages, vm::setLockTaskPackage, vm::startLockTaskMode, + vm:: getLockTaskFeatures, vm::setLockTaskFeatures, ::navigateUp) + } + composable { + CaCertScreen(vm.installedCaCerts, vm::getCaCerts, vm::installCaCert, vm::parseCaCert, + vm::exportCaCert, vm::uninstallCaCert, vm::uninstallAllCaCerts, ::navigateUp) } - composable { CaCertScreen(::navigateUp) } composable { SecurityLoggingScreen(::navigateUp) } - composable { DisableAccountManagementScreen(::navigateUp) } - composable { SystemUpdatePolicyScreen(::navigateUp) } - composable { InstallSystemUpdateScreen(::navigateUp) } - composable { FrpPolicyScreen(::navigateUp) } - composable { WipeDataScreen(::navigateUp) } + composable { + DisableAccountManagementScreen(vm.mdAccountTypes, vm::getMdAccountTypes, + vm::setMdAccountType, ::navigateUp) + } + composable { + SystemUpdatePolicyScreen(vm::getSystemUpdatePolicy, vm::setSystemUpdatePolicy, + vm::getPendingSystemUpdate, ::navigateUp) + } + composable { + InstallSystemUpdateScreen(vm::installSystemUpdate, ::navigateUp) + } + composable { + FrpPolicyScreen(vm::getFrpPolicy, vm::setFrpPolicy, ::navigateUp) + } + composable { WipeDataScreen(vm::wipeData, ::navigateUp) } composable { NetworkScreen(::navigateUp, ::navigate) } composable { WifiScreen(::navigateUp, ::navigate) { navController.navigate(AddNetwork, it)} } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 3ea5e76..3e1f743 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -1,16 +1,25 @@ package com.bintianqi.owndroid +import android.app.ActivityOptions import android.app.Application import android.app.PendingIntent +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyManager.InstallSystemUpdateCallback +import android.app.admin.FactoryResetProtectionPolicy import android.app.admin.PackagePolicy +import android.app.admin.SystemUpdateInfo +import android.app.admin.SystemUpdatePolicy import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.ApplicationInfo import android.content.pm.PackageInstaller import android.content.pm.PackageManager +import android.net.Uri import android.os.Build.VERSION +import android.os.HardwarePropertiesManager import androidx.annotation.RequiresApi import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -22,16 +31,28 @@ import androidx.lifecycle.viewModelScope import com.bintianqi.owndroid.Privilege.DAR import com.bintianqi.owndroid.Privilege.DPM import com.bintianqi.owndroid.dpm.AppStatus +import com.bintianqi.owndroid.dpm.CaCertInfo +import com.bintianqi.owndroid.dpm.FrpPolicyInfo +import com.bintianqi.owndroid.dpm.HardwareProperties +import com.bintianqi.owndroid.dpm.PendingSystemUpdateInfo +import com.bintianqi.owndroid.dpm.SystemOptionsStatus +import com.bintianqi.owndroid.dpm.SystemUpdatePolicyInfo import com.bintianqi.owndroid.dpm.getPackageInstaller import com.bintianqi.owndroid.dpm.isValidPackageName import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage import com.bintianqi.owndroid.dpm.permissionList +import com.bintianqi.owndroid.dpm.temperatureTypes import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.security.MessageDigest +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate import java.util.concurrent.Executors class MyViewModel(application: Application): AndroidViewModel(application) { @@ -377,6 +398,395 @@ class MyViewModel(application: Application): AndroidViewModel(application) { false } } + + @RequiresApi(24) + fun reboot() { + DPM.reboot(DAR) + } + @RequiresApi(24) + fun requestBugReport(): Boolean { + return DPM.requestBugreport(DAR) + } + @RequiresApi(24) + fun getOrgName(): String { + return DPM.getOrganizationName(DAR).toString() + } + @RequiresApi(24) + fun setOrgName(name: String) { + DPM.setOrganizationName(DAR, name) + } + @RequiresApi(31) + fun setOrgId(id: String) { + DPM.setOrganizationId(id) + } + @RequiresApi(31) + fun getEnrollmentSpecificId(): String { + return DPM.enrollmentSpecificId + } + val systemOptionsStatus = MutableStateFlow(SystemOptionsStatus()) + fun getSystemOptionsStatus() { + val privilege = Privilege.status.value + systemOptionsStatus.value = SystemOptionsStatus( + cameraDisabled = DPM.getCameraDisabled(null), + screenCaptureDisabled = DPM.getScreenCaptureDisabled(null), + statusBarDisabled = if (VERSION.SDK_INT >= 34 && + privilege.run { device || (profile && affiliated) }) + DPM.isStatusBarDisabled else false, + autoTimeEnabled = if (VERSION.SDK_INT >= 30 && privilege.run { device || org }) + DPM.getAutoTimeEnabled(DAR) else false, + autoTimeZoneEnabled = if (VERSION.SDK_INT >= 30 && privilege.run { device || org }) + DPM.getAutoTimeZoneEnabled(DAR) else false, + autoTimeRequired = if (VERSION.SDK_INT < 30) DPM.autoTimeRequired else false, + masterVolumeMuted = DPM.isMasterVolumeMuted(DAR), + backupServiceEnabled = if (VERSION.SDK_INT >= 26) DPM.isBackupServiceEnabled(DAR) else false, + btContactSharingDisabled = if (VERSION.SDK_INT >= 23 && privilege.work) + DPM.getBluetoothContactSharingDisabled(DAR) else false, + commonCriteriaMode = if (VERSION.SDK_INT >= 30) DPM.isCommonCriteriaModeEnabled(DAR) else false, + usbSignalEnabled = if (VERSION.SDK_INT >= 31) DPM.isUsbDataSignalingEnabled else false, + canDisableUsbSignal = if (VERSION.SDK_INT >= 31) DPM.canUsbDataSignalingBeDisabled() else false + ) + } + fun setCameraDisabled(disabled: Boolean) { + DPM.setCameraDisabled(DAR, disabled) + createShortcuts(application) + systemOptionsStatus.update { it.copy(cameraDisabled = DPM.getCameraDisabled(null)) } + } + fun setScreenCaptureDisabled(disabled: Boolean) { + DPM.setScreenCaptureDisabled(DAR, disabled) + systemOptionsStatus.update { + it.copy(screenCaptureDisabled = DPM.getScreenCaptureDisabled(null)) + } + } + @RequiresApi(23) + fun setStatusBarDisabled(disabled: Boolean) { + val result = DPM.setStatusBarDisabled(DAR, disabled) + if (result) systemOptionsStatus.update { it.copy(statusBarDisabled = disabled) } + } + @RequiresApi(30) + fun setAutoTimeEnabled(enabled: Boolean) { + DPM.setAutoTimeEnabled(DAR, enabled) + systemOptionsStatus.update { it.copy(autoTimeEnabled = DPM.getAutoTimeEnabled(DAR)) } + } + @RequiresApi(30) + fun setAutoTimeZoneEnabled(enabled: Boolean) { + DPM.setAutoTimeZoneEnabled(DAR, enabled) + systemOptionsStatus.update { + it.copy(autoTimeZoneEnabled = DPM.getAutoTimeZoneEnabled(DAR)) + } + } + @Suppress("DEPRECATION") + fun setAutoTimeRequired(required: Boolean) { + DPM.setAutoTimeRequired(DAR, required) + systemOptionsStatus.update { it.copy(autoTimeRequired = DPM.autoTimeRequired) } + } + fun setMasterVolumeMuted(muted: Boolean) { + DPM.setMasterVolumeMuted(DAR, muted) + createShortcuts(application) + systemOptionsStatus.update { it.copy(masterVolumeMuted = DPM.isMasterVolumeMuted(DAR)) } + } + @RequiresApi(26) + fun setBackupServiceEnabled(enabled: Boolean) { + DPM.setBackupServiceEnabled(DAR, enabled) + systemOptionsStatus.update { + it.copy(backupServiceEnabled = DPM.isBackupServiceEnabled(DAR)) + } + } + @RequiresApi(23) + fun setBtContactSharingDisabled(disabled: Boolean) { + DPM.setBluetoothContactSharingDisabled(DAR, disabled) + systemOptionsStatus.update { + it.copy(btContactSharingDisabled = DPM.getBluetoothContactSharingDisabled(DAR)) + } + } + @RequiresApi(30) + fun setCommonCriteriaModeEnabled(enabled: Boolean) { + DPM.setCommonCriteriaModeEnabled(DAR, enabled) + systemOptionsStatus.update { + it.copy(commonCriteriaMode = DPM.isCommonCriteriaModeEnabled(DAR)) + } + } + @RequiresApi(31) + fun setUsbSignalEnabled(enabled: Boolean) { + DPM.isUsbDataSignalingEnabled = enabled + systemOptionsStatus.update { it.copy(usbSignalEnabled = DPM.isUsbDataSignalingEnabled) } + } + @RequiresApi(23) + fun setKeyguardDisabled(disabled: Boolean): Boolean { + return DPM.setKeyguardDisabled(DAR, disabled) + } + fun lockScreen(evictKey: Boolean) { + if (VERSION.SDK_INT >= 26 && Privilege.status.value.work) { + DPM.lockNow(if (evictKey) DevicePolicyManager.FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY else 0) + } else { + DPM.lockNow() + } + } + val hardwareProperties = MutableStateFlow(HardwareProperties()) + var hpRefreshInterval = 1000L + fun setHpRefreshInterval(interval: Float) { + hpRefreshInterval = (interval * 1000).toLong() + } + @RequiresApi(24) + suspend fun getHardwareProperties() { + val hpm = application.getSystemService(HardwarePropertiesManager::class.java) + while (true) { + val properties = HardwareProperties( + temperatureTypes.map { (type, _) -> + type to hpm.getDeviceTemperatures(type, HardwarePropertiesManager.TEMPERATURE_CURRENT).toList() + }.toMap(), + hpm.cpuUsages.map { it.active to it.total }, + hpm.fanSpeeds.toList() + ) + if (properties.cpuUsages.isEmpty() && properties.fanSpeeds.isEmpty() && + properties.temperatures.isEmpty()) { + break + } + delay(hpRefreshInterval) + } + } + @RequiresApi(28) + fun setTime(time: Long): Boolean { + return DPM.setTime(DAR, time) + } + @RequiresApi(28) + fun setTimeZone(tz: String): Boolean { + return DPM.setTimeZone(DAR, tz) + } + @RequiresApi(36) + fun getAutoTimePolicy(): Int { + return DPM.autoTimePolicy + } + @RequiresApi(36) + fun setAutoTimePolicy(policy: Int) { + DPM.autoTimePolicy = policy + } + @RequiresApi(36) + fun getAutoTimeZonePolicy(): Int { + return DPM.autoTimeZonePolicy + } + @RequiresApi(36) + fun setAutoTimeZonePolicy(policy: Int) { + DPM.autoTimeZonePolicy = policy + } + @RequiresApi(35) + fun getContentProtectionPolicy(): Int { + return DPM.getContentProtectionPolicy(DAR) + } + @RequiresApi(35) + fun setContentProtectionPolicy(policy: Int) { + DPM.setContentProtectionPolicy(DAR, policy) + } + @RequiresApi(23) + fun getPermissionPolicy(): Int { + return DPM.getPermissionPolicy(DAR) + } + @RequiresApi(23) + fun setPermissionPolicy(policy: Int) { + DPM.setPermissionPolicy(DAR, policy) + } + @RequiresApi(34) + fun getMtePolicy(): Int { + return DPM.mtePolicy + } + @RequiresApi(34) + fun setMtePolicy(policy: Int): Boolean { + return try { + DPM.mtePolicy = policy + true + } catch (_: UnsupportedOperationException) { + false + } + } + @RequiresApi(31) + fun getNsAppPolicy(): Int { + return DPM.nearbyAppStreamingPolicy + } + @RequiresApi(31) + fun setNsAppPolicy(policy: Int) { + DPM.nearbyAppStreamingPolicy = policy + } + @RequiresApi(31) + fun getNsNotificationPolicy(): Int { + return DPM.nearbyNotificationStreamingPolicy + } + @RequiresApi(31) + fun setNsNotificationPolicy(policy: Int) { + DPM.nearbyNotificationStreamingPolicy = policy + } + val lockTaskPackages = MutableStateFlow(emptyList()) + @RequiresApi(26) + fun getLockTaskPackages() { + lockTaskPackages.value = DPM.getLockTaskPackages(DAR).map { getAppInfo(it) } + } + @RequiresApi(26) + fun setLockTaskPackage(name: String, status: Boolean) { + DPM.setLockTaskPackages(DAR, + lockTaskPackages.value.map { it.name } + .run { if (status) plus(name) else minus(name) } + .toTypedArray() + ) + getLockTaskPackages() + } + @RequiresApi(28) + fun startLockTaskMode(packageName: String, activity: String): Int { + if (!NotificationUtils.checkPermission(application)) return 0 + if (!DPM.isLockTaskPermitted(packageName)) return 1 + val options = ActivityOptions.makeBasic().setLockTaskEnabled(true) + val intent = if(activity.isNotEmpty()) { + Intent().setComponent(ComponentName(packageName, activity)) + } else PM.getLaunchIntentForPackage(packageName) + if (intent != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + application.startActivity(intent, options.toBundle()) + return 0 + } else { + return 2 + } + } + @RequiresApi(28) + fun getLockTaskFeatures(): Int { + return DPM.getLockTaskFeatures(DAR) + } + @RequiresApi(28) + fun setLockTaskFeatures(flags: Int): String? { + try { + DPM.setLockTaskFeatures(DAR, flags) + return null + } catch (e: IllegalArgumentException) { + return e.message + } + } + val installedCaCerts = MutableStateFlow(emptyList()) + fun getCaCerts() { + viewModelScope.launch { + installedCaCerts.value = DPM.getInstalledCaCerts(DAR).mapNotNull { parseCaCert(it) } + } + } + fun parseCaCert(uri: Uri): CaCertInfo? { + return try { + application.contentResolver.openInputStream(uri)?.use { + parseCaCert(it.readBytes()) + } + } catch(e: Exception) { + e.printStackTrace() + null + } + } + fun parseCaCert(bytes: ByteArray): CaCertInfo? { + val hash = MessageDigest.getInstance("SHA-256").digest(bytes).toHexString() + return try { + val factory = CertificateFactory.getInstance("X.509") + val cert = factory.generateCertificate(bytes.inputStream()) as X509Certificate + CaCertInfo( + hash, cert.serialNumber.toString(16), + cert.issuerX500Principal.name, cert.subjectX500Principal.name, + parseDate(cert.notBefore), parseDate(cert.notAfter), bytes + ) + } catch (e: CertificateException) { + e.printStackTrace() + null + } + } + fun installCaCert(cert: CaCertInfo): Boolean { + val result = DPM.installCaCert(DAR, cert.bytes) + if (result) getCaCerts() + return result + } + fun uninstallCaCert(cert: CaCertInfo) { + DPM.uninstallCaCert(DAR, cert.bytes) + getCaCerts() + } + fun uninstallAllCaCerts() { + DPM.uninstallAllUserCaCerts(DAR) + getCaCerts() + } + fun exportCaCert(uri: Uri, cert: CaCertInfo) { + application.contentResolver.openOutputStream(uri)?.use { + it.write(cert.bytes) + } + } + val mdAccountTypes = MutableStateFlow(emptyList()) + fun getMdAccountTypes() { + mdAccountTypes.value = DPM.accountTypesWithManagementDisabled?.toList() ?: emptyList() + } + fun setMdAccountType(type: String, disabled: Boolean) { + DPM.setAccountManagementDisabled(DAR, type, disabled) + getMdAccountTypes() + } + @RequiresApi(30) + fun getFrpPolicy(): FrpPolicyInfo { + return try { + val policy = DPM.getFactoryResetProtectionPolicy(DAR) + FrpPolicyInfo( + true, policy != null, policy?.isFactoryResetProtectionEnabled ?: false, + policy?.factoryResetProtectionAccounts ?: emptyList() + ) + } catch (_: UnsupportedOperationException) { + FrpPolicyInfo(false, false, false, emptyList()) + } + } + @RequiresApi(30) + fun setFrpPolicy(info: FrpPolicyInfo) { + val policy = if (info.usePolicy) { + FactoryResetProtectionPolicy.Builder() + .setFactoryResetProtectionEnabled(info.enabled) + .setFactoryResetProtectionAccounts(info.accounts) + .build() + } else null + DPM.setFactoryResetProtectionPolicy(DAR, policy) + } + fun wipeData(wipeDevice: Boolean, flags: Int, reason: String) { + if (wipeDevice && VERSION.SDK_INT >= 34) { + DPM.wipeDevice(flags) + } else { + if(VERSION.SDK_INT >= 28 && reason.isNotEmpty()) { + DPM.wipeData(flags, reason) + } else { + DPM.wipeData(flags) + } + } + } + @RequiresApi(23) + fun getSystemUpdatePolicy(): SystemUpdatePolicyInfo { + val policy = DPM.systemUpdatePolicy + return SystemUpdatePolicyInfo( + policy?.policyType ?: -1, policy?.installWindowStart ?: 0, policy?.installWindowEnd ?: 0 + ) + } + @RequiresApi(23) + fun setSystemUpdatePolicy(info: SystemUpdatePolicyInfo) { + val policy = when (info.type) { + SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC -> SystemUpdatePolicy.createAutomaticInstallPolicy() + SystemUpdatePolicy.TYPE_INSTALL_WINDOWED -> + SystemUpdatePolicy.createWindowedInstallPolicy(info.start, info.end) + SystemUpdatePolicy.TYPE_POSTPONE -> SystemUpdatePolicy.createPostponeInstallPolicy() + else -> null + } + DPM.setSystemUpdatePolicy(DAR, policy) + } + @RequiresApi(26) + fun getPendingSystemUpdate(): PendingSystemUpdateInfo { + val update = DPM.getPendingSystemUpdate(DAR) + return PendingSystemUpdateInfo(update != null, update?.receivedTime ?: 0, + update?.securityPatchState == SystemUpdateInfo.SECURITY_PATCH_STATE_TRUE) + } + @RequiresApi(29) + fun installSystemUpdate(uri: Uri, callback: (String) -> Unit) { + val callback = object: InstallSystemUpdateCallback() { + override fun onInstallUpdateError(errorCode: Int, errorMessage: String) { + super.onInstallUpdateError(errorCode, errorMessage) + val errDetail = when(errorCode) { + UPDATE_ERROR_BATTERY_LOW -> R.string.battery_low + UPDATE_ERROR_UPDATE_FILE_INVALID -> R.string.update_file_invalid + UPDATE_ERROR_INCORRECT_OS_VERSION -> R.string.incorrect_os_ver + UPDATE_ERROR_FILE_NOT_FOUND -> R.string.file_not_exist + else -> R.string.unknown_error + } + callback(application.getString(errDetail) + "\n$errorMessage") + } + } + DPM.installSystemUpdate(DAR, uri, application.mainExecutor, callback) + } } data class ThemeSettings( diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 2878e55..f8bab24 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -54,6 +54,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -556,7 +557,8 @@ fun CredentialManagerPolicyScreen( cmPackages: MutableStateFlow>, getCmPolicy: () -> Int, setCmPackage: (String, Boolean) -> Unit, setCmPolicy: (Int) -> Unit, onNavigateUp: () -> Unit ) { - var policy by remember { mutableIntStateOf(getCmPolicy()) } + val context = LocalContext.current + var policy by rememberSaveable { mutableIntStateOf(getCmPolicy()) } val packages by cmPackages.collectAsStateWithLifecycle() var packageName by remember { mutableStateOf("") } LaunchedEffect(Unit) { @@ -574,26 +576,29 @@ fun CredentialManagerPolicyScreen( } Spacer(Modifier.padding(vertical = 4.dp)) } - items(packages, { it.name }) { + if (policy != -1) items(packages, { it.name }) { ApplicationItem(it) { setCmPackage(it.name, false) } } item { Column(Modifier.padding(horizontal = HorizontalPadding)) { - PackageNameTextField(packageName, onChoosePackage, - Modifier.padding(vertical = 8.dp)) { packageName = it } - Button( - { - setCmPackage(packageName, true) - packageName = "" - }, - Modifier.fillMaxWidth(), - enabled = packageName.isValidPackageName - ) { - Text(stringResource(R.string.add)) + if (policy != -1) { + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(vertical = 8.dp)) { packageName = it } + Button( + { + setCmPackage(packageName, true) + packageName = "" + }, + Modifier.fillMaxWidth(), + enabled = packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } } Button( { setCmPolicy(policy) + context.showOperationResultToast(true) }, Modifier.fillMaxWidth() ) { @@ -617,9 +622,8 @@ fun PermittedAsAndImPackages( val context = LocalContext.current val packages by packagesState.collectAsStateWithLifecycle() var packageName by remember { mutableStateOf("") } - var allowAll by remember { mutableStateOf(false) } + var allowAll by rememberSaveable { mutableStateOf(getPackages()) } LaunchedEffect(Unit) { - allowAll = getPackages() packageName = chosenPackage.receive() } MyLazyScaffold(title, onNavigateUp) { 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 86ad241..99227f6 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -1,10 +1,7 @@ package com.bintianqi.owndroid.dpm import android.annotation.SuppressLint -import android.app.ActivityOptions import android.app.admin.DevicePolicyManager -import android.app.admin.DevicePolicyManager.FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY -import android.app.admin.DevicePolicyManager.InstallSystemUpdateCallback import android.app.admin.DevicePolicyManager.MTE_DISABLED import android.app.admin.DevicePolicyManager.MTE_ENABLED import android.app.admin.DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY @@ -19,15 +16,11 @@ import android.app.admin.DevicePolicyManager.WIPE_EUICC import android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE import android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA import android.app.admin.DevicePolicyManager.WIPE_SILENTLY -import android.app.admin.FactoryResetProtectionPolicy import android.app.admin.SystemUpdateInfo -import android.app.admin.SystemUpdatePolicy import android.app.admin.SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC import android.app.admin.SystemUpdatePolicy.TYPE_INSTALL_WINDOWED import android.app.admin.SystemUpdatePolicy.TYPE_POSTPONE -import android.content.ComponentName import android.content.Context -import android.content.Intent import android.net.Uri import android.os.Build.VERSION import android.os.HardwarePropertiesManager @@ -43,12 +36,12 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row 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.lazy.LazyColumn @@ -68,6 +61,7 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.ExperimentalMaterial3Api @@ -80,11 +74,7 @@ import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Slider -import androidx.compose.material3.Switch import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text @@ -101,7 +91,6 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -117,15 +106,15 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bintianqi.owndroid.AppInfo import com.bintianqi.owndroid.HorizontalPadding -import com.bintianqi.owndroid.NotificationUtils +import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SP -import com.bintianqi.owndroid.createShortcuts import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.humanReadableDate -import com.bintianqi.owndroid.parseDate +import com.bintianqi.owndroid.parseTimestamp import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CheckBoxItem @@ -139,26 +128,23 @@ import com.bintianqi.owndroid.ui.MySmallTitleScaffold import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.Notes import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.uriToStream -import kotlinx.coroutines.Dispatchers +import com.bintianqi.owndroid.yesOrNo import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import java.io.ByteArrayOutputStream -import java.security.MessageDigest -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate import java.util.Date import java.util.TimeZone -import java.util.concurrent.Executors import kotlin.math.roundToLong @Serializable object SystemManager @Composable -fun SystemManagerScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { +fun SystemManagerScreen( + vm: MyViewModel, onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit +) { val context = LocalContext.current val privilege by Privilege.status.collectAsStateWithLifecycle() /** 1: reboot, 2: bug report, 3: org name, 4: org id, 5: enrollment specific id*/ @@ -248,9 +234,9 @@ fun SystemManagerScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { TextButton( onClick = { if(dialog == 1) { - Privilege.DPM.reboot(Privilege.DAR) + vm.reboot() } else { - context.showOperationResultToast(Privilege.DPM.requestBugreport(Privilege.DAR)) + context.showOperationResultToast(vm.requestBugReport()) } dialog = 0 } @@ -266,12 +252,15 @@ fun SystemManagerScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { text = { val focusMgr = LocalFocusManager.current LaunchedEffect(Unit) { - if(dialog == 5 && VERSION.SDK_INT >= 31) input = Privilege.DPM.enrollmentSpecificId + if (dialog == 5 && VERSION.SDK_INT >= 31) input = vm.getEnrollmentSpecificId() + if (dialog == 3 && VERSION.SDK_INT >= 24) input = vm.getOrgName() } Column { OutlinedTextField( input, { input = it }, - Modifier.fillMaxWidth().padding(bottom = if (dialog != 3) 8.dp else 0.dp), + Modifier + .fillMaxWidth() + .padding(bottom = if (dialog != 3) 8.dp else 0.dp), readOnly = dialog == 5, label = { Text(stringResource( @@ -301,16 +290,12 @@ fun SystemManagerScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { confirmButton = { TextButton( onClick = { - try { - if (dialog == 3 && VERSION.SDK_INT >= 24) Privilege.DPM.setOrganizationName(Privilege.DAR, input) - if (dialog == 4 && VERSION.SDK_INT >= 31) { - Privilege.DPM.setOrganizationId(input) - enrollmentSpecificId = Privilege.DPM.enrollmentSpecificId - } - dialog = 0 - } catch(_: IllegalStateException) { - context.showOperationResultToast(false) + if (dialog == 3 && VERSION.SDK_INT >= 24) vm.setOrgName(input) + if (dialog == 4 && VERSION.SDK_INT >= 31) { + vm.setOrgId(input) + enrollmentSpecificId = vm.getEnrollmentSpecificId() } + dialog = 0 }, enabled = dialog != 4 || input.length in 6..64 ) { @@ -321,77 +306,69 @@ fun SystemManagerScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { } } +data class SystemOptionsStatus( + val cameraDisabled: Boolean = false, + val screenCaptureDisabled: Boolean = false, + val statusBarDisabled: Boolean = false, + val autoTimeEnabled: Boolean = true, + val autoTimeZoneEnabled: Boolean = true, + val autoTimeRequired: Boolean = true, + val masterVolumeMuted: Boolean = false, + val backupServiceEnabled: Boolean = false, + val btContactSharingDisabled: Boolean = false, + val commonCriteriaMode: Boolean = false, + val usbSignalEnabled: Boolean = true, + val canDisableUsbSignal: Boolean = true +) + @Serializable object SystemOptions @Composable -fun SystemOptionsScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current +fun SystemOptionsScreen(vm: MyViewModel, onNavigateUp: () -> Unit) { val privilege by Privilege.status.collectAsStateWithLifecycle() var dialog by remember { mutableIntStateOf(0) } + val status by vm.systemOptionsStatus.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { vm.getSystemOptionsStatus() } MyScaffold(R.string.options, onNavigateUp, 0.dp) { - SwitchItem(R.string.disable_cam, icon = R.drawable.no_photography_fill0, - getState = { Privilege.DPM.getCameraDisabled(null) }, onCheckedChange = { - Privilege.DPM.setCameraDisabled(Privilege.DAR, it) - createShortcuts(context) - } - ) - SwitchItem(R.string.disable_screen_capture, icon = R.drawable.screenshot_fill0, - getState = { Privilege.DPM.getScreenCaptureDisabled(null) }, - onCheckedChange = { Privilege.DPM.setScreenCaptureDisabled(Privilege.DAR, it) } - ) - if(VERSION.SDK_INT >= 34 && (privilege.device || (privilege.profile && privilege.affiliated))) { - SwitchItem(R.string.disable_status_bar, icon = R.drawable.notifications_fill0, - getState = { Privilege.DPM.isStatusBarDisabled}, - onCheckedChange = { Privilege.DPM.setStatusBarDisabled(Privilege.DAR, it) } - ) + SwitchItem(R.string.disable_cam, status.cameraDisabled, vm::setCameraDisabled, + R.drawable.no_photography_fill0) + SwitchItem(R.string.disable_screen_capture, status.screenCaptureDisabled, + vm::setScreenCaptureDisabled, R.drawable.screenshot_fill0) + if (VERSION.SDK_INT >= 34 && privilege.run { device || (profile && affiliated) }) { + SwitchItem(R.string.disable_status_bar, status.statusBarDisabled, + vm::setStatusBarDisabled, R.drawable.notifications_fill0) } - if(privilege.device || privilege.org) { + if (privilege.device || privilege.org) { if(VERSION.SDK_INT >= 30) { - SwitchItem(R.string.auto_time, icon = R.drawable.schedule_fill0, - getState = { Privilege.DPM.getAutoTimeEnabled(Privilege.DAR) }, - onCheckedChange = { Privilege.DPM.setAutoTimeEnabled(Privilege.DAR, it) } - ) - SwitchItem(R.string.auto_timezone, icon = R.drawable.globe_fill0, - getState = { Privilege.DPM.getAutoTimeZoneEnabled(Privilege.DAR) }, - onCheckedChange = { Privilege.DPM.setAutoTimeZoneEnabled(Privilege.DAR, it) } - ) + SwitchItem(R.string.auto_time, status.autoTimeEnabled, vm::setAutoTimeEnabled, + R.drawable.schedule_fill0) + SwitchItem(R.string.auto_timezone, status.autoTimeZoneEnabled, + vm::setAutoTimeZoneEnabled, R.drawable.globe_fill0) } else { - SwitchItem(R.string.require_auto_time, icon = R.drawable.schedule_fill0, - getState = { Privilege.DPM.autoTimeRequired }, - onCheckedChange = { Privilege.DPM.setAutoTimeRequired(Privilege.DAR, it) }, padding = false) + SwitchItem(R.string.require_auto_time, status.autoTimeRequired, + vm::setAutoTimeRequired, R.drawable.schedule_fill0) } } - if (!privilege.work) SwitchItem(R.string.master_mute, icon = R.drawable.volume_off_fill0, - getState = { Privilege.DPM.isMasterVolumeMuted(Privilege.DAR) }, onCheckedChange = { - Privilege.DPM.setMasterVolumeMuted(Privilege.DAR, it) - createShortcuts(context) - } - ) - if(VERSION.SDK_INT >= 26) { + if (!privilege.work) SwitchItem(R.string.master_mute, + status.masterVolumeMuted, vm::setMasterVolumeMuted, R.drawable.volume_off_fill0) + if (VERSION.SDK_INT >= 26) { SwitchItem(R.string.backup_service, icon = R.drawable.backup_fill0, - getState = { Privilege.DPM.isBackupServiceEnabled(Privilege.DAR) }, - onCheckedChange = { Privilege.DPM.setBackupServiceEnabled(Privilege.DAR, it) }, - onClickBlank = { dialog = 1 } - ) + state = status.backupServiceEnabled, onCheckedChange = vm::setBackupServiceEnabled, + onClickBlank = { dialog = 1 }) } - if(VERSION.SDK_INT >= 24 && privilege.work) { - SwitchItem(R.string.disable_bt_contact_share, icon = R.drawable.account_circle_fill0, - getState = { Privilege.DPM.getBluetoothContactSharingDisabled(Privilege.DAR) }, - onCheckedChange = { Privilege.DPM.setBluetoothContactSharingDisabled(Privilege.DAR, it) } - ) + if (VERSION.SDK_INT >= 24 && privilege.work) { + SwitchItem(R.string.disable_bt_contact_share, status.btContactSharingDisabled, + vm::setBtContactSharingDisabled, R.drawable.account_circle_fill0) } - if(VERSION.SDK_INT >= 30 && privilege.device) { - SwitchItem(R.string.common_criteria_mode , icon =R.drawable.security_fill0, - getState = { Privilege.DPM.isCommonCriteriaModeEnabled(Privilege.DAR) }, - onCheckedChange = { Privilege.DPM.setCommonCriteriaModeEnabled(Privilege.DAR, it) }, - onClickBlank = { dialog = 2 } - ) + if(VERSION.SDK_INT >= 30 && (privilege.device || privilege.org)) { + SwitchItem(R.string.common_criteria_mode, icon = R.drawable.security_fill0, + state = status.commonCriteriaMode, + onCheckedChange = vm::setCommonCriteriaModeEnabled, + onClickBlank = { dialog = 2 }) } - if(VERSION.SDK_INT >= 31 && (privilege.device || privilege.org) && Privilege.DPM.canUsbDataSignalingBeDisabled()) { - SwitchItem( - R.string.disable_usb_signal, icon = R.drawable.usb_fill0, getState = { !Privilege.DPM.isUsbDataSignalingEnabled }, - onCheckedChange = { Privilege.DPM.isUsbDataSignalingEnabled = !it }, - ) + if (VERSION.SDK_INT >= 31 && (privilege.device || privilege.org) && status.canDisableUsbSignal) { + SwitchItem(R.string.enable_usb_signal, status.usbSignalEnabled, + vm::setUsbSignalEnabled, R.drawable.usb_fill0) } } if(dialog != 0) AlertDialog( @@ -414,23 +391,26 @@ fun SystemOptionsScreen(onNavigateUp: () -> Unit) { @Serializable object Keyguard @Composable -fun KeyguardScreen(onNavigateUp: () -> Unit) { +fun KeyguardScreen( + setKeyguardDisabled: (Boolean) -> Boolean, lock: (Boolean) -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current val privilege by Privilege.status.collectAsStateWithLifecycle() MyScaffold(R.string.keyguard, onNavigateUp) { - if(VERSION.SDK_INT >= 23 && (privilege.device || (VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated))) { + if (VERSION.SDK_INT >= 23 && (privilege.device || + (VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated))) { Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() ) { Button( - onClick = { context.showOperationResultToast(Privilege.DPM.setKeyguardDisabled(Privilege.DAR, true)) }, + onClick = { context.showOperationResultToast(setKeyguardDisabled(true)) }, modifier = Modifier.fillMaxWidth(0.49F) ) { Text(stringResource(R.string.disable)) } Button( - onClick = { context.showOperationResultToast(Privilege.DPM.setKeyguardDisabled(Privilege.DAR, false)) }, + onClick = { context.showOperationResultToast(setKeyguardDisabled(false)) }, modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.enable)) @@ -441,69 +421,61 @@ fun KeyguardScreen(onNavigateUp: () -> Unit) { } if(VERSION.SDK_INT >= 23) Text(text = stringResource(R.string.lock_now), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 2.dp)) - var flag by remember { mutableIntStateOf(0) } - if(VERSION.SDK_INT >= 26 && privilege.work) { - CheckBoxItem( - R.string.evict_credential_encryption_key, - flag and FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY != 0 - ) { flag = flag xor FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY } - Spacer(Modifier.padding(vertical = 2.dp)) - } + var evictKey by remember { mutableStateOf(false) } Button( - onClick = { - if(VERSION.SDK_INT >= 26) Privilege.DPM.lockNow(flag) else Privilege.DPM.lockNow() - }, + onClick = { lock(evictKey) }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.lock_now)) } - if(VERSION.SDK_INT >= 26 && privilege.work) { + if (VERSION.SDK_INT >= 26 && privilege.work) { + CheckBoxItem(R.string.evict_credential_encryption_key, evictKey) { evictKey = true } + Spacer(Modifier.height(5.dp)) Notes(R.string.info_evict_credential_encryption_key) } } } +data class HardwareProperties( + val temperatures: Map> = emptyMap(), + val cpuUsages: List> = emptyList(), + val fanSpeeds: List = emptyList() +) + +@RequiresApi(24) +val temperatureTypes = mapOf( + HardwarePropertiesManager.DEVICE_TEMPERATURE_CPU to R.string.cpu_temp, + HardwarePropertiesManager.DEVICE_TEMPERATURE_GPU to R.string.gpu_temp, + HardwarePropertiesManager.DEVICE_TEMPERATURE_BATTERY to R.string.battery_temp, + HardwarePropertiesManager.DEVICE_TEMPERATURE_SKIN to R.string.skin_temp +) + @Serializable object HardwareMonitor @RequiresApi(24) @Composable -fun HardwareMonitorScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - val hpm = context.getSystemService(HardwarePropertiesManager::class.java) +fun HardwareMonitorScreen( + hardwareProperties: StateFlow, getHardwareProperties: suspend () -> Unit, + setRefreshInterval: (Float) -> Unit, + onNavigateUp: () -> Unit +) { + val properties by hardwareProperties.collectAsStateWithLifecycle() var refreshInterval by remember { mutableFloatStateOf(1F) } val refreshIntervalMs = (refreshInterval * 1000).roundToLong() - val temperatures = remember { mutableStateMapOf>() } - val tempTypeMap = mapOf( - HardwarePropertiesManager.DEVICE_TEMPERATURE_CPU to R.string.cpu_temp, - HardwarePropertiesManager.DEVICE_TEMPERATURE_GPU to R.string.gpu_temp, - HardwarePropertiesManager.DEVICE_TEMPERATURE_BATTERY to R.string.battery_temp, - HardwarePropertiesManager.DEVICE_TEMPERATURE_SKIN to R.string.skin_temp - ) - val cpuUsages = remember { mutableStateListOf>() } - val fanSpeeds = remember { mutableStateListOf() } - fun refresh() { - cpuUsages.clear() - cpuUsages.addAll(hpm.cpuUsages.map { it.active to it.total }) - temperatures.clear() - tempTypeMap.forEach { - temperatures += it.key to hpm.getDeviceTemperatures(it.key, HardwarePropertiesManager.TEMPERATURE_CURRENT).toList() - } - fanSpeeds.clear() - fanSpeeds.addAll(hpm.fanSpeeds.toList()) - } LaunchedEffect(Unit) { - while(true) { - refresh() - delay(refreshIntervalMs) - } + getHardwareProperties() } MyScaffold(R.string.hardware_monitor, onNavigateUp) { - Text(stringResource(R.string.refresh_interval), style = typography.titleLarge, modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)) - Slider(refreshInterval, { refreshInterval = it }, valueRange = 0.5F..2F, steps = 14) + Text(stringResource(R.string.refresh_interval), Modifier.padding(top = 8.dp, bottom = 4.dp), + style = typography.titleLarge) + Slider(refreshInterval, { + refreshInterval = it + setRefreshInterval(it) + }, valueRange = 0.5F..2F, steps = 14) Text("${refreshIntervalMs}ms") Spacer(Modifier.padding(vertical = 10.dp)) - temperatures.forEach { tempMapItem -> - Text(stringResource(tempTypeMap[tempMapItem.key]!!), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) + properties.temperatures.forEach { tempMapItem -> + Text(stringResource(temperatureTypes[tempMapItem.key]!!), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) if(tempMapItem.value.isEmpty()) { Text(stringResource(R.string.unsupported)) } else { @@ -517,10 +489,10 @@ fun HardwareMonitorScreen(onNavigateUp: () -> Unit) { Spacer(Modifier.padding(vertical = 10.dp)) } Text(stringResource(R.string.cpu_usages), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) - if(cpuUsages.isEmpty()) { + if (properties.cpuUsages.isEmpty()) { Text(stringResource(R.string.unsupported)) } else { - cpuUsages.forEachIndexed { index, usage -> + properties.cpuUsages.forEachIndexed { index, usage -> Row(modifier = Modifier.padding(vertical = 4.dp)) { Text(index.toString(), style = typography.titleMedium, modifier = Modifier.padding(start = 8.dp, end = 12.dp)) Column { @@ -532,10 +504,10 @@ fun HardwareMonitorScreen(onNavigateUp: () -> Unit) { } Spacer(Modifier.padding(vertical = 10.dp)) Text(stringResource(R.string.fan_speeds), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) - if(fanSpeeds.isEmpty()) { + if (properties.fanSpeeds.isEmpty()) { Text(stringResource(R.string.unsupported)) } else { - fanSpeeds.forEachIndexed { index, speed -> + properties.fanSpeeds.forEachIndexed { index, speed -> Row(modifier = Modifier.padding(vertical = 4.dp)) { Text(index.toString(), style = typography.titleMedium, modifier = Modifier.padding(start = 8.dp, end = 12.dp)) Text("$speed RPM") @@ -550,10 +522,13 @@ fun HardwareMonitorScreen(onNavigateUp: () -> Unit) { @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(28) @Composable -fun ChangeTimeScreen(onNavigateUp: () -> Unit) { +fun ChangeTimeScreen(setTime: (Long) -> Boolean, onNavigateUp: () -> Unit) { val context = LocalContext.current val focusMgr = LocalFocusManager.current + var tab by remember { mutableIntStateOf(0) } val pagerState = rememberPagerState { 2 } + tab = pagerState.currentPage + val coroutine = rememberCoroutineScope() var picker by remember { mutableIntStateOf(0) } //0:None, 1:DatePicker, 2:TimePicker val datePickerState = rememberDatePickerState() val timePickerState = rememberTimePickerState() @@ -561,87 +536,90 @@ fun ChangeTimeScreen(onNavigateUp: () -> Unit) { val timeInteractionSource = remember { MutableInteractionSource() } if(dateInteractionSource.collectIsPressedAsState().value) picker = 1 if(timeInteractionSource.collectIsPressedAsState().value) picker = 2 - MyScaffold(R.string.change_time, onNavigateUp) { - SingleChoiceSegmentedButtonRow( - modifier = Modifier - .fillMaxWidth() - .padding(top = 4.dp) + Scaffold( + topBar = { + TopAppBar( + { Text(stringResource(R.string.change_time)) }, + navigationIcon = { NavIcon(onNavigateUp) }, + colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer) + ) + }, + contentWindowInsets = WindowInsets.ime + ) { paddingValues -> + Column( + Modifier + .fillMaxSize() + .padding(paddingValues) ) { - val coroutine = rememberCoroutineScope() - SegmentedButton( - selected = pagerState.targetPage == 0, shape = SegmentedButtonDefaults.itemShape(0, 2), - onClick = { - coroutine.launch { - pagerState.animateScrollToPage(0) - } - } - ) { - Text(stringResource(R.string.selector)) + TabRow(tab) { + Tab( + tab == 0, { coroutine.launch { pagerState.animateScrollToPage(0) } }, + text = { Text(stringResource(R.string.selector)) } + ) + Tab( + tab == 1, { coroutine.launch { pagerState.animateScrollToPage(1) } }, + text = { Text(stringResource(R.string.manually_input)) } + ) } - SegmentedButton( - selected = pagerState.targetPage == 1, shape = SegmentedButtonDefaults.itemShape(1, 2), - onClick = { - coroutine.launch { - pagerState.animateScrollToPage(1) - } - } - ) { - Text(stringResource(R.string.manually_input)) - } - } - HorizontalPager( - state = pagerState, modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.Top - ) { page -> - Column(Modifier.padding(top = 4.dp)) { - if(page == 0) { - OutlinedTextField( - value = datePickerState.selectedDateMillis?.humanReadableDate ?: "", - onValueChange = {}, readOnly = true, - label = { Text(stringResource(R.string.date)) }, - interactionSource = dateInteractionSource, - modifier = Modifier.fillMaxWidth() - ) - OutlinedTextField( - value = timePickerState.hour.toString() + ":" + timePickerState.minute.toString(), - onValueChange = {}, readOnly = true, - label = { Text(stringResource(R.string.time)) }, - interactionSource = timeInteractionSource, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - ) - Button( - onClick = { - val timeMillis = datePickerState.selectedDateMillis!! + timePickerState.hour * 3600000 + timePickerState.minute * 60000 - context.showOperationResultToast(Privilege.DPM.setTime(Privilege.DAR, timeMillis)) - }, - modifier = Modifier.fillMaxWidth(), - enabled = datePickerState.selectedDateMillis != null - ) { - Text(stringResource(R.string.apply)) - } - } else { - var inputTime by remember { mutableStateOf("") } - OutlinedTextField( - value = inputTime, - label = { Text(stringResource(R.string.time_unit_ms)) }, - onValueChange = { inputTime = it }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), - modifier = Modifier.fillMaxWidth() - ) - Button( - onClick = { - val timeMillis = inputTime.toLong() - context.showOperationResultToast(Privilege.DPM.setTime(Privilege.DAR, timeMillis)) - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - enabled = inputTime.toLongOrNull() != null - ) { - Text(stringResource(R.string.apply)) + HorizontalPager( + pagerState, Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { page -> + Column( + Modifier + .fillMaxSize() + .padding(top = 8.dp) + .padding(horizontal = HorizontalPadding) + ) { + if(page == 0) { + OutlinedTextField( + value = datePickerState.selectedDateMillis?.humanReadableDate ?: "", + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.date)) }, + interactionSource = dateInteractionSource, + modifier = Modifier.fillMaxWidth() + ) + OutlinedTextField( + value = timePickerState.hour.toString() + ":" + timePickerState.minute.toString(), + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.time)) }, + interactionSource = timeInteractionSource, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) + Button( + onClick = { + val timeMillis = datePickerState.selectedDateMillis!! + + timePickerState.hour * 3600000 + timePickerState.minute * 60000 + context.showOperationResultToast(setTime(timeMillis)) + }, + modifier = Modifier.fillMaxWidth(), + enabled = datePickerState.selectedDateMillis != null + ) { + Text(stringResource(R.string.apply)) + } + } else { + var inputTime by remember { mutableStateOf("") } + OutlinedTextField( + value = inputTime, + label = { Text(stringResource(R.string.time_unit_ms)) }, + onValueChange = { inputTime = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), + modifier = Modifier.fillMaxWidth() + ) + Button( + onClick = { + context.showOperationResultToast(setTime(inputTime.toLong())) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + enabled = inputTime.toLongOrNull() != null + ) { + Text(stringResource(R.string.apply)) + } } } } @@ -672,7 +650,7 @@ fun ChangeTimeScreen(onNavigateUp: () -> Unit) { @RequiresApi(28) @Composable -fun ChangeTimeZoneScreen(onNavigateUp: () -> Unit) { +fun ChangeTimeZoneScreen(setTimeZone: (String) -> Boolean, onNavigateUp: () -> Unit) { val context = LocalContext.current val focusMgr = LocalFocusManager.current var inputTimezone by remember { mutableStateOf("") } @@ -694,9 +672,10 @@ fun ChangeTimeZoneScreen(onNavigateUp: () -> Unit) { Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { - context.showOperationResultToast(Privilege.DPM.setTimeZone(Privilege.DAR, inputTimezone)) + context.showOperationResultToast(setTimeZone(inputTimezone)) }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + enabled = inputTimezone.isNotEmpty() ) { Text(stringResource(R.string.apply)) } @@ -735,8 +714,11 @@ fun ChangeTimeZoneScreen(onNavigateUp: () -> Unit) { @RequiresApi(36) @Composable -fun AutoTimePolicyScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.auto_time_policy, onNavigateUp, 0.dp) { - var policy by remember { mutableIntStateOf(Privilege.DPM.autoTimePolicy) } +fun AutoTimePolicyScreen( + getPolicy: () -> Int, setPolicy: (Int) -> Unit, onNavigateUp: () -> Unit +) = MyScaffold(R.string.auto_time_policy, onNavigateUp, 0.dp) { + val context = LocalContext.current + var policy by remember { mutableIntStateOf(getPolicy()) } listOf( DevicePolicyManager.AUTO_TIME_ENABLED to R.string.enable, DevicePolicyManager.AUTO_TIME_DISABLED to R.string.disabled, @@ -746,10 +728,15 @@ fun AutoTimePolicyScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.auto_ti policy = it.first } } - Button({ - Privilege.DPM.autoTimePolicy = policy - policy = Privilege.DPM.autoTimePolicy - }, Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)) { + Button( + { + setPolicy(policy) + context.showOperationResultToast(true) + }, + Modifier + .fillMaxWidth() + .padding(horizontal = HorizontalPadding) + ) { Text(stringResource(R.string.apply)) } } @@ -758,8 +745,11 @@ fun AutoTimePolicyScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.auto_ti @RequiresApi(36) @Composable -fun AutoTimeZonePolicyScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.auto_timezone_policy, onNavigateUp, 0.dp) { - var policy by remember { mutableIntStateOf(Privilege.DPM.autoTimeZonePolicy) } +fun AutoTimeZonePolicyScreen( + getPolicy: () -> Int, setPolicy: (Int) -> Unit, onNavigateUp: () -> Unit +) = MyScaffold(R.string.auto_timezone_policy, onNavigateUp, 0.dp) { + val context = LocalContext.current + var policy by remember { mutableIntStateOf(getPolicy()) } listOf( DevicePolicyManager.AUTO_TIME_ZONE_ENABLED to R.string.enable, DevicePolicyManager.AUTO_TIME_ZONE_DISABLED to R.string.disabled, @@ -770,9 +760,11 @@ fun AutoTimeZonePolicyScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.aut } } Button({ - Privilege.DPM.autoTimeZonePolicy = policy - policy = Privilege.DPM.autoTimeZonePolicy - }, Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)) { + setPolicy(policy) + context.showOperationResultToast(true) + }, Modifier + .fillMaxWidth() + .padding(horizontal = HorizontalPadding)) { Text(stringResource(R.string.apply)) } } @@ -961,11 +953,11 @@ fun KeyPairs(navCtrl: NavHostController) { @RequiresApi(35) @Composable -fun ContentProtectionPolicyScreen(onNavigateUp: () -> Unit) { +fun ContentProtectionPolicyScreen( + getPolicy: () -> Int, setPolicy: (Int) -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current - var policy by remember { mutableIntStateOf(DevicePolicyManager.CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY) } - fun refresh() { policy = Privilege.DPM.getContentProtectionPolicy(Privilege.DAR) } - LaunchedEffect(Unit) { refresh() } + var policy by remember { mutableIntStateOf(getPolicy()) } MyScaffold(R.string.content_protection_policy, onNavigateUp, 0.dp) { mapOf( DevicePolicyManager.CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY to R.string.not_controlled_by_policy, @@ -976,8 +968,7 @@ fun ContentProtectionPolicyScreen(onNavigateUp: () -> Unit) { } Button( onClick = { - Privilege.DPM.setContentProtectionPolicy(Privilege.DAR, policy) - refresh() + setPolicy(policy) context.showOperationResultToast(true) }, modifier = Modifier @@ -994,9 +985,11 @@ fun ContentProtectionPolicyScreen(onNavigateUp: () -> Unit) { @RequiresApi(23) @Composable -fun PermissionPolicyScreen(onNavigateUp: () -> Unit) { +fun PermissionPolicyScreen( + getPolicy: () -> Int, setPolicy: (Int) -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current - var selectedPolicy by remember { mutableIntStateOf(Privilege.DPM.getPermissionPolicy(Privilege.DAR)) } + var selectedPolicy by remember { mutableIntStateOf(getPolicy()) } MyScaffold(R.string.permission_policy, onNavigateUp, 0.dp) { FullWidthRadioButtonItem(R.string.default_stringres, selectedPolicy == PERMISSION_POLICY_PROMPT) { selectedPolicy = PERMISSION_POLICY_PROMPT @@ -1007,15 +1000,14 @@ fun PermissionPolicyScreen(onNavigateUp: () -> Unit) { FullWidthRadioButtonItem(R.string.auto_deny, selectedPolicy == PERMISSION_POLICY_AUTO_DENY) { selectedPolicy = PERMISSION_POLICY_AUTO_DENY } - Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { - Privilege.DPM.setPermissionPolicy(Privilege.DAR,selectedPolicy) + setPolicy(selectedPolicy) context.showOperationResultToast(true) }, modifier = Modifier .fillMaxWidth() - .padding(horizontal = HorizontalPadding) + .padding(HorizontalPadding, 5.dp) ) { Text(stringResource(R.string.apply)) } @@ -1027,24 +1019,19 @@ fun PermissionPolicyScreen(onNavigateUp: () -> Unit) { @RequiresApi(34) @Composable -fun MtePolicyScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - var selectedMtePolicy by remember { mutableIntStateOf(Privilege.DPM.mtePolicy) } +fun MtePolicyScreen( + getPolicy: () -> Int, setPolicy: (Int) -> Boolean, onNavigateUp: () -> Unit +) { + var policy by remember { mutableIntStateOf(getPolicy()) } MyScaffold(R.string.mte_policy, onNavigateUp, 0.dp) { - FullWidthRadioButtonItem(R.string.decide_by_user, selectedMtePolicy == MTE_NOT_CONTROLLED_BY_POLICY) { - selectedMtePolicy = MTE_NOT_CONTROLLED_BY_POLICY + FullWidthRadioButtonItem(R.string.decide_by_user, policy == MTE_NOT_CONTROLLED_BY_POLICY) { + policy = MTE_NOT_CONTROLLED_BY_POLICY } - FullWidthRadioButtonItem(R.string.enabled, selectedMtePolicy == MTE_ENABLED) { selectedMtePolicy = MTE_ENABLED } - FullWidthRadioButtonItem(R.string.disabled, selectedMtePolicy == MTE_DISABLED) { selectedMtePolicy = MTE_DISABLED } + FullWidthRadioButtonItem(R.string.enabled, policy == MTE_ENABLED) { policy = MTE_ENABLED } + FullWidthRadioButtonItem(R.string.disabled, policy == MTE_DISABLED) { policy = MTE_DISABLED } Button( onClick = { - try { - Privilege.DPM.mtePolicy = selectedMtePolicy - context.showOperationResultToast(true) - } catch(_: java.lang.UnsupportedOperationException) { - context.popToast(R.string.unsupported) - } - selectedMtePolicy = Privilege.DPM.mtePolicy + if (!setPolicy(policy)) policy = getPolicy() }, modifier = Modifier .fillMaxWidth() @@ -1060,9 +1047,12 @@ fun MtePolicyScreen(onNavigateUp: () -> Unit) { @RequiresApi(31) @Composable -fun NearbyStreamingPolicyScreen(onNavigateUp: () -> Unit) { +fun NearbyStreamingPolicyScreen( + getAppPolicy: () -> Int, setAppPolicy: (Int) -> Unit, getNotificationPolicy: () -> Int, + setNotificationPolicy: (Int) -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current - var appPolicy by remember { mutableIntStateOf(Privilege.DPM.nearbyAppStreamingPolicy) } + var appPolicy by remember { mutableIntStateOf(getAppPolicy()) } MySmallTitleScaffold(R.string.nearby_streaming_policy, onNavigateUp, 0.dp) { Text( stringResource(R.string.nearby_app_streaming), @@ -1080,8 +1070,7 @@ fun NearbyStreamingPolicyScreen(onNavigateUp: () -> Unit) { ) { appPolicy = NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY } Button( onClick = { - Privilege.DPM.nearbyAppStreamingPolicy = appPolicy - appPolicy = Privilege.DPM.nearbyAppStreamingPolicy + setAppPolicy(appPolicy) context.showOperationResultToast(true) }, modifier = Modifier @@ -1091,7 +1080,8 @@ fun NearbyStreamingPolicyScreen(onNavigateUp: () -> Unit) { Text(stringResource(R.string.apply)) } Notes(R.string.info_nearby_app_streaming_policy, HorizontalPadding) - var notificationPolicy by remember { mutableIntStateOf(Privilege.DPM.nearbyNotificationStreamingPolicy) } + Spacer(Modifier.height(20.dp)) + var notificationPolicy by remember { mutableIntStateOf(getNotificationPolicy()) } Text( stringResource(R.string.nearby_notification_streaming), Modifier.padding(start = 8.dp, top = 10.dp, bottom = 4.dp), style = typography.titleLarge @@ -1114,8 +1104,7 @@ fun NearbyStreamingPolicyScreen(onNavigateUp: () -> Unit) { ) { notificationPolicy = NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY } Button( onClick = { - Privilege.DPM.nearbyNotificationStreamingPolicy = notificationPolicy - notificationPolicy = Privilege.DPM.nearbyNotificationStreamingPolicy + setNotificationPolicy(notificationPolicy) context.showOperationResultToast(true) }, modifier = Modifier @@ -1134,12 +1123,18 @@ fun NearbyStreamingPolicyScreen(onNavigateUp: () -> Unit) { @RequiresApi(28) @Composable fun LockTaskModeScreen( - chosenPackage: Channel, onChoosePackage: () -> Unit, onNavigateUp: () -> Unit + chosenPackage: Channel, onChoosePackage: () -> Unit, + lockTaskPackages: StateFlow>, getLockTaskPackages: () -> Unit, + setLockTaskPackage: (String, Boolean) -> Unit, startLockTaskMode: (String, String) -> Unit, + getLockTaskFeatures: () -> Int, setLockTaskFeature: (Int) -> String?, onNavigateUp: () -> Unit ) { val coroutine = rememberCoroutineScope() val pagerState = rememberPagerState { 3 } var tabIndex by remember { mutableIntStateOf(0) } tabIndex = pagerState.targetPage + LaunchedEffect(Unit) { + getLockTaskPackages() + } Scaffold( topBar = { TopAppBar( @@ -1170,26 +1165,12 @@ fun LockTaskModeScreen( ) } HorizontalPager(pagerState, verticalAlignment = Alignment.Top) { page -> - if(page == 0 || page == 1) { - Column( - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = HorizontalPadding) - .padding(bottom = 80.dp) - ) { - if(page == 0) StartLockTaskMode(chosenPackage, onChoosePackage) - else LockTaskPackages(chosenPackage, onChoosePackage) - } + if(page == 0) { + StartLockTaskMode(startLockTaskMode, chosenPackage, onChoosePackage) + } else if (page == 1) { + LockTaskPackages(chosenPackage, onChoosePackage, lockTaskPackages, setLockTaskPackage) } else { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(bottom = 80.dp) - ) { - LockTaskFeatures() - } + LockTaskFeatures(getLockTaskFeatures, setLockTaskFeature) } } } @@ -1198,204 +1179,182 @@ fun LockTaskModeScreen( @RequiresApi(28) @Composable -private fun ColumnScope.StartLockTaskMode( +private fun StartLockTaskMode( + startLockTaskMode: (String, String) -> Unit, chosenPackage: Channel, onChoosePackage: () -> Unit ) { - val context = LocalContext.current val focusMgr = LocalFocusManager.current - var startLockTaskApp by rememberSaveable { mutableStateOf("") } - var startLockTaskActivity by rememberSaveable { mutableStateOf("") } + var packageName by rememberSaveable { mutableStateOf("") } + var activity by rememberSaveable { mutableStateOf("") } var specifyActivity by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { - startLockTaskApp = chosenPackage.receive() + packageName = chosenPackage.receive() } - Spacer(Modifier.padding(vertical = 5.dp)) - PackageNameTextField(startLockTaskApp, onChoosePackage, - Modifier.padding(vertical = 3.dp), { startLockTaskApp = it }) - CheckBoxItem(R.string.specify_activity, specifyActivity) { specifyActivity = it } - AnimatedVisibility(specifyActivity) { - OutlinedTextField( - value = startLockTaskActivity, - onValueChange = { startLockTaskActivity = it }, - label = { Text("Activity") }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = HorizontalPadding) + .verticalScroll(rememberScrollState()) + ) { + Spacer(Modifier.height(5.dp)) + PackageNameTextField(packageName, onChoosePackage) { packageName = it } + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(specifyActivity, { + specifyActivity = it + activity = "" + }) + OutlinedTextField( + value = activity, + onValueChange = { activity = it }, + label = { Text("Activity") }, + enabled = specifyActivity, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), + modifier = Modifier.fillMaxWidth() + ) + } + Button( modifier = Modifier .fillMaxWidth() - .padding(bottom = 5.dp) - ) + .padding(bottom = 5.dp), + onClick = { + startLockTaskMode(packageName, activity) + }, + enabled = packageName.isNotBlank() && (!specifyActivity || activity.isNotBlank()) + ) { + Text(stringResource(R.string.start)) + } + Notes(R.string.info_start_lock_task_mode) } - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - if(!NotificationUtils.checkPermission(context)) return@Button - if(!Privilege.DPM.isLockTaskPermitted(startLockTaskApp)) { - context.popToast(R.string.app_not_allowed) - return@Button - } - val options = ActivityOptions.makeBasic().setLockTaskEnabled(true) - val packageManager = context.packageManager - val launchIntent = if(specifyActivity) Intent().setComponent(ComponentName(startLockTaskApp, startLockTaskActivity)) - else packageManager.getLaunchIntentForPackage(startLockTaskApp) - if (launchIntent != null) { - context.startActivity(launchIntent, options.toBundle()) - } else { - context.showOperationResultToast(false) - } - }, - enabled = startLockTaskApp.isNotBlank() && (!specifyActivity || startLockTaskActivity.isNotBlank()) - ) { - Text(stringResource(R.string.start)) - } - Notes(R.string.info_start_lock_task_mode) } @RequiresApi(26) @Composable -private fun ColumnScope.LockTaskPackages( - chosenPackage: Channel, onChoosePackage: () -> Unit +private fun LockTaskPackages( + chosenPackage: Channel, onChoosePackage: () -> Unit, + lockTaskPackages: StateFlow>, setLockTaskPackage: (String, Boolean) -> Unit ) { - val context = LocalContext.current - val lockTaskPackages = remember { mutableStateListOf() } - var input by rememberSaveable { mutableStateOf("") } + val packages by lockTaskPackages.collectAsStateWithLifecycle() + var packageName by rememberSaveable { mutableStateOf("") } LaunchedEffect(Unit) { - lockTaskPackages.addAll(Privilege.DPM.getLockTaskPackages(Privilege.DAR)) - input = chosenPackage.receive() + packageName = chosenPackage.receive() } - Spacer(Modifier.padding(vertical = 5.dp)) - if(lockTaskPackages.isEmpty()) Text(text = stringResource(R.string.none)) - for(i in lockTaskPackages) { - ListItem(i) { lockTaskPackages -= i } - } - PackageNameTextField(input, onChoosePackage, - Modifier.padding(vertical = 3.dp), { input = it }) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Button( - onClick = { - lockTaskPackages.add(input) - input = "" - }, - modifier = Modifier.fillMaxWidth(0.49F) - ) { - Text(stringResource(R.string.add)) + LazyColumn { + items(packages, { it.name }) { + ApplicationItem(it) { setLockTaskPackage(it.name, false) } } - Button( - onClick = { - lockTaskPackages.remove(input) - input = "" - }, - modifier = Modifier.fillMaxWidth(0.96F) - ) { - Text(stringResource(R.string.remove)) + item { + Column(Modifier + .padding(horizontal = HorizontalPadding) + .padding(bottom = 40.dp)) { + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(vertical = 3.dp)) { packageName = it } + Button( + onClick = { + setLockTaskPackage(packageName, true) + packageName = "" + }, + modifier = Modifier.fillMaxWidth(), + enabled = packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } + Notes(R.string.info_lock_task_packages) + } } } - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - Privilege.DPM.setLockTaskPackages(Privilege.DAR, lockTaskPackages.toTypedArray()) - context.showOperationResultToast(true) - } - ) { - Text(stringResource(R.string.apply)) - } - Notes(R.string.info_lock_task_packages) } @RequiresApi(28) @Composable -private fun ColumnScope.LockTaskFeatures() { +private fun LockTaskFeatures( + getLockTaskFeatures: () -> Int, setLockTaskFeature: (Int) -> String? +) { val context = LocalContext.current - var flags by remember { mutableIntStateOf(0) } - var custom by rememberSaveable { mutableStateOf(false) } + var flags by remember { mutableIntStateOf(getLockTaskFeatures()) } var errorMessage by remember { mutableStateOf(null) } - fun refresh() { - flags = Privilege.DPM.getLockTaskFeatures(Privilege.DAR) - custom = flags != 0 - } - LaunchedEffect(Unit) { refresh() } - Spacer(Modifier.padding(vertical = 5.dp)) - FullWidthRadioButtonItem(R.string.disable_all, !custom) { custom = false } - FullWidthRadioButtonItem(R.string.custom, custom) { custom = true } - AnimatedVisibility(custom, Modifier.padding(top = 4.dp)) { - Column { - listOf( - DevicePolicyManager.LOCK_TASK_FEATURE_SYSTEM_INFO to R.string.ltf_sys_info, - DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS to R.string.ltf_notifications, - DevicePolicyManager.LOCK_TASK_FEATURE_HOME to R.string.ltf_home, - DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW to R.string.ltf_overview, - DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS to R.string.ltf_global_actions, - DevicePolicyManager.LOCK_TASK_FEATURE_KEYGUARD to R.string.ltf_keyguard - ).let { - if(VERSION.SDK_INT >= 30) - it.plus(DevicePolicyManager.LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK to R.string.ltf_block_activity_start_in_task) - else it - }.forEach { (id, title) -> - FullWidthCheckBoxItem(title, flags and id != 0) { flags = flags xor id } - } - } - } - Button( - modifier = Modifier + Column( + Modifier .fillMaxWidth() - .padding(vertical = 4.dp, horizontal = HorizontalPadding), - onClick = { - try { - Privilege.DPM.setLockTaskFeatures(Privilege.DAR, flags) - context.showOperationResultToast(true) - } catch (e: IllegalArgumentException) { - errorMessage = e.message - } - refresh() - } + .verticalScroll(rememberScrollState()) ) { - Text(stringResource(R.string.apply)) + Spacer(Modifier.padding(vertical = 5.dp)) + listOf( + DevicePolicyManager.LOCK_TASK_FEATURE_SYSTEM_INFO to R.string.ltf_sys_info, + DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS to R.string.ltf_notifications, + DevicePolicyManager.LOCK_TASK_FEATURE_HOME to R.string.ltf_home, + DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW to R.string.ltf_overview, + DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS to R.string.ltf_global_actions, + DevicePolicyManager.LOCK_TASK_FEATURE_KEYGUARD to R.string.ltf_keyguard + ).let { + if(VERSION.SDK_INT >= 30) it.plus( + DevicePolicyManager.LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK to + R.string.ltf_block_activity_start_in_task) + else it + }.forEach { (id, title) -> + FullWidthCheckBoxItem(title, flags and id != 0) { flags = flags xor id } + } + Button( + onClick = { + val result = setLockTaskFeature(flags) + if (result == null) { + context.showOperationResultToast(true) + } else { + errorMessage = result + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = HorizontalPadding) + ) { + Text(stringResource(R.string.apply)) + } + Spacer(Modifier.height(40.dp)) + ErrorDialog(errorMessage) { errorMessage = null } } - ErrorDialog(errorMessage) { errorMessage = null } } data class CaCertInfo( val hash: String, - val data: ByteArray + val serialNumber: String, + val issuer: String, + val subject: String, + val issuedTime: String, + val expiresTime: String, + val bytes: ByteArray ) @Serializable object CaCert @OptIn(ExperimentalMaterial3Api::class, ExperimentalStdlibApi::class) @Composable -fun CaCertScreen(onNavigateUp: () -> Unit) { +fun CaCertScreen( + caCertificates: StateFlow>, getCerts: () -> Unit, + installCert: (CaCertInfo) -> Boolean, parseCert: (Uri) -> CaCertInfo?, + exportCert: (Uri, CaCertInfo) -> Unit, uninstallCert: (CaCertInfo) -> Unit, + uninstallAllCerts: () -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current /** 0:none, 1:install, 2:info, 3:uninstall all */ var dialog by remember { mutableIntStateOf(0) } - var caCertByteArray by remember { mutableStateOf(byteArrayOf()) } - val coroutine = rememberCoroutineScope() - val getCertLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {uri -> + val caCerts by caCertificates.collectAsStateWithLifecycle() + var selectedCaCert by remember { mutableStateOf(null) } + val getCertLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.OpenDocument()) { uri -> if(uri != null) { - uriToStream(context, uri) { - caCertByteArray = it.readBytes() - } + selectedCaCert = parseCert(uri) dialog = 1 } } - val exportCertLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument()) { uri -> - if(uri != null) { - context.contentResolver.openOutputStream(uri)?.use { - it.write(caCertByteArray) - } - context.showOperationResultToast(true) - } + val exportCertLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument()) { uri -> + if(uri != null) exportCert(uri, selectedCaCert!!) } - val caCerts = remember { mutableStateListOf() } - fun refresh() { - caCerts.clear() - coroutine.launch(Dispatchers.IO) { - val md = MessageDigest.getInstance("SHA-256") - Privilege.DPM.getInstalledCaCerts(Privilege.DAR).forEach { ba -> - val hash = md.digest(ba).toHexString() - withContext(Dispatchers.Main) { caCerts += CaCertInfo(hash, ba) } - } - } - } - LaunchedEffect(Unit) { refresh() } + LaunchedEffect(Unit) { getCerts() } Scaffold( topBar = { TopAppBar( @@ -1429,8 +1388,7 @@ fun CaCertScreen(onNavigateUp: () -> Unit) { Modifier .fillMaxWidth() .clickable { - caCertByteArray = cert.data - dialog = 2 + selectedCaCert = cert } .animateItem() .padding(vertical = 10.dp, horizontal = 8.dp) @@ -1440,40 +1398,34 @@ fun CaCertScreen(onNavigateUp: () -> Unit) { HorizontalDivider() } item { - if(caCerts.isEmpty()) Text(stringResource(R.string.no_ca_cert), Modifier.padding(top = 8.dp), colorScheme.onSurfaceVariant) - else Spacer(Modifier.padding(vertical = 30.dp)) + Spacer(Modifier.height(40.dp)) } } - if(dialog != 0) AlertDialog( - text = { - if(dialog == 3) Text(stringResource(R.string.uninstall_all_user_ca_cert)) - else { - var text: String - val sha256 = MessageDigest.getInstance("SHA-256").digest(caCertByteArray).toHexString() - try { - val cf = CertificateFactory.getInstance("X.509") - val cert = cf.generateCertificate(caCertByteArray.inputStream()) as X509Certificate - text = "Serial number\n" + cert.serialNumber.toString(16) + "\n\n" + - "Subject\n" + cert.subjectX500Principal.name + "\n\n" + - "Issuer\n" + cert.issuerX500Principal.name + "\n\n" + - "Issued on: " + parseDate(cert.notBefore) + "\n" + - "Expires on: " + parseDate(cert.notAfter) + "\n\n" + - "SHA-256 fingerprint" + "\n$sha256" - } catch(e: Exception) { - e.printStackTrace() - text = stringResource(R.string.parse_cert_failed) - } - Column(Modifier.verticalScroll(rememberScrollState())) { - SelectionContainer { - Text(text) - } - if(dialog == 2) Row(Modifier - .fillMaxWidth() - .padding(top = 4.dp), Arrangement.SpaceBetween) { + if (selectedCaCert != null && (dialog == 1 || dialog == 2)) { + val cert = selectedCaCert!! + AlertDialog( + text = { + Column { + Text("Serial number", style = typography.labelLarge) + SelectionContainer { Text(cert.serialNumber) } + Text("Subject", style = typography.labelLarge) + SelectionContainer { Text(cert.subject) } + Text("Issuer", style = typography.labelLarge) + SelectionContainer { Text(cert.issuer) } + Text("Issued on", style = typography.labelLarge) + SelectionContainer { Text(cert.issuedTime) } + Text("Expires on", style = typography.labelLarge) + SelectionContainer { Text(cert.expiresTime) } + Text("SHA-256 fingerprint", style = typography.labelLarge) + SelectionContainer { Text(cert.hash) } + if (dialog == 2) Row( + Modifier + .fillMaxWidth() + .padding(top = 4.dp), Arrangement.SpaceBetween + ) { TextButton( onClick = { - Privilege.DPM.uninstallCaCert(Privilege.DAR, caCertByteArray) - refresh() + uninstallCert(cert) dialog = 0 }, modifier = Modifier.fillMaxWidth(0.49F), @@ -1483,7 +1435,7 @@ fun CaCertScreen(onNavigateUp: () -> Unit) { } FilledTonalButton( onClick = { - exportCertLauncher.launch(sha256.substring(0..7) + ".0") + exportCertLauncher.launch(cert.hash.substring(0..7) + ".0") }, modifier = Modifier.fillMaxWidth(0.96F) ) { @@ -1491,34 +1443,58 @@ fun CaCertScreen(onNavigateUp: () -> Unit) { } } } - } - }, - confirmButton = { - TextButton({ - try { - if(dialog == 1) { - context.showOperationResultToast(Privilege.DPM.installCaCert(Privilege.DAR, caCertByteArray)) + }, + confirmButton = { + if (dialog == 1) { + TextButton({ + context.showOperationResultToast(installCert(cert)) + dialog = 0 + }) { + Text(stringResource(R.string.install)) } - if(dialog == 3) { - Privilege.DPM.uninstallAllUserCaCerts(Privilege.DAR) + } else { + TextButton({ + dialog = 0 + }) { + Text(stringResource(R.string.confirm)) } - refresh() - dialog = 0 - } catch(e: Exception) { - e.printStackTrace() - context.showOperationResultToast(false) } - }) { - Text(stringResource(if(dialog == 1) R.string.install else R.string.confirm)) - } - }, - dismissButton = { - if(dialog != 2) TextButton({ dialog = 0 }) { - Text(stringResource(R.string.cancel)) - } - }, - onDismissRequest = { dialog = 0 } - ) + }, + dismissButton = { + if (dialog == 1) { + TextButton({ + dialog = 0 + }) { + Text(stringResource(R.string.cancel)) + } + } + }, + onDismissRequest = {} + ) + } + if (dialog == 3) { + AlertDialog( + text = { + Text(stringResource(R.string.uninstall_all_user_ca_cert)) + }, + confirmButton = { + TextButton({ + uninstallAllCerts() + dialog = 0 + }) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton({ + dialog = 0 + }) { + Text(stringResource(R.string.cancel)) + } + }, + onDismissRequest = { dialog = 0 } + ) + } } } @@ -1602,21 +1578,19 @@ fun SecurityLoggingScreen(onNavigateUp: () -> Unit) { @Serializable object DisableAccountManagement @Composable -fun DisableAccountManagementScreen(onNavigateUp: () -> Unit) { +fun DisableAccountManagementScreen( + mdAccounts: StateFlow>, getMdAccounts: () -> Unit, + setMdAccount: (String, Boolean) -> Unit, onNavigateUp: () -> Unit +) { val focusMgr = LocalFocusManager.current + val list by mdAccounts.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { getMdAccounts() } MyScaffold(R.string.disable_account_management, onNavigateUp) { - val list = remember { mutableStateListOf() } - fun refreshList() { - list.clear() - Privilege.DPM.accountTypesWithManagementDisabled?.forEach { list += it } - } - LaunchedEffect(Unit) { refreshList() } + Column(modifier = Modifier.animateContentSize()) { - if(list.isEmpty()) Text(stringResource(R.string.none)) for(i in list) { ListItem(i) { - Privilege.DPM.setAccountManagementDisabled(Privilege.DAR, i, false) - refreshList() + setMdAccount(i, false) } } } @@ -1628,9 +1602,8 @@ fun DisableAccountManagementScreen(onNavigateUp: () -> Unit) { trailingIcon = { IconButton( onClick = { - Privilege.DPM.setAccountManagementDisabled(Privilege.DAR, inputText, true) + setMdAccount(inputText, true) inputText = "" - refreshList() }, enabled = inputText != "" ) { @@ -1648,58 +1621,53 @@ fun DisableAccountManagementScreen(onNavigateUp: () -> Unit) { } } +data class FrpPolicyInfo( + val supported: Boolean, + val usePolicy: Boolean, + val enabled: Boolean, + val accounts: List +) + @Serializable object FrpPolicy @RequiresApi(30) @Composable -fun FrpPolicyScreen(onNavigateUp: () -> Unit) { +fun FrpPolicyScreen( + getFrpPolicy: () -> FrpPolicyInfo, setFrpPolicy: (FrpPolicyInfo) -> Unit, + onNavigateUp: () -> Unit +) { val focusMgr = LocalFocusManager.current var usePolicy by remember { mutableStateOf(false) } var enabled by remember { mutableStateOf(false) } - var unsupported by remember { mutableStateOf(false) } + var supported by remember { mutableStateOf(false) } val accountList = remember { mutableStateListOf() } var inputAccount by remember { mutableStateOf("") } LaunchedEffect(Unit) { - var policy: FactoryResetProtectionPolicy? = null - try { - policy = Privilege.DPM.getFactoryResetProtectionPolicy(Privilege.DAR) - } catch(_: UnsupportedOperationException) { - unsupported = true - policy = null - } finally { - if(policy == null) { - usePolicy = false - } else { - usePolicy = true - enabled = policy.isFactoryResetProtectionEnabled - } + val info = getFrpPolicy() + supported = info.supported + if (info.supported) { + usePolicy = info.usePolicy + enabled = info.enabled + accountList.addAll(info.accounts) } } - MyScaffold(R.string.frp_policy, onNavigateUp) { - if(unsupported) { + MyScaffold(R.string.frp_policy, onNavigateUp, 0.dp) { + if (!supported) { Column( Modifier .fillMaxWidth() - .padding(vertical = 8.dp) + .padding(HorizontalPadding, 8.dp) .clip(RoundedCornerShape(8.dp)) .background(colorScheme.primaryContainer) ) { Text(stringResource(R.string.frp_not_supported), Modifier.padding(8.dp), color = colorScheme.onPrimaryContainer) } } else { - Row( - horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 6.dp, vertical = 8.dp) - ) { - Text(stringResource(R.string.use_policy), style = typography.titleLarge) - Switch(checked = usePolicy, onCheckedChange = { usePolicy = it }) - } + SwitchItem(R.string.use_policy, usePolicy, { usePolicy = it }) } - AnimatedVisibility(usePolicy) { - Column { - CheckBoxItem(R.string.enable_frp, enabled) { enabled = it } + if (usePolicy) { + FullWidthCheckBoxItem(R.string.enable_frp, enabled) { enabled = it } + Column(Modifier.padding(horizontal = HorizontalPadding)) { Text(stringResource(R.string.account_list_is)) Column(modifier = Modifier.animateContentSize()) { if(accountList.isEmpty()) Text(stringResource(R.string.none)) @@ -1727,61 +1695,65 @@ fun FrpPolicyScreen(onNavigateUp: () -> Unit) { 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)) + } } - if(!unsupported) Button( - onClick = { - focusMgr.clearFocus() - val policy = FactoryResetProtectionPolicy.Builder() - .setFactoryResetProtectionEnabled(enabled) - .setFactoryResetProtectionAccounts(accountList) - .build() - Privilege.DPM.setFactoryResetProtectionPolicy(Privilege.DAR, policy) - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - ) { - Text(stringResource(R.string.apply)) - } - Notes(R.string.info_frp_policy) + Notes(R.string.info_frp_policy, HorizontalPadding) } } @Serializable object WipeData @Composable -fun WipeDataScreen(onNavigateUp: () -> Unit) { +fun WipeDataScreen( + wipeData: (Boolean, Int, String) -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val privilege by Privilege.status.collectAsStateWithLifecycle() val focusMgr = LocalFocusManager.current - var flag by remember { mutableIntStateOf(0) } - var warning by remember { mutableStateOf(false) } - var wipeDevice by remember { mutableStateOf(false) } - var silent by remember { mutableStateOf(false) } + var flag by remember { mutableIntStateOf(WIPE_SILENTLY) } + var dialog by remember { mutableIntStateOf(0) } // 0: none, 1: wipe data, 2: wipe device var reason by remember { mutableStateOf("") } - MyScaffold(R.string.wipe_data, onNavigateUp) { - CheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { flag = flag xor WIPE_EXTERNAL_STORAGE } - if(VERSION.SDK_INT >= 22 && privilege.device) CheckBoxItem( - R.string.wipe_reset_protection_data, flag and WIPE_RESET_PROTECTION_DATA != 0) { flag = flag xor WIPE_RESET_PROTECTION_DATA } - if(VERSION.SDK_INT >= 28) CheckBoxItem(R.string.wipe_euicc, flag and WIPE_EUICC != 0) { flag = flag xor WIPE_EUICC } - if(VERSION.SDK_INT >= 29) CheckBoxItem(R.string.wipe_silently, silent) { silent = it } - AnimatedVisibility(!silent && VERSION.SDK_INT >= 28) { - OutlinedTextField( - value = reason, onValueChange = { reason = it }, - label = { Text(stringResource(R.string.reason)) }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 3.dp) - ) + MyScaffold(R.string.wipe_data, onNavigateUp, 0.dp) { + FullWidthCheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { + flag = flag xor WIPE_EXTERNAL_STORAGE } - Spacer(Modifier.padding(vertical = 5.dp)) - if(VERSION.SDK_INT < 34 || !userManager.isSystemUser) { + if(VERSION.SDK_INT >= 22 && privilege.device) FullWidthCheckBoxItem( + R.string.wipe_reset_protection_data, flag and WIPE_RESET_PROTECTION_DATA != 0) { + flag = flag xor WIPE_RESET_PROTECTION_DATA + } + if(VERSION.SDK_INT >= 28) FullWidthCheckBoxItem(R.string.wipe_euicc, + flag and WIPE_EUICC != 0) { + flag = flag xor WIPE_EUICC + } + if (VERSION.SDK_INT < 34 || !userManager.isSystemUser) { + if(VERSION.SDK_INT >= 29) CheckBoxItem(R.string.wipe_silently, flag and WIPE_SILENTLY != 0) { + flag = flag xor WIPE_SILENTLY + reason = "" + } + AnimatedVisibility(flag and WIPE_SILENTLY != 0 && VERSION.SDK_INT >= 28) { + OutlinedTextField( + value = reason, onValueChange = { reason = it }, + label = { Text(stringResource(R.string.reason)) }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) + } Button( onClick = { focusMgr.clearFocus() - wipeDevice = false - warning = true + dialog = 1 }, colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), modifier = Modifier.fillMaxWidth() @@ -1793,8 +1765,7 @@ fun WipeDataScreen(onNavigateUp: () -> Unit) { Button( onClick = { focusMgr.clearFocus() - wipeDevice = true - warning = true + dialog = 2 }, colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), modifier = Modifier.fillMaxWidth() @@ -1803,8 +1774,7 @@ fun WipeDataScreen(onNavigateUp: () -> Unit) { } } } - if(warning) { - LaunchedEffect(Unit) { silent = reason == "" } + if (dialog != 0) { AlertDialog( title = { Text(text = stringResource(R.string.warning), color = colorScheme.error) @@ -1818,7 +1788,7 @@ fun WipeDataScreen(onNavigateUp: () -> Unit) { color = colorScheme.error ) }, - onDismissRequest = { warning = false }, + onDismissRequest = { dialog = 0 }, confirmButton = { var timer by remember { mutableIntStateOf(6) } LaunchedEffect(Unit) { @@ -1830,16 +1800,7 @@ fun WipeDataScreen(onNavigateUp: () -> Unit) { val timerText = if(timer > 0) "(${timer}s)" else "" TextButton( onClick = { - if(silent && VERSION.SDK_INT >= 29) { flag = flag or WIPE_SILENTLY } - if(wipeDevice && VERSION.SDK_INT >= 34) { - Privilege.DPM.wipeDevice(flag) - } else { - if(VERSION.SDK_INT >= 28 && reason != "") { - Privilege.DPM.wipeData(flag, reason) - } else { - Privilege.DPM.wipeData(flag) - } - } + wipeData(dialog == 2, flag, reason) }, colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error), modifier = Modifier.animateContentSize(), @@ -1849,7 +1810,7 @@ fun WipeDataScreen(onNavigateUp: () -> Unit) { } }, dismissButton = { - TextButton(onClick = { warning = false }) { + TextButton(onClick = { dialog = 0 }) { Text(stringResource(R.string.cancel)) } } @@ -1857,35 +1818,53 @@ fun WipeDataScreen(onNavigateUp: () -> Unit) { } } +data class SystemUpdatePolicyInfo(val type: Int, val start: Int, val end: Int) +data class PendingSystemUpdateInfo(val exists: Boolean, val time: Long, val securityPatch: Boolean) + @Serializable object SetSystemUpdatePolicy @RequiresApi(23) @Composable -fun SystemUpdatePolicyScreen(onNavigateUp: () -> Unit) { +fun SystemUpdatePolicyScreen( + getPolicy: () -> SystemUpdatePolicyInfo, setPolicy: (SystemUpdatePolicyInfo) -> Unit, + getPendingUpdate: () -> PendingSystemUpdateInfo, onNavigateUp: () -> Unit +) { val context = LocalContext.current val focusMgr = LocalFocusManager.current + var policyType by remember { mutableIntStateOf(-1) } + var windowedPolicyStart by remember { mutableStateOf("") } + var windowedPolicyEnd by remember { mutableStateOf("") } + var pendingUpdate by remember { mutableStateOf(PendingSystemUpdateInfo(false, 0, false)) } + LaunchedEffect(Unit) { + val policy = getPolicy() + policyType = policy.type + if (policy.type == TYPE_INSTALL_WINDOWED) { + windowedPolicyStart = policy.start.toString() + windowedPolicyEnd = policy.end.toString() + } + if (VERSION.SDK_INT >= 26) pendingUpdate = getPendingUpdate() + } MyScaffold(R.string.system_update_policy, onNavigateUp, 0.dp) { - var selectedPolicy by remember { mutableStateOf(Privilege.DPM.systemUpdatePolicy?.policyType) } + FullWidthRadioButtonItem(R.string.none, policyType == -1) { policyType = -1 } FullWidthRadioButtonItem( R.string.system_update_policy_automatic, - selectedPolicy == TYPE_INSTALL_AUTOMATIC - ) { selectedPolicy = TYPE_INSTALL_AUTOMATIC } + policyType == TYPE_INSTALL_AUTOMATIC + ) { policyType = TYPE_INSTALL_AUTOMATIC } FullWidthRadioButtonItem( R.string.system_update_policy_install_windowed, - selectedPolicy == TYPE_INSTALL_WINDOWED - ) { selectedPolicy = TYPE_INSTALL_WINDOWED } + policyType == TYPE_INSTALL_WINDOWED + ) { policyType = TYPE_INSTALL_WINDOWED } FullWidthRadioButtonItem( R.string.system_update_policy_postpone, - selectedPolicy == TYPE_POSTPONE - ) { selectedPolicy = TYPE_POSTPONE } - FullWidthRadioButtonItem(R.string.none, selectedPolicy == null) { selectedPolicy = null } - var windowedPolicyStart by remember { mutableStateOf("") } - var windowedPolicyEnd by remember { mutableStateOf("") } - AnimatedVisibility(selectedPolicy == 2) { + policyType == TYPE_POSTPONE + ) { policyType = TYPE_POSTPONE } + AnimatedVisibility(policyType == TYPE_INSTALL_WINDOWED) { Column(Modifier.padding(horizontal = HorizontalPadding)) { - Row(Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), Arrangement.SpaceBetween) { + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), Arrangement.SpaceBetween + ) { OutlinedTextField( value = windowedPolicyStart, label = { Text(stringResource(R.string.start_time)) }, @@ -1896,47 +1875,41 @@ fun SystemUpdatePolicyScreen(onNavigateUp: () -> Unit) { ) OutlinedTextField( value = windowedPolicyEnd, - onValueChange = {windowedPolicyEnd = it }, + onValueChange = { windowedPolicyEnd = it }, label = { Text(stringResource(R.string.end_time)) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), - modifier = Modifier - .fillMaxWidth(0.96F) - .padding(bottom = 2.dp) + modifier = Modifier.fillMaxWidth(0.96F) ) } - Text(stringResource(R.string.minutes_in_one_day), color = colorScheme.onSurfaceVariant, style = typography.bodyMedium) + Text(stringResource(R.string.minutes_in_one_day), + color = colorScheme.onSurfaceVariant, style = typography.bodyMedium) } } Button( onClick = { - val policy = - when(selectedPolicy) { - TYPE_INSTALL_AUTOMATIC-> SystemUpdatePolicy.createAutomaticInstallPolicy() - TYPE_INSTALL_WINDOWED-> SystemUpdatePolicy.createWindowedInstallPolicy(windowedPolicyStart.toInt(), windowedPolicyEnd.toInt()) - TYPE_POSTPONE-> SystemUpdatePolicy.createPostponeInstallPolicy() - else -> null - } - Privilege.DPM.setSystemUpdatePolicy(Privilege.DAR, policy) + setPolicy(SystemUpdatePolicyInfo( + policyType, windowedPolicyStart.toIntOrNull() ?: 0, + windowedPolicyEnd.toIntOrNull() ?: 0 + )) context.showOperationResultToast(true) }, modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp, horizontal = HorizontalPadding) + .padding(vertical = 4.dp, horizontal = HorizontalPadding), + enabled = policyType != TYPE_INSTALL_WINDOWED || + listOf(windowedPolicyStart, windowedPolicyEnd).map { it.toIntOrNull() } + .all { it != null && it <= 1440 } ) { Text(stringResource(R.string.apply)) } - if(VERSION.SDK_INT >= 26) { - val sysUpdateInfo = Privilege.DPM.getPendingSystemUpdate(Privilege.DAR) + if (VERSION.SDK_INT >= 26) { Column(Modifier.padding(HorizontalPadding)) { - if(sysUpdateInfo != null) { - Text(text = stringResource(R.string.update_received_time, Date(sysUpdateInfo.receivedTime))) - val securityPatchStateText = when(sysUpdateInfo.securityPatchState) { - SystemUpdateInfo.SECURITY_PATCH_STATE_FALSE -> R.string.no - SystemUpdateInfo.SECURITY_PATCH_STATE_TRUE -> R.string.yes - else -> R.string.unknown - } - Text(text = stringResource(R.string.is_security_patch, stringResource(securityPatchStateText))) + if (pendingUpdate.exists) { + Text(stringResource(R.string.update_received_time, + parseTimestamp(pendingUpdate.time))) + Text(stringResource(R.string.is_security_patch, + stringResource(pendingUpdate.securityPatch.yesOrNo))) } else { Text(text = stringResource(R.string.no_system_update)) } @@ -1949,23 +1922,11 @@ fun SystemUpdatePolicyScreen(onNavigateUp: () -> Unit) { @SuppressLint("NewApi") @Composable -fun InstallSystemUpdateScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - val callback = object: InstallSystemUpdateCallback() { - override fun onInstallUpdateError(errorCode: Int, errorMessage: String) { - super.onInstallUpdateError(errorCode, errorMessage) - val errDetail = when(errorCode) { - UPDATE_ERROR_BATTERY_LOW -> R.string.battery_low - UPDATE_ERROR_UPDATE_FILE_INVALID -> R.string.update_file_invalid - UPDATE_ERROR_INCORRECT_OS_VERSION -> R.string.incorrect_os_ver - UPDATE_ERROR_FILE_NOT_FOUND -> R.string.file_not_exist - else -> R.string.unknown - } - val errMsg = context.getString(R.string.install_system_update_failed) + context.getString(errDetail) - context.popToast(errMsg) - } - } +fun InstallSystemUpdateScreen( + installSystemUpdate: (Uri, (String) -> Unit) -> Unit, onNavigateUp: () -> Unit +) { var uri by remember { mutableStateOf(null) } + var installing by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf(null) } val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri = it } MyScaffold(R.string.install_system_update, onNavigateUp) { @@ -1979,21 +1940,17 @@ fun InstallSystemUpdateScreen(onNavigateUp: () -> Unit) { ) { Text(stringResource(R.string.select_ota_package)) } - AnimatedVisibility(uri != null) { - Button( - onClick = { - val executor = Executors.newCachedThreadPool() - try { - Privilege.DPM.installSystemUpdate(Privilege.DAR, uri!!, executor, callback) - context.popToast(R.string.start_install_system_update) - } catch(e: Exception) { - errorMessage = e.message - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.install_system_update)) - } + Button( + onClick = { + installing = true + installSystemUpdate(uri!!) { message -> + errorMessage = message + } + }, + modifier = Modifier.fillMaxWidth(), + enabled = uri != null && !installing + ) { + Text(stringResource(R.string.install_system_update)) } Spacer(Modifier.padding(vertical = 10.dp)) Notes(R.string.auto_reboot_after_install_succeed) diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index 51447c1..e6ce051 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -233,6 +233,22 @@ fun SwitchItem( } } +@Composable +fun SwitchItem( + title: Int, state: Boolean, onCheckedChange: (Boolean) -> Unit, icon: Int? = null +) { + Row( + Modifier.fillMaxWidth().padding(25.dp, 5.dp, 15.dp, 5.dp), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) { + if (icon != null) Icon(painterResource(icon), null, Modifier.padding(end = 20.dp)) + Text(stringResource(title), style = typography.titleLarge) + } + Switch(state, onCheckedChange, Modifier.padding(start = 10.dp)) + } +} + @Composable fun InfoItem(title: Int, text: Int, withInfo: Boolean = false, onClick: () -> Unit = {}) = InfoItem(title, stringResource(text), withInfo, onClick) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6e2ede2..3f7533d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -129,7 +129,7 @@ Служба резервного копирования Отключить обмен контактами по Bluetooth Режим общих критериев - Отключить USB-сигнал + Enable USB signal Блокировка экрана (Keyguard) Заблокировать сейчас Удалить ключ шифрования учетных данных diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index cff76b6..c95bc0d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -138,7 +138,7 @@ Yedekleme Servisi Bluetooth Kişi Paylaşımını Devre Dışı Bırak Ortak Kriterler Modu - USB Sinyalini Devre Dışı Bırak + Enable USB signal Kilit Ekranı Ekranı Şimdi Kilitle Kilit Ekranı diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index abd9192..ce5eda5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -135,7 +135,7 @@ 备份服务 禁止蓝牙分享联系人 通用标准模式 - 禁用USB信号 + 启用USB信号 锁屏 立即锁屏 锁屏 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b963b9..da76e17 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -144,7 +144,7 @@ Backup service Disable bluetooth contact sharing Common criteria mode - Disable USB signal + Enable USB signal Keyguard Lock screen now Lock screen