Files
OwnDroid/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt
BinTianqi b734522171 Fix crash caused by DelegatedScope
Use LargeTopAppBar in most screens
Optimize Package Chooser
Install existing app
2025-02-23 11:50:24 +08:00

1891 lines
84 KiB
Kotlin

package com.bintianqi.owndroid.dpm
import android.annotation.SuppressLint
import android.app.ActivityOptions
import android.app.AlertDialog
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.LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK
import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS
import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME
import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_KEYGUARD
import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS
import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW
import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_SYSTEM_INFO
import android.app.admin.DevicePolicyManager.MTE_DISABLED
import android.app.admin.DevicePolicyManager.MTE_ENABLED
import android.app.admin.DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY
import android.app.admin.DevicePolicyManager.NEARBY_STREAMING_DISABLED
import android.app.admin.DevicePolicyManager.NEARBY_STREAMING_ENABLED
import android.app.admin.DevicePolicyManager.NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY
import android.app.admin.DevicePolicyManager.NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY
import android.app.admin.DevicePolicyManager.PERMISSION_POLICY_AUTO_DENY
import android.app.admin.DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT
import android.app.admin.DevicePolicyManager.PERMISSION_POLICY_PROMPT
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
import android.os.UserManager
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Add
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.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.bintianqi.owndroid.ChoosePackageContract
import com.bintianqi.owndroid.NotificationUtils
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SharedPrefs
import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.humanReadableDate
import com.bintianqi.owndroid.parseDate
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem
import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem
import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.InfoCard
import com.bintianqi.owndroid.ui.ListItem
import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.MySmallTitleScaffold
import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.RadioButtonItem
import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.uriToStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
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.collections.addAll
import kotlin.math.roundToLong
@Serializable object SystemManager
@Composable
fun SystemManagerScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val sp = SharedPrefs(context)
val dhizuku = sp.dhizuku
val deviceOwner = context.isDeviceOwner
val profileOwner = context.isProfileOwner
var dialog by remember { mutableIntStateOf(0) }
MyScaffold(R.string.system, 0.dp, onNavigateUp) {
if(deviceOwner || profileOwner) {
FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { onNavigate(SystemOptions) }
}
FunctionItem(R.string.keyguard, icon = R.drawable.screen_lock_portrait_fill0) { onNavigate(Keyguard) }
if(VERSION.SDK_INT >= 24 && deviceOwner && !dhizuku)
FunctionItem(R.string.hardware_monitor, icon = R.drawable.memory_fill0) { onNavigate(HardwareMonitor) }
if(VERSION.SDK_INT >= 24 && deviceOwner) {
FunctionItem(R.string.reboot, icon = R.drawable.restart_alt_fill0) { dialog = 1 }
}
if(deviceOwner && VERSION.SDK_INT >= 24 && (VERSION.SDK_INT < 28 || dpm.isAffiliatedUser)) {
FunctionItem(R.string.bug_report, icon = R.drawable.bug_report_fill0) { dialog = 2 }
}
if(VERSION.SDK_INT >= 28 && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(R.string.change_time, icon = R.drawable.schedule_fill0) { onNavigate(ChangeTime) }
FunctionItem(R.string.change_timezone, icon = R.drawable.schedule_fill0) { onNavigate(ChangeTimeZone) }
}
/*if(VERSION.SDK_INT >= 28 && (deviceOwner || profileOwner))
FunctionItem(R.string.key_pairs, icon = R.drawable.key_vertical_fill0) { navCtrl.navigate("KeyPairs") }*/
if(VERSION.SDK_INT >= 35 && (deviceOwner || (profileOwner && dpm.isAffiliatedUser)))
FunctionItem(R.string.content_protection_policy, icon = R.drawable.search_fill0) { onNavigate(ContentProtectionPolicy) }
if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner)) {
FunctionItem(R.string.permission_policy, icon = R.drawable.key_fill0) { onNavigate(PermissionPolicy) }
}
if(VERSION.SDK_INT >= 34 && deviceOwner) {
FunctionItem(R.string.mte_policy, icon = R.drawable.memory_fill0) { onNavigate(MtePolicy) }
}
if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) {
FunctionItem(R.string.nearby_streaming_policy, icon = R.drawable.share_fill0) { onNavigate(NearbyStreamingPolicy) }
}
if(VERSION.SDK_INT >= 28 && deviceOwner) {
FunctionItem(R.string.lock_task_mode, icon = R.drawable.lock_fill0) { onNavigate(LockTaskMode) }
}
if(deviceOwner || profileOwner) {
FunctionItem(R.string.ca_cert, icon = R.drawable.license_fill0) { onNavigate(CaCert) }
}
if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(R.string.security_logging, icon = R.drawable.description_fill0) { onNavigate(SecurityLogging) }
}
if(deviceOwner || profileOwner) {
FunctionItem(R.string.disable_account_management, icon = R.drawable.account_circle_fill0) { onNavigate(DisableAccountManagement) }
}
if(VERSION.SDK_INT >= 23 && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(R.string.system_update_policy, icon = R.drawable.system_update_fill0) { onNavigate(SetSystemUpdatePolicy) }
}
if(VERSION.SDK_INT >= 29 && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(R.string.install_system_update, icon = R.drawable.system_update_fill0) { onNavigate(InstallSystemUpdate) }
}
if(VERSION.SDK_INT >= 30 && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(R.string.frp_policy, icon = R.drawable.device_reset_fill0) { onNavigate(FrpPolicy) }
}
if(sp.displayDangerousFeatures && context.isDeviceAdmin && !(VERSION.SDK_INT >= 24 && profileOwner && dpm.isManagedProfile(receiver))) {
FunctionItem(R.string.wipe_data, icon = R.drawable.device_reset_fill0) { onNavigate(WipeData) }
}
}
if(dialog != 0 &&VERSION.SDK_INT >= 24) AlertDialog(
onDismissRequest = { dialog = 0 },
title = { Text(stringResource(if(dialog == 1) R.string.reboot else R.string.bug_report)) },
text = { Text(stringResource(if(dialog == 1) R.string.info_reboot else R.string.confirm_bug_report)) },
dismissButton = {
TextButton(onClick = { dialog = 0 }) {
Text(stringResource(R.string.cancel))
}
},
confirmButton = {
TextButton(
onClick = {
if(dialog == 1) {
dpm.reboot(receiver)
} else {
context.showOperationResultToast(dpm.requestBugreport(receiver))
}
dialog = 0
}
) {
Text(stringResource(R.string.confirm))
}
},
modifier = Modifier.fillMaxWidth()
)
}
@Serializable object SystemOptions
@Composable
fun SystemOptionsScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val deviceOwner = context.isDeviceOwner
val profileOwner = context.isProfileOwner
val um = context.getSystemService(Context.USER_SERVICE) as UserManager
var dialog by remember { mutableIntStateOf(0) }
MyScaffold(R.string.options, 0.dp, onNavigateUp) {
if(deviceOwner || profileOwner) {
SwitchItem(R.string.disable_cam, icon = R.drawable.photo_camera_fill0,
getState = { dpm.getCameraDisabled(null) }, onCheckedChange = { dpm.setCameraDisabled(receiver,it) }
)
}
if(deviceOwner || profileOwner) {
SwitchItem(R.string.disable_screen_capture, icon = R.drawable.screenshot_fill0,
getState = { dpm.getScreenCaptureDisabled(null) }, onCheckedChange = { dpm.setScreenCaptureDisabled(receiver,it) }
)
}
if(VERSION.SDK_INT >= 34 && (deviceOwner || (profileOwner && dpm.isAffiliatedUser))) {
SwitchItem(R.string.disable_status_bar, icon = R.drawable.notifications_fill0,
getState = { dpm.isStatusBarDisabled}, onCheckedChange = { dpm.setStatusBarDisabled(receiver,it) }
)
}
if(deviceOwner || (VERSION.SDK_INT >= 23 && profileOwner && um.isSystemUser) || dpm.isOrgProfile(receiver)) {
if(VERSION.SDK_INT >= 30) {
SwitchItem(R.string.auto_time, icon = R.drawable.schedule_fill0,
getState = { dpm.getAutoTimeEnabled(receiver) }, onCheckedChange = { dpm.setAutoTimeEnabled(receiver,it) }
)
SwitchItem(R.string.auto_timezone, icon = R.drawable.globe_fill0,
getState = { dpm.getAutoTimeZoneEnabled(receiver) }, onCheckedChange = { dpm.setAutoTimeZoneEnabled(receiver,it) }
)
} else {
SwitchItem(R.string.require_auto_time, icon = R.drawable.schedule_fill0,
getState = { dpm.autoTimeRequired }, onCheckedChange = { dpm.setAutoTimeRequired(receiver,it) }, padding = false)
}
}
if(deviceOwner || profileOwner) {
SwitchItem(R.string.master_mute, icon = R.drawable.volume_up_fill0,
getState = { dpm.isMasterVolumeMuted(receiver) }, onCheckedChange = { dpm.setMasterVolumeMuted(receiver,it) }
)
}
if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) {
SwitchItem(R.string.backup_service, icon = R.drawable.backup_fill0,
getState = { dpm.isBackupServiceEnabled(receiver) }, onCheckedChange = { dpm.setBackupServiceEnabled(receiver,it) },
onClickBlank = { dialog = 1 }
)
}
if(VERSION.SDK_INT >= 24 && profileOwner && dpm.isManagedProfile(receiver)) {
SwitchItem(R.string.disable_bt_contact_share, icon = R.drawable.account_circle_fill0,
getState = { dpm.getBluetoothContactSharingDisabled(receiver) },
onCheckedChange = { dpm.setBluetoothContactSharingDisabled(receiver,it) }
)
}
if(VERSION.SDK_INT >= 30 && deviceOwner) {
SwitchItem(R.string.common_criteria_mode , icon =R.drawable.security_fill0,
getState = { dpm.isCommonCriteriaModeEnabled(receiver) }, onCheckedChange = { dpm.setCommonCriteriaModeEnabled(receiver,it) },
onClickBlank = { dialog = 2 }
)
}
if(VERSION.SDK_INT >= 31 && (deviceOwner || dpm.isOrgProfile(receiver)) && dpm.canUsbDataSignalingBeDisabled()) {
SwitchItem(
R.string.disable_usb_signal, icon = R.drawable.usb_fill0, getState = { !dpm.isUsbDataSignalingEnabled },
onCheckedChange = { dpm.isUsbDataSignalingEnabled = !it },
)
}
}
if(dialog != 0) AlertDialog(
text = {
Text(stringResource(
when(dialog) {
1 -> R.string.info_backup_service
2 -> R.string.info_common_criteria_mode
else -> R.string.options
}
))
},
confirmButton = {
TextButton(onClick = { dialog = 0 }) { Text(stringResource(R.string.confirm)) }
},
onDismissRequest = { dialog = 0 }
)
}
@Serializable object Keyguard
@Composable
fun KeyguardScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val deviceOwner = context.isDeviceOwner
val profileOwner = context.isProfileOwner
MyScaffold(R.string.keyguard, 8.dp, onNavigateUp) {
if(VERSION.SDK_INT >= 23) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = { context.showOperationResultToast(dpm.setKeyguardDisabled(receiver, true)) },
enabled = deviceOwner || (VERSION.SDK_INT >= 28 && profileOwner && dpm.isAffiliatedUser),
modifier = Modifier.fillMaxWidth(0.49F)
) {
Text(stringResource(R.string.disable))
}
Button(
onClick = { context.showOperationResultToast(dpm.setKeyguardDisabled(receiver, false)) },
enabled = deviceOwner || (VERSION.SDK_INT >= 28 && profileOwner && dpm.isAffiliatedUser),
modifier = Modifier.fillMaxWidth(0.96F)
) {
Text(stringResource(R.string.enable))
}
}
InfoCard(R.string.info_disable_keyguard)
Spacer(Modifier.padding(vertical = 12.dp))
}
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 && profileOwner && dpm.isManagedProfile(receiver)) {
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))
}
Button(
onClick = {
if(VERSION.SDK_INT >= 26) dpm.lockNow(flag) else dpm.lockNow()
},
enabled = context.isDeviceAdmin,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.lock_now))
}
if(VERSION.SDK_INT >= 26 && profileOwner && dpm.isManagedProfile(receiver)) {
InfoCard(R.string.info_evict_credential_encryption_key)
}
}
}
@Serializable object HardwareMonitor
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(24)
@Composable
fun HardwareMonitorScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val hpm = context.getSystemService(HardwarePropertiesManager::class.java)
var refreshInterval by remember { mutableFloatStateOf(1F) }
val refreshIntervalMs = (refreshInterval * 1000).roundToLong()
val temperatures = remember { mutableStateMapOf<Int, List<Float>>() }
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<Pair<Long, Long>>() }
val fanSpeeds = remember { mutableStateListOf<Float>() }
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)
}
}
MySmallTitleScaffold(R.string.hardware_monitor, 8.dp, onNavigateUp) {
Text(stringResource(R.string.refresh_interval), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp))
Slider(refreshInterval, { refreshInterval = 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))
if(tempMapItem.value.isEmpty()) {
Text(stringResource(R.string.unsupported))
} else {
tempMapItem.value.forEachIndexed { index, temp ->
Row(modifier = Modifier.padding(vertical = 4.dp)) {
Text(index.toString(), style = typography.titleMedium, modifier = Modifier.padding(start = 8.dp, end = 12.dp))
Text(if(temp == HardwarePropertiesManager.UNDEFINED_TEMPERATURE) stringResource(R.string.undefined) else temp.toString())
}
}
}
Spacer(Modifier.padding(vertical = 10.dp))
}
Text(stringResource(R.string.cpu_usages), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp))
if(cpuUsages.isEmpty()) {
Text(stringResource(R.string.unsupported))
} else {
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 {
Text(stringResource(R.string.active) + ": " + usage.first + "ms")
Text(stringResource(R.string.total) + ": " + usage.second + "ms")
}
}
}
}
Spacer(Modifier.padding(vertical = 10.dp))
Text(stringResource(R.string.fan_speeds), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp))
if(fanSpeeds.isEmpty()) {
Text(stringResource(R.string.unsupported))
} else {
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")
}
}
}
}
}
@Serializable object ChangeTime
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(28)
@Composable
fun ChangeTimeScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
val coroutine = rememberCoroutineScope()
val pagerState = rememberPagerState { 2 }
var manualInput by remember { mutableStateOf(false) }
var inputTime by remember { mutableStateOf("")}
var picker by remember { mutableIntStateOf(0) } //0:None, 1:DatePicker, 2:TimePicker
val datePickerState = rememberDatePickerState()
val timePickerState = rememberTimePickerState()
val dateInteractionSource = remember { MutableInteractionSource() }
val timeInteractionSource = remember { MutableInteractionSource() }
if(dateInteractionSource.collectIsPressedAsState().value) picker = 1
if(timeInteractionSource.collectIsPressedAsState().value) picker = 2
val isInputLegal = (manualInput && (try { inputTime.toLong() } catch(_: Exception) { -1 }) >= 0) ||
(!manualInput && datePickerState.selectedDateMillis != null)
MyScaffold(R.string.change_time, 8.dp, onNavigateUp) {
SingleChoiceSegmentedButtonRow(
modifier = Modifier.fillMaxWidth().padding(top = 4.dp)
) {
SegmentedButton(
selected = !manualInput, shape = SegmentedButtonDefaults.itemShape(0, 2),
onClick = {
manualInput = false
coroutine.launch {
pagerState.animateScrollToPage(0)
}
}
) {
Text(stringResource(R.string.selector))
}
SegmentedButton(
selected = manualInput, shape = SegmentedButtonDefaults.itemShape(1, 2),
onClick = {
manualInput = true
coroutine.launch {
pagerState.animateScrollToPage(1)
}
}
) {
Text(stringResource(R.string.manually_input))
}
}
HorizontalPager(
state = pagerState, modifier = Modifier.height(140.dp).padding(top = 4.dp),
verticalAlignment = Alignment.Top
) { page ->
if(page == 0) Column {
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()
)
}
if(page == 1) OutlinedTextField(
value = inputTime,
label = { Text(stringResource(R.string.time_unit_ms)) },
onValueChange = { inputTime = it },
supportingText = { Text(stringResource(R.string.info_change_time)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }),
modifier = Modifier.fillMaxWidth()
)
}
Button(
onClick = {
val timeMillis = if(manualInput) inputTime.toLong()
else datePickerState.selectedDateMillis!! + timePickerState.hour * 3600000 + timePickerState.minute * 60000
context.showOperationResultToast(dpm.setTime(receiver, timeMillis))
},
modifier = Modifier.fillMaxWidth(),
enabled = isInputLegal
) {
Text(stringResource(R.string.apply))
}
}
if(picker == 1) DatePickerDialog(
confirmButton = {
TextButton(onClick = { picker = 0; focusMgr.clearFocus() } ) {
Text(stringResource(R.string.confirm))
}
},
onDismissRequest = { picker = 0; focusMgr.clearFocus() }
) {
DatePicker(datePickerState)
}
if(picker == 2) AlertDialog(
text = { TimePicker(timePickerState) },
confirmButton = {
TextButton(onClick = { picker = 0; focusMgr.clearFocus() } ) {
Text(stringResource(R.string.confirm))
}
},
onDismissRequest = { picker = 0; focusMgr.clearFocus() }
)
}
@Serializable object ChangeTimeZone
@RequiresApi(28)
@Composable
fun ChangeTimeZoneScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val focusMgr = LocalFocusManager.current
val receiver = context.getReceiver()
var inputTimezone by remember { mutableStateOf("") }
var dialog by remember { mutableStateOf(false) }
MyScaffold(R.string.change_timezone, 8.dp, onNavigateUp) {
OutlinedTextField(
value = inputTimezone,
label = { Text(stringResource(R.string.timezone_id)) },
onValueChange = { inputTimezone = it },
trailingIcon = {
IconButton(onClick = { dialog = true }) {
Icon(imageVector = Icons.AutoMirrored.Default.List, contentDescription = null)
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
context.showOperationResultToast(dpm.setTimeZone(receiver, inputTimezone))
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.apply))
}
Spacer(Modifier.padding(vertical = 10.dp))
InfoCard(R.string.disable_auto_time_zone_before_set)
}
if(dialog) AlertDialog(
text = {
LazyColumn {
items(TimeZone.getAvailableIDs()) {
Text(
text = it,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 1.dp)
.clip(RoundedCornerShape(15))
.clickable {
inputTimezone = it
dialog = false
}
.padding(start = 6.dp, top = 10.dp, bottom = 10.dp)
)
}
}
},
confirmButton = {
TextButton(onClick = { dialog = false }) {
Text(stringResource(R.string.cancel))
}
},
onDismissRequest = { dialog = false }
)
}
/*@RequiresApi(28)
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun KeyPairs(navCtrl: NavHostController) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var alias by remember { mutableStateOf("") }
var purpose by remember { mutableIntStateOf(0) }
//var keySpecType by remember { mutableIntStateOf() }
var ecStdName by remember { mutableStateOf("") }
var rsaKeySize by remember { mutableStateOf("") }
var rsaExponent by remember { mutableStateOf("") }
var algorithm by remember { mutableStateOf("") }
var idAttestationFlags by remember { mutableIntStateOf(0) }
MyScaffold(R.string.key_pairs, 8.dp, navCtrl) {
OutlinedTextField(
value = alias, onValueChange = { alias = it }, label = { Text(stringResource(R.string.alias)) },
modifier = Modifier.fillMaxWidth()
)
Text(stringResource(R.string.algorithm), style = typography.titleLarge)
SingleChoiceSegmentedButtonRow {
*//*SegmentedButton(
algorithm == "DH", { algorithm = "DH" },
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 4)
) {
Text("DH")
}
SegmentedButton(
algorithm == "DSA", { algorithm = "DSA" },
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 4)
) {
Text("DSA")
}*//*
SegmentedButton(
algorithm == "EC", { algorithm = "EC" },
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2)
) {
Text("EC")
}
SegmentedButton(
algorithm == "RSA", { algorithm = "RSA" },
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2)
) {
Text("RSA")
}
}
AnimatedVisibility(algorithm != "") {
Text(stringResource(R.string.key_specification), style = typography.titleLarge)
}
AnimatedVisibility(algorithm == "EC") {
OutlinedTextField(
value = ecStdName, onValueChange = { ecStdName = it }, label = { Text(stringResource(R.string.standard_name)) },
modifier = Modifier.fillMaxWidth()
)
}
AnimatedVisibility(algorithm == "RSA") {
Column {
OutlinedTextField(
value = rsaKeySize, onValueChange = { rsaKeySize = it }, label = { Text(stringResource(R.string.key_size)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = rsaExponent, onValueChange = { rsaExponent = it }, label = { Text(stringResource(R.string.exponent)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
}
}
Text(stringResource(R.string.key_purpose), style = typography.titleLarge)
FlowRow {
if(VERSION.SDK_INT >= 23) {
InputChip(
purpose and KeyProperties.PURPOSE_ENCRYPT != 0,
{ purpose = purpose xor KeyProperties.PURPOSE_ENCRYPT },
{ Text(stringResource(R.string.kp_encrypt)) },
Modifier.padding(horizontal = 4.dp)
)
InputChip(
purpose and KeyProperties.PURPOSE_DECRYPT != 0,
{ purpose = purpose xor KeyProperties.PURPOSE_DECRYPT },
{ Text(stringResource(R.string.kp_decrypt)) },
Modifier.padding(horizontal = 4.dp)
)
InputChip(
purpose and KeyProperties.PURPOSE_SIGN != 0,
{ purpose = purpose xor KeyProperties.PURPOSE_SIGN },
{ Text(stringResource(R.string.kp_sign)) },
Modifier.padding(horizontal = 4.dp)
)
InputChip(
purpose and KeyProperties.PURPOSE_VERIFY != 0,
{ purpose = purpose xor KeyProperties.PURPOSE_VERIFY },
{ Text(stringResource(R.string.kp_verify)) },
Modifier.padding(horizontal = 4.dp)
)
}
if(VERSION.SDK_INT >= 28) InputChip(
purpose and KeyProperties.PURPOSE_WRAP_KEY != 0,
{ purpose = purpose xor KeyProperties.PURPOSE_WRAP_KEY },
{ Text(stringResource(R.string.kp_wrap)) },
Modifier.padding(horizontal = 4.dp)
)
if(VERSION.SDK_INT >= 31) {
InputChip(
purpose and KeyProperties.PURPOSE_AGREE_KEY != 0,
{ purpose = purpose xor KeyProperties.PURPOSE_AGREE_KEY },
{ Text(stringResource(R.string.kp_agree)) },
Modifier.padding(horizontal = 4.dp)
)
InputChip(
purpose and KeyProperties.PURPOSE_ATTEST_KEY != 0,
{ purpose = purpose xor KeyProperties.PURPOSE_ATTEST_KEY },
{ Text(stringResource(R.string.kp_attest)) },
Modifier.padding(horizontal = 4.dp)
)
}
}
Text(stringResource(R.string.attestation_record_identifiers), style = typography.titleLarge)
FlowRow {
InputChip(
idAttestationFlags and DevicePolicyManager.ID_TYPE_BASE_INFO != 0,
{ idAttestationFlags = idAttestationFlags xor DevicePolicyManager.ID_TYPE_BASE_INFO },
{ Text(stringResource(R.string.base_info)) },
Modifier.padding(horizontal = 4.dp)
)
InputChip(
idAttestationFlags and DevicePolicyManager.ID_TYPE_SERIAL != 0,
{ idAttestationFlags = idAttestationFlags xor DevicePolicyManager.ID_TYPE_SERIAL },
{ Text(stringResource(R.string.serial_number)) },
Modifier.padding(horizontal = 4.dp)
)
InputChip(
idAttestationFlags and DevicePolicyManager.ID_TYPE_IMEI != 0,
{ idAttestationFlags = idAttestationFlags xor DevicePolicyManager.ID_TYPE_IMEI },
{ Text("IMEI") },
Modifier.padding(horizontal = 4.dp)
)
InputChip(
idAttestationFlags and DevicePolicyManager.ID_TYPE_MEID != 0,
{ idAttestationFlags = idAttestationFlags xor DevicePolicyManager.ID_TYPE_MEID },
{ Text("MEID") },
Modifier.padding(horizontal = 4.dp)
)
if(VERSION.SDK_INT >= 30) InputChip(
idAttestationFlags and DevicePolicyManager.ID_TYPE_INDIVIDUAL_ATTESTATION != 0,
{ idAttestationFlags = idAttestationFlags xor DevicePolicyManager.ID_TYPE_INDIVIDUAL_ATTESTATION },
{ Text(stringResource(R.string.individual_certificate)) },
Modifier.padding(horizontal = 4.dp)
)
}
Button(
onClick = {
try {
val aps = if(algorithm == "EC") ECGenParameterSpec(ecStdName)
else RSAKeyGenParameterSpec(rsaKeySize.toInt(), rsaExponent.toBigInteger())
val keySpec = KeyGenParameterSpec.Builder(alias, purpose).run {
setAlgorithmParameterSpec(aps)
this.setAttestationChallenge()
build()
}
dpm.generateKeyPair(receiver, algorithm, keySpec, idAttestationFlags)
} catch(e: Exception) {
AlertDialog.Builder(context)
.setTitle(R.string.error)
.setMessage(e.message ?: "")
.setPositiveButton(R.string.confirm) { dialog, _ -> dialog.dismiss() }
.show()
}
},
modifier = Modifier.fillMaxWidth(),
enabled = alias != "" && purpose != 0 &&
((algorithm == "EC") || (algorithm == "RSA" && rsaKeySize.all { it.isDigit() } && rsaExponent.all { it.isDigit() }))
) {
Text(stringResource(R.string.generate))
}
}
}*/
@Serializable object ContentProtectionPolicy
@RequiresApi(35)
@Composable
fun ContentProtectionPolicyScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var policy by remember { mutableIntStateOf(DevicePolicyManager.CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY) }
fun refresh() { policy = dpm.getContentProtectionPolicy(receiver) }
LaunchedEffect(Unit) { refresh() }
MyScaffold(R.string.content_protection_policy, 0.dp, onNavigateUp) {
mapOf(
DevicePolicyManager.CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY to R.string.not_controlled_by_policy,
DevicePolicyManager.CONTENT_PROTECTION_ENABLED to R.string.enabled,
DevicePolicyManager.CONTENT_PROTECTION_DISABLED to R.string.disabled
).forEach { (policyId, string) ->
FullWidthRadioButtonItem(string, policy == policyId) { policy = policyId }
}
Button(
onClick = {
dpm.setContentProtectionPolicy(receiver, policy)
refresh()
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 8.dp)
) {
Text(stringResource(R.string.apply))
}
InfoCard(R.string.info_content_protection_policy, 8.dp)
}
}
@Serializable object PermissionPolicy
@RequiresApi(23)
@Composable
fun PermissionPolicyScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var selectedPolicy by remember { mutableIntStateOf(dpm.getPermissionPolicy(receiver)) }
MyScaffold(R.string.permission_policy, 0.dp, onNavigateUp) {
FullWidthRadioButtonItem(R.string.default_stringres, selectedPolicy == PERMISSION_POLICY_PROMPT) {
selectedPolicy = PERMISSION_POLICY_PROMPT
}
FullWidthRadioButtonItem(R.string.auto_grant, selectedPolicy == PERMISSION_POLICY_AUTO_GRANT) {
selectedPolicy = PERMISSION_POLICY_AUTO_GRANT
}
FullWidthRadioButtonItem(R.string.auto_deny, selectedPolicy == PERMISSION_POLICY_AUTO_DENY) {
selectedPolicy = PERMISSION_POLICY_AUTO_DENY
}
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
dpm.setPermissionPolicy(receiver,selectedPolicy)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)
) {
Text(stringResource(R.string.apply))
}
InfoCard(R.string.info_permission_policy, 8.dp)
}
}
@Serializable object MtePolicy
@RequiresApi(34)
@Composable
fun MtePolicyScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
var selectedMtePolicy by remember { mutableIntStateOf(dpm.mtePolicy) }
MyScaffold(R.string.mte_policy, 0.dp, onNavigateUp) {
FullWidthRadioButtonItem(R.string.decide_by_user, selectedMtePolicy == MTE_NOT_CONTROLLED_BY_POLICY) {
selectedMtePolicy = 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 }
Button(
onClick = {
try {
dpm.mtePolicy = selectedMtePolicy
context.showOperationResultToast(true)
} catch(_: java.lang.UnsupportedOperationException) {
Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show()
}
selectedMtePolicy = dpm.mtePolicy
},
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 8.dp)
) {
Text(stringResource(R.string.apply))
}
InfoCard(R.string.info_mte_policy, 8.dp)
}
}
@Serializable object NearbyStreamingPolicy
@RequiresApi(31)
@Composable
fun NearbyStreamingPolicyScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
var appPolicy by remember { mutableIntStateOf(dpm.nearbyAppStreamingPolicy) }
MyScaffold(R.string.nearby_streaming_policy, 0.dp, onNavigateUp) {
Text(
stringResource(R.string.nearby_app_streaming),
Modifier.padding(start = 8.dp, top = 10.dp, bottom = 4.dp), style = typography.titleLarge
)
FullWidthRadioButtonItem(
R.string.decide_by_user,
appPolicy == NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY
) { appPolicy = NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY }
FullWidthRadioButtonItem(R.string.enabled, appPolicy == NEARBY_STREAMING_ENABLED) { appPolicy = NEARBY_STREAMING_ENABLED }
FullWidthRadioButtonItem(R.string.disabled, appPolicy == NEARBY_STREAMING_DISABLED) { appPolicy = NEARBY_STREAMING_DISABLED }
FullWidthRadioButtonItem(
R.string.enable_if_secure_enough,
appPolicy == NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY
) { appPolicy = NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY }
Button(
onClick = {
dpm.nearbyAppStreamingPolicy = appPolicy
appPolicy = dpm.nearbyAppStreamingPolicy
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 8.dp)
) {
Text(stringResource(R.string.apply))
}
InfoCard(R.string.info_nearby_app_streaming_policy, 8.dp)
var notificationPolicy by remember { mutableIntStateOf(dpm.nearbyNotificationStreamingPolicy) }
Text(
stringResource(R.string.nearby_notification_streaming),
Modifier.padding(start = 8.dp, top = 10.dp, bottom = 4.dp), style = typography.titleLarge
)
FullWidthRadioButtonItem(
R.string.decide_by_user,
notificationPolicy == NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY
) { notificationPolicy = NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY }
FullWidthRadioButtonItem(
R.string.enabled,
notificationPolicy == NEARBY_STREAMING_ENABLED
) { notificationPolicy = NEARBY_STREAMING_ENABLED }
FullWidthRadioButtonItem(
R.string.disabled,
notificationPolicy == NEARBY_STREAMING_DISABLED
) { notificationPolicy = NEARBY_STREAMING_DISABLED }
FullWidthRadioButtonItem(
R.string.enable_if_secure_enough,
notificationPolicy == NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY
) { notificationPolicy = NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY }
Button(
onClick = {
dpm.nearbyNotificationStreamingPolicy = notificationPolicy
notificationPolicy = dpm.nearbyNotificationStreamingPolicy
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 8.dp)
) {
Text(stringResource(R.string.apply))
}
InfoCard(R.string.info_nearby_notification_streaming_policy, 8.dp)
}
}
@Serializable object LockTaskMode
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(28)
@Composable
fun LockTaskModeScreen(onNavigateUp: () -> Unit) {
val coroutine = rememberCoroutineScope()
val pagerState = rememberPagerState { 3 }
var tabIndex by remember { mutableIntStateOf(0) }
tabIndex = pagerState.targetPage
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.lock_task_mode)) },
navigationIcon = { NavIcon(onNavigateUp) },
colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer)
)
}
) { paddingValues ->
Column(
modifier = Modifier.fillMaxSize().padding(paddingValues)
) {
TabRow(tabIndex) {
Tab(
tabIndex == 0, onClick = { coroutine.launch { pagerState.animateScrollToPage(0) } },
text = { Text(stringResource(R.string.start)) }
)
Tab(
tabIndex == 1, onClick = { coroutine.launch { pagerState.animateScrollToPage(1) } },
text = { Text(stringResource(R.string.applications)) }
)
Tab(
tabIndex == 2, onClick = { coroutine.launch { pagerState.animateScrollToPage(2) } },
text = { Text(stringResource(R.string.features)) }
)
}
HorizontalPager(pagerState, verticalAlignment = Alignment.Top) { page ->
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(start = 8.dp, end = 8.dp, bottom = 80.dp)
) {
if(page == 0) StartLockTaskMode()
else if(page == 1) LockTaskPackages()
else LockTaskFeatures()
}
}
}
}
}
@RequiresApi(28)
@Composable
private fun ColumnScope.StartLockTaskMode() {
val context = LocalContext.current
val dpm = context.getDPM()
val focusMgr = LocalFocusManager.current
var startLockTaskApp by rememberSaveable { mutableStateOf("") }
var startLockTaskActivity by rememberSaveable { mutableStateOf("") }
var specifyActivity by rememberSaveable { mutableStateOf(false) }
val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result ->
result?.let { startLockTaskApp = it }
}
Spacer(Modifier.padding(vertical = 5.dp))
OutlinedTextField(
value = startLockTaskApp,
onValueChange = { startLockTaskApp = it },
label = { Text(stringResource(R.string.package_name)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
trailingIcon = {
Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(50))
.clickable { choosePackage.launch(null) }
.padding(3.dp))
},
modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp)
)
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() }),
modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp)
)
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
if(!NotificationUtils.checkPermission(context)) return@Button
if(!dpm.isLockTaskPermitted(startLockTaskApp)) {
Toast.makeText(context, R.string.app_not_allowed, Toast.LENGTH_SHORT).show()
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 {
Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show()
}
},
enabled = startLockTaskApp.isNotBlank() && (!specifyActivity || startLockTaskActivity.isNotBlank())
) {
Text(stringResource(R.string.start))
}
InfoCard(R.string.info_start_lock_task_mode)
}
@RequiresApi(26)
@Composable
private fun ColumnScope.LockTaskPackages() {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
val lockTaskPackages = remember { mutableStateListOf<String>() }
var input by rememberSaveable { mutableStateOf("") }
val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result ->
result?.let { input = it }
}
LaunchedEffect(Unit) { lockTaskPackages.addAll(dpm.getLockTaskPackages(receiver)) }
Spacer(Modifier.padding(vertical = 5.dp))
if(lockTaskPackages.isEmpty()) Text(text = stringResource(R.string.none))
for(i in lockTaskPackages) {
ListItem(i) { lockTaskPackages -= i }
}
OutlinedTextField(
value = input,
onValueChange = { input = it },
label = { Text(stringResource(R.string.package_name)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
trailingIcon = {
Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(50))
.clickable { choosePackage.launch(null) }
.padding(3.dp))
},
modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp)
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Button(
onClick = {
lockTaskPackages.add(input)
input = ""
},
modifier = Modifier.fillMaxWidth(0.49F)
) {
Text(stringResource(R.string.add))
}
Button(
onClick = {
lockTaskPackages.remove(input)
input = ""
},
modifier = Modifier.fillMaxWidth(0.96F)
) {
Text(stringResource(R.string.remove))
}
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
dpm.setLockTaskPackages(receiver, lockTaskPackages.toTypedArray())
context.showOperationResultToast(true)
}
) {
Text(stringResource(R.string.apply))
}
InfoCard(R.string.info_lock_task_packages)
}
@RequiresApi(28)
@Composable
private fun ColumnScope.LockTaskFeatures() {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var flags by remember { mutableIntStateOf(0) }
var custom by rememberSaveable { mutableStateOf(false) }
fun refresh() {
flags = dpm.getLockTaskFeatures(receiver)
custom = flags != 0
}
LaunchedEffect(Unit) { refresh() }
Spacer(Modifier.padding(vertical = 5.dp))
RadioButtonItem(R.string.disable_all, !custom) { custom = false }
RadioButtonItem(R.string.custom, custom) { custom = true }
AnimatedVisibility(custom) {
Column {
CheckBoxItem(
R.string.ltf_sys_info,
flags and LOCK_TASK_FEATURE_SYSTEM_INFO != 0
) { flags = flags xor LOCK_TASK_FEATURE_SYSTEM_INFO }
CheckBoxItem(
R.string.ltf_notifications,
flags and LOCK_TASK_FEATURE_NOTIFICATIONS != 0
) { flags = flags xor LOCK_TASK_FEATURE_NOTIFICATIONS }
CheckBoxItem(
R.string.ltf_home,
flags and LOCK_TASK_FEATURE_HOME != 0
) { flags = flags xor LOCK_TASK_FEATURE_HOME }
CheckBoxItem(
R.string.ltf_overview,
flags and LOCK_TASK_FEATURE_OVERVIEW != 0
) { flags = flags xor LOCK_TASK_FEATURE_OVERVIEW }
CheckBoxItem(
R.string.ltf_global_actions,
flags and LOCK_TASK_FEATURE_GLOBAL_ACTIONS != 0
) { flags = flags xor LOCK_TASK_FEATURE_GLOBAL_ACTIONS }
CheckBoxItem(
R.string.ltf_keyguard,
flags and LOCK_TASK_FEATURE_KEYGUARD != 0
) { flags = flags xor LOCK_TASK_FEATURE_KEYGUARD }
if(VERSION.SDK_INT >= 30) {
CheckBoxItem(
R.string.ltf_block_activity_start_in_task,
flags and LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK != 0
) { flags = flags xor LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK }
}
}
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
try {
dpm.setLockTaskFeatures(receiver, flags)
context.showOperationResultToast(true)
} catch (e: IllegalArgumentException) {
AlertDialog.Builder(context)
.setTitle(R.string.error)
.setMessage(e.message)
.setPositiveButton(R.string.confirm) { dialog, _ -> dialog.dismiss() }
.show()
}
refresh()
}
) {
Text(stringResource(R.string.apply))
}
}
data class CaCertInfo(
val hash: String,
val data: ByteArray
)
@Serializable object CaCert
@OptIn(ExperimentalMaterial3Api::class, ExperimentalStdlibApi::class)
@Composable
fun CaCertScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
/** 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 ->
if(uri != null) {
uriToStream(context, uri) {
caCertByteArray = it.readBytes()
}
dialog = 1
}
}
val exportCertLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
if(uri != null) {
context.contentResolver.openOutputStream(uri)?.use {
it.write(caCertByteArray)
}
context.showOperationResultToast(true)
}
}
val caCerts = remember { mutableStateListOf<CaCertInfo>() }
fun refresh() {
caCerts.clear()
coroutine.launch(Dispatchers.IO) {
val md = MessageDigest.getInstance("SHA-256")
dpm.getInstalledCaCerts(receiver).forEach { ba ->
val hash = md.digest(ba).toHexString()
withContext(Dispatchers.Main) { caCerts += CaCertInfo(hash, ba) }
}
}
}
LaunchedEffect(Unit) { refresh() }
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.ca_cert)) },
navigationIcon = { NavIcon(onNavigateUp) },
actions = {
IconButton({ dialog = 3 }) {
Icon(Icons.Outlined.Delete, stringResource(R.string.delete))
}
}
)
},
floatingActionButton = {
FloatingActionButton({
Toast.makeText(context, R.string.select_ca_cert, Toast.LENGTH_SHORT).show()
getCertLauncher.launch(arrayOf("*/*"))
}) {
Icon(Icons.Default.Add, stringResource(R.string.install))
}
}
) { paddingValues ->
LazyColumn(
Modifier.fillMaxSize().padding(paddingValues),
horizontalAlignment = Alignment.CenterHorizontally
) {
items(caCerts, { it.hash }) { cert ->
Column(
Modifier.fillMaxWidth().clickable{
caCertByteArray = cert.data
dialog = 2
}.animateItem().padding(vertical = 10.dp, horizontal = 8.dp)
) {
Text(cert.hash.substring(0..7))
}
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))
}
}
if(dialog != 0) AlertDialog(
text = {
if(dialog == 3) Text(stringResource(R.string.uninstall_all_user_ca_cert))
else {
var text = ""
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) {
TextButton(
onClick = {
dpm.uninstallCaCert(receiver, caCertByteArray)
refresh()
dialog = 0
},
modifier = Modifier.fillMaxWidth(0.49F),
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
) {
Text(stringResource(R.string.uninstall))
}
FilledTonalButton(
onClick = {
exportCertLauncher.launch(sha256.substring(0..7) + ".0")
},
modifier = Modifier.fillMaxWidth(0.96F)
) {
Text(stringResource(R.string.export))
}
}
}
}
},
confirmButton = {
TextButton({
try {
if(dialog == 1) {
context.showOperationResultToast(dpm.installCaCert(receiver, caCertByteArray))
}
if(dialog == 3) {
dpm.uninstallAllUserCaCerts(receiver)
}
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 }
)
}
}
@Serializable object SecurityLogging
@RequiresApi(24)
@Composable
fun SecurityLoggingScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val logFile = context.filesDir.resolve("SecurityLogs.json")
var fileSize by remember { mutableLongStateOf(0) }
LaunchedEffect(Unit) { fileSize = logFile.length() }
var preRebootSecurityLogs by remember { mutableStateOf(byteArrayOf()) }
val exportPreRebootSecurityLogs = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri ->
if(uri != null) context.contentResolver.openOutputStream(uri)?.use { outStream ->
preRebootSecurityLogs.inputStream().copyTo(outStream)
}
}
val exportSecurityLogs = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri ->
if(uri != null) context.contentResolver.openOutputStream(uri)?.use { outStream ->
outStream.write("[".toByteArray())
logFile.inputStream().use { it.copyTo(outStream) }
outStream.write("]".toByteArray())
context.showOperationResultToast(true)
}
}
MyScaffold(R.string.security_logging, 8.dp, onNavigateUp) {
SwitchItem(
R.string.enable,
getState = { dpm.isSecurityLoggingEnabled(receiver) }, onCheckedChange = { dpm.setSecurityLoggingEnabled(receiver, it) },
padding = false
)
Text(stringResource(R.string.log_file_size_is, formatFileSize(fileSize)))
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Button(
onClick = {
exportSecurityLogs.launch("SecurityLogs.json")
},
enabled = fileSize > 0,
modifier = Modifier.fillMaxWidth(0.49F)
) {
Text(stringResource(R.string.export_logs))
}
Button(
onClick = {
logFile.delete()
fileSize = logFile.length()
},
enabled = fileSize > 0,
modifier = Modifier.fillMaxWidth(0.96F)
) {
Text(stringResource(R.string.delete_logs))
}
}
InfoCard(R.string.info_security_log)
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
val logs = dpm.retrievePreRebootSecurityLogs(receiver)
if(logs == null) {
Toast.makeText(context, R.string.no_logs, Toast.LENGTH_SHORT).show()
return@Button
} else {
val outputStream = ByteArrayOutputStream()
outputStream.write("[".encodeToByteArray())
processSecurityLogs(logs, outputStream)
outputStream.write("]".encodeToByteArray())
preRebootSecurityLogs = outputStream.toByteArray()
exportPreRebootSecurityLogs.launch("PreRebootSecurityLogs.json")
}
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.pre_reboot_security_logs))
}
InfoCard(R.string.info_pre_reboot_security_log)
}
}
@Serializable object DisableAccountManagement
@Composable
fun DisableAccountManagementScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
MyScaffold(R.string.disable_account_management, 8.dp, onNavigateUp) {
val list = remember { mutableStateListOf<String>() }
fun refreshList() {
list.clear()
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) {
dpm.setAccountManagementDisabled(receiver, i, false)
refreshList()
}
}
}
var inputText by remember{ mutableStateOf("") }
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
label = { Text(stringResource(R.string.account_type)) },
trailingIcon = {
IconButton(
onClick = {
dpm.setAccountManagementDisabled(receiver, inputText, true)
inputText = ""
refreshList()
},
enabled = inputText != ""
) {
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.add))
}
},
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() })
)
Spacer(Modifier.padding(vertical = 10.dp))
InfoCard(R.string.info_disable_account_management)
}
}
@Serializable object FrpPolicy
@RequiresApi(30)
@Composable
fun FrpPolicyScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val focusMgr = LocalFocusManager.current
val receiver = context.getReceiver()
var usePolicy by remember { mutableStateOf(false) }
var enabled by remember { mutableStateOf(false) }
var unsupported by remember { mutableStateOf(false) }
val accountList = remember { mutableStateListOf<String>() }
var inputAccount by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
var policy: FactoryResetProtectionPolicy? = FactoryResetProtectionPolicy.Builder().build()
try {
policy = dpm.getFactoryResetProtectionPolicy(receiver)
} catch(_: UnsupportedOperationException) {
unsupported = true
policy = null
} finally {
if(policy == null) {
usePolicy = false
} else {
usePolicy = true
enabled = policy.isFactoryResetProtectionEnabled
}
}
}
MyScaffold(R.string.frp_policy, 8.dp, onNavigateUp) {
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 })
}
AnimatedVisibility(usePolicy) {
Column {
CheckBoxItem(R.string.enable_frp, enabled) { enabled = it }
Text(stringResource(R.string.account_list_is))
Column(modifier = Modifier.animateContentSize()) {
if(accountList.isEmpty()) Text(stringResource(R.string.none))
for(i in accountList) {
ListItem(i) { accountList -= i }
}
}
OutlinedTextField(
value = inputAccount,
onValueChange = { inputAccount = it },
label = { Text(stringResource(R.string.account)) },
trailingIcon = {
IconButton(
onClick = {
accountList += inputAccount
inputAccount = ""
},
enabled = inputAccount.isNotBlank()
) {
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.add))
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.padding(vertical = 2.dp))
}
}
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
focusMgr.clearFocus()
if(unsupported) {
Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show()
} else {
val policy = FactoryResetProtectionPolicy.Builder()
.setFactoryResetProtectionEnabled(enabled)
.setFactoryResetProtectionAccounts(accountList)
.build()
dpm.setFactoryResetProtectionPolicy(receiver, policy)
}
},
modifier = Modifier.width(100.dp).align(Alignment.CenterHorizontally)
) {
Text(stringResource(R.string.apply))
}
Spacer(Modifier.padding(vertical = 10.dp))
if(unsupported) Text(stringResource(R.string.frp_policy_not_supported))
Spacer(Modifier.padding(vertical = 6.dp))
InfoCard(R.string.info_frp_policy)
}
}
@Serializable object WipeData
@Composable
fun WipeDataScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
val dpm = context.getDPM()
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 reason by remember { mutableStateOf("") }
MyScaffold(R.string.wipe_data, 8.dp, onNavigateUp) {
CheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { flag = flag xor WIPE_EXTERNAL_STORAGE }
if(VERSION.SDK_INT >= 22 && context.isDeviceOwner) 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)
)
}
Spacer(Modifier.padding(vertical = 5.dp))
if(VERSION.SDK_INT < 34 || !userManager.isSystemUser) {
Button(
onClick = {
focusMgr.clearFocus()
wipeDevice = false
warning = true
},
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
modifier = Modifier.fillMaxWidth()
) {
Text("WipeData")
}
}
if (VERSION.SDK_INT >= 34 && context.isDeviceOwner) {
Button(
onClick = {
focusMgr.clearFocus()
wipeDevice = true
warning = true
},
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
modifier = Modifier.fillMaxWidth()
) {
Text("WipeDevice")
}
}
}
if(warning) {
LaunchedEffect(Unit) { silent = reason == "" }
AlertDialog(
title = {
Text(text = stringResource(R.string.warning), color = colorScheme.error)
},
text = {
Text(
text = stringResource(
if(VERSION.SDK_INT >= 23 && userManager.isSystemUser) R.string.wipe_data_warning
else R.string.info_wipe_data_in_managed_user
),
color = colorScheme.error
)
},
onDismissRequest = { warning = false },
confirmButton = {
var timer by remember { mutableIntStateOf(6) }
LaunchedEffect(Unit) {
while(timer > 0) {
timer -= 1
delay(1000)
}
}
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) {
dpm.wipeDevice(flag)
} else {
if(VERSION.SDK_INT >= 28 && reason != "") {
dpm.wipeData(flag, reason)
} else {
dpm.wipeData(flag)
}
}
},
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error),
modifier = Modifier.animateContentSize(),
enabled = timer == 0
) {
Text(stringResource(R.string.confirm) + timerText)
}
},
dismissButton = {
TextButton(onClick = { warning = false }) {
Text(stringResource(R.string.cancel))
}
}
)
}
}
@Serializable object SetSystemUpdatePolicy
@RequiresApi(23)
@Composable
fun SystemUpdatePolicyScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
MyScaffold(R.string.system_update_policy, 0.dp, onNavigateUp) {
var selectedPolicy by remember { mutableStateOf(dpm.systemUpdatePolicy?.policyType) }
FullWidthRadioButtonItem(
R.string.system_update_policy_automatic,
selectedPolicy == TYPE_INSTALL_AUTOMATIC
) { selectedPolicy = TYPE_INSTALL_AUTOMATIC }
FullWidthRadioButtonItem(
R.string.system_update_policy_install_windowed,
selectedPolicy == TYPE_INSTALL_WINDOWED
) { selectedPolicy = 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) {
Column(Modifier.padding(horizontal = 8.dp)) {
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), Arrangement.SpaceBetween) {
OutlinedTextField(
value = windowedPolicyStart,
label = { Text(stringResource(R.string.start_time)) },
onValueChange = { windowedPolicyStart = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
modifier = Modifier.fillMaxWidth(0.49F)
)
OutlinedTextField(
value = windowedPolicyEnd,
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)
)
}
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
}
dpm.setSystemUpdatePolicy(receiver,policy)
context.showOperationResultToast(true)
},
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 8.dp)
) {
Text(stringResource(R.string.apply))
}
if(VERSION.SDK_INT >= 26) {
val sysUpdateInfo = dpm.getPendingSystemUpdate(receiver)
Column(Modifier.padding(8.dp)) {
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)))
} else {
Text(text = stringResource(R.string.no_system_update))
}
}
}
}
}
@Serializable object InstallSystemUpdate
@SuppressLint("NewApi")
@Composable
fun InstallSystemUpdateScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
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)
Toast.makeText(context, errMsg, Toast.LENGTH_SHORT).show()
}
}
var uri by remember { mutableStateOf<Uri?>(null) }
val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri = it }
MyScaffold(R.string.install_system_update, 8.dp, onNavigateUp) {
Button(
onClick = {
getFileLauncher.launch("application/zip")
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.select_ota_package))
}
AnimatedVisibility(uri != null) {
Button(
onClick = {
val executor = Executors.newCachedThreadPool()
try {
dpm.installSystemUpdate(receiver, uri!!, executor, callback)
Toast.makeText(context, R.string.start_install_system_update, Toast.LENGTH_SHORT).show()
} catch(e: Exception) {
Toast.makeText(
context,
context.getString(R.string.install_system_update_failed) + e.cause.toString(),
Toast.LENGTH_SHORT
).show()
}
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.install_system_update))
}
}
Spacer(Modifier.padding(vertical = 10.dp))
InfoCard(R.string.auto_reboot_after_install_succeed)
}
}