package com.bintianqi.owndroid.dpm import android.annotation.SuppressLint import android.app.admin.DevicePolicyManager 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.SystemUpdateInfo 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.Context import android.net.Uri import android.os.Build.VERSION import android.os.HardwarePropertiesManager import android.os.UserManager 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.background 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.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 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.Checkbox 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.Slider 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.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.stringResource 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.MyViewModel import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SP import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.humanReadableDate import com.bintianqi.owndroid.parseTimestamp import com.bintianqi.owndroid.popToast import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.ErrorDialog import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem import com.bintianqi.owndroid.ui.FunctionItem 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.Notes import com.bintianqi.owndroid.ui.SwitchItem 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.serialization.Serializable import java.io.ByteArrayOutputStream import java.util.Date import java.util.TimeZone import kotlin.math.roundToLong @Serializable object SystemManager @Composable 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*/ var dialog by remember { mutableIntStateOf(0) } var enrollmentSpecificId by remember { mutableStateOf(if (VERSION.SDK_INT >= 31 && (privilege.device || privilege.profile)) Privilege.DPM.enrollmentSpecificId else "") } MyScaffold(R.string.system, onNavigateUp, 0.dp) { 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 && privilege.device && !privilege.dhizuku) FunctionItem(R.string.hardware_monitor, icon = R.drawable.memory_fill0) { onNavigate(HardwareMonitor) } if(VERSION.SDK_INT >= 24 && privilege.device) { FunctionItem(R.string.reboot, icon = R.drawable.restart_alt_fill0) { dialog = 1 } } if(VERSION.SDK_INT >= 24 && privilege.device && (VERSION.SDK_INT < 28 || privilege.affiliated)) { FunctionItem(R.string.bug_report, icon = R.drawable.bug_report_fill0) { dialog = 2 } } if(VERSION.SDK_INT >= 28 && (privilege.device || privilege.org)) { FunctionItem(R.string.change_time, icon = R.drawable.schedule_fill0) { onNavigate(ChangeTime) } FunctionItem(R.string.change_timezone, icon = R.drawable.globe_fill0) { onNavigate(ChangeTimeZone) } } if (VERSION.SDK_INT >= 36 && (privilege.device || privilege.org)) { FunctionItem(R.string.auto_time_policy, icon = R.drawable.schedule_fill0) { onNavigate(AutoTimePolicy) } FunctionItem(R.string.auto_timezone_policy, icon = R.drawable.globe_fill0) { onNavigate(AutoTimeZonePolicy) } } /*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 && (privilege.device || (privilege.profile && privilege.affiliated))) FunctionItem(R.string.content_protection_policy, icon = R.drawable.search_fill0) { onNavigate(ContentProtectionPolicy) } if(VERSION.SDK_INT >= 23) { FunctionItem(R.string.permission_policy, icon = R.drawable.key_fill0) { onNavigate(PermissionPolicy) } } if(VERSION.SDK_INT >= 34 && privilege.device) { FunctionItem(R.string.mte_policy, icon = R.drawable.memory_fill0) { onNavigate(MtePolicy) } } if(VERSION.SDK_INT >= 31) { FunctionItem(R.string.nearby_streaming_policy, icon = R.drawable.share_fill0) { onNavigate(NearbyStreamingPolicy) } } if(VERSION.SDK_INT >= 28 && privilege.device) { FunctionItem(R.string.lock_task_mode, icon = R.drawable.lock_fill0) { onNavigate(LockTaskMode) } } FunctionItem(R.string.ca_cert, icon = R.drawable.license_fill0) { onNavigate(CaCert) } if(VERSION.SDK_INT >= 26 && !privilege.dhizuku && (privilege.device || privilege.org)) { FunctionItem(R.string.security_logging, icon = R.drawable.description_fill0) { onNavigate(SecurityLogging) } } FunctionItem(R.string.device_info, icon = R.drawable.perm_device_information_fill0) { onNavigate(DeviceInfo) } if(VERSION.SDK_INT >= 24 && (privilege.profile || (VERSION.SDK_INT >= 26 && privilege.device))) { FunctionItem(R.string.org_name, icon = R.drawable.corporate_fare_fill0) { dialog = 3 } } if(VERSION.SDK_INT >= 31) { FunctionItem(R.string.org_id, icon = R.drawable.corporate_fare_fill0) { dialog = 4 } } if(enrollmentSpecificId != "") { FunctionItem(R.string.enrollment_specific_id, icon = R.drawable.id_card_fill0) { dialog = 5 } } if(VERSION.SDK_INT >= 24 && (privilege.device || privilege.org)) { FunctionItem(R.string.lock_screen_info, icon = R.drawable.screen_lock_portrait_fill0) { onNavigate(LockScreenInfo) } } if(VERSION.SDK_INT >= 24) { FunctionItem(R.string.support_messages, icon = R.drawable.chat_fill0) { onNavigate(SupportMessage) } } FunctionItem(R.string.disable_account_management, icon = R.drawable.account_circle_fill0) { onNavigate(DisableAccountManagement) } if(VERSION.SDK_INT >= 23 && (privilege.device || privilege.org)) { FunctionItem(R.string.system_update_policy, icon = R.drawable.system_update_fill0) { onNavigate(SetSystemUpdatePolicy) } } if(VERSION.SDK_INT >= 29 && (privilege.device || privilege.org)) { FunctionItem(R.string.install_system_update, icon = R.drawable.system_update_fill0) { onNavigate(InstallSystemUpdate) } } if(VERSION.SDK_INT >= 30 && (privilege.device || privilege.org)) { FunctionItem(R.string.frp_policy, icon = R.drawable.device_reset_fill0) { onNavigate(FrpPolicy) } } if(SP.displayDangerousFeatures && !privilege.work) { FunctionItem(R.string.wipe_data, icon = R.drawable.device_reset_fill0) { onNavigate(WipeData) } } } if((dialog == 1 || dialog == 2) && 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) { vm.reboot() } else { context.showOperationResultToast(vm.requestBugReport()) } dialog = 0 } ) { Text(stringResource(R.string.confirm)) } }, modifier = Modifier.fillMaxWidth() ) if(dialog in 3..5) { var input by remember { mutableStateOf("") } AlertDialog( text = { val focusMgr = LocalFocusManager.current LaunchedEffect(Unit) { 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), readOnly = dialog == 5, label = { Text(stringResource( when(dialog){ 3 -> R.string.org_name 4 -> R.string.org_id 5 -> R.string.enrollment_specific_id else -> R.string.place_holder } )) }, supportingText = { if(dialog == 4) Text(stringResource(R.string.length_6_to_64)) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions { focusMgr.clearFocus() }, textStyle = typography.bodyLarge ) if(dialog == 5) Text(stringResource(R.string.info_enrollment_specific_id)) if(dialog == 4) Text(stringResource(R.string.info_org_id)) } }, onDismissRequest = { dialog = 0 }, dismissButton = { if (dialog != 5) TextButton({ dialog = 0 }) { Text(stringResource(R.string.cancel)) } }, confirmButton = { TextButton( onClick = { 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 ) { Text(stringResource(R.string.confirm)) } } ) } } 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(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, 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(VERSION.SDK_INT >= 30) { 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, status.autoTimeRequired, vm::setAutoTimeRequired, R.drawable.schedule_fill0) } } 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, state = status.backupServiceEnabled, onCheckedChange = vm::setBackupServiceEnabled, onClickBlank = { dialog = 1 }) } 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 || 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) && status.canDisableUsbSignal) { SwitchItem(R.string.enable_usb_signal, status.usbSignalEnabled, vm::setUsbSignalEnabled, R.drawable.usb_fill0) } } 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( 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))) { Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() ) { Button( onClick = { context.showOperationResultToast(setKeyguardDisabled(true)) }, modifier = Modifier.fillMaxWidth(0.49F) ) { Text(stringResource(R.string.disable)) } Button( onClick = { context.showOperationResultToast(setKeyguardDisabled(false)) }, modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.enable)) } } Notes(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 evictKey by remember { mutableStateOf(false) } Button( onClick = { lock(evictKey) }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.lock_now)) } 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( 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() LaunchedEffect(Unit) { getHardwareProperties() } MyScaffold(R.string.hardware_monitor, onNavigateUp) { 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)) 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 { 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 (properties.cpuUsages.isEmpty()) { Text(stringResource(R.string.unsupported)) } else { 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 { 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 (properties.fanSpeeds.isEmpty()) { Text(stringResource(R.string.unsupported)) } else { 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") } } } } } @Serializable object ChangeTime @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(28) @Composable 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() val dateInteractionSource = remember { MutableInteractionSource() } val timeInteractionSource = remember { MutableInteractionSource() } if(dateInteractionSource.collectIsPressedAsState().value) picker = 1 if(timeInteractionSource.collectIsPressedAsState().value) picker = 2 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) ) { 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)) } ) } 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)) } } } } } } 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(setTimeZone: (String) -> Boolean, onNavigateUp: () -> Unit) { val context = LocalContext.current val focusMgr = LocalFocusManager.current var inputTimezone by remember { mutableStateOf("") } var dialog by remember { mutableStateOf(false) } MyScaffold(R.string.change_timezone, 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(setTimeZone(inputTimezone)) }, modifier = Modifier.fillMaxWidth(), enabled = inputTimezone.isNotEmpty() ) { Text(stringResource(R.string.apply)) } Spacer(Modifier.padding(vertical = 10.dp)) Notes(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 } ) } @Serializable object AutoTimePolicy @RequiresApi(36) @Composable 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, DevicePolicyManager.AUTO_TIME_NOT_CONTROLLED_BY_POLICY to R.string.not_controlled_by_policy ).forEach { FullWidthRadioButtonItem(it.second, it.first == policy) { policy = it.first } } Button( { setPolicy(policy) context.showOperationResultToast(true) }, Modifier .fillMaxWidth() .padding(horizontal = HorizontalPadding) ) { Text(stringResource(R.string.apply)) } } @Serializable object AutoTimeZonePolicy @RequiresApi(36) @Composable 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, DevicePolicyManager.AUTO_TIME_ZONE_NOT_CONTROLLED_BY_POLICY to R.string.not_controlled_by_policy ).forEach { FullWidthRadioButtonItem(it.second, it.first == policy) { policy = it.first } } Button({ setPolicy(policy) context.showOperationResultToast(true) }, Modifier .fillMaxWidth() .padding(horizontal = HorizontalPadding)) { Text(stringResource(R.string.apply)) } } /*@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( getPolicy: () -> Int, setPolicy: (Int) -> Unit, onNavigateUp: () -> Unit ) { val context = LocalContext.current 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, 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 = { setPolicy(policy) context.showOperationResultToast(true) }, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp, horizontal = HorizontalPadding) ) { Text(stringResource(R.string.apply)) } Notes(R.string.info_content_protection_policy, HorizontalPadding) } } @Serializable object PermissionPolicy @RequiresApi(23) @Composable fun PermissionPolicyScreen( getPolicy: () -> Int, setPolicy: (Int) -> Unit, onNavigateUp: () -> Unit ) { val context = LocalContext.current 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 } 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 } Button( onClick = { setPolicy(selectedPolicy) context.showOperationResultToast(true) }, modifier = Modifier .fillMaxWidth() .padding(HorizontalPadding, 5.dp) ) { Text(stringResource(R.string.apply)) } Notes(R.string.info_permission_policy, HorizontalPadding) } } @Serializable object MtePolicy @RequiresApi(34) @Composable 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, policy == MTE_NOT_CONTROLLED_BY_POLICY) { policy = MTE_NOT_CONTROLLED_BY_POLICY } FullWidthRadioButtonItem(R.string.enabled, policy == MTE_ENABLED) { policy = MTE_ENABLED } FullWidthRadioButtonItem(R.string.disabled, policy == MTE_DISABLED) { policy = MTE_DISABLED } Button( onClick = { if (!setPolicy(policy)) policy = getPolicy() }, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp, horizontal = HorizontalPadding) ) { Text(stringResource(R.string.apply)) } Notes(R.string.info_mte_policy, HorizontalPadding) } } @Serializable object NearbyStreamingPolicy @RequiresApi(31) @Composable fun NearbyStreamingPolicyScreen( getAppPolicy: () -> Int, setAppPolicy: (Int) -> Unit, getNotificationPolicy: () -> Int, setNotificationPolicy: (Int) -> Unit, onNavigateUp: () -> Unit ) { val context = LocalContext.current var appPolicy by remember { mutableIntStateOf(getAppPolicy()) } MySmallTitleScaffold(R.string.nearby_streaming_policy, onNavigateUp, 0.dp) { 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 = { setAppPolicy(appPolicy) context.showOperationResultToast(true) }, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp, horizontal = HorizontalPadding) ) { Text(stringResource(R.string.apply)) } Notes(R.string.info_nearby_app_streaming_policy, HorizontalPadding) 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 ) 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 = { setNotificationPolicy(notificationPolicy) context.showOperationResultToast(true) }, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp, horizontal = HorizontalPadding) ) { Text(stringResource(R.string.apply)) } Notes(R.string.info_nearby_notification_streaming_policy, HorizontalPadding) } } @Serializable object LockTaskMode @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(28) @Composable fun LockTaskModeScreen( 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( title = { Text(stringResource(R.string.lock_task_mode)) }, navigationIcon = { NavIcon(onNavigateUp) }, colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer) ) }, contentWindowInsets = WindowInsets.ime ) { 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 -> if(page == 0) { StartLockTaskMode(startLockTaskMode, chosenPackage, onChoosePackage) } else if (page == 1) { LockTaskPackages(chosenPackage, onChoosePackage, lockTaskPackages, setLockTaskPackage) } else { LockTaskFeatures(getLockTaskFeatures, setLockTaskFeature) } } } } } @RequiresApi(28) @Composable private fun StartLockTaskMode( startLockTaskMode: (String, String) -> Unit, chosenPackage: Channel, onChoosePackage: () -> Unit ) { val focusMgr = LocalFocusManager.current var packageName by rememberSaveable { mutableStateOf("") } var activity by rememberSaveable { mutableStateOf("") } var specifyActivity by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { packageName = chosenPackage.receive() } 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), onClick = { startLockTaskMode(packageName, activity) }, enabled = packageName.isNotBlank() && (!specifyActivity || activity.isNotBlank()) ) { Text(stringResource(R.string.start)) } Notes(R.string.info_start_lock_task_mode) } } @RequiresApi(26) @Composable private fun LockTaskPackages( chosenPackage: Channel, onChoosePackage: () -> Unit, lockTaskPackages: StateFlow>, setLockTaskPackage: (String, Boolean) -> Unit ) { val packages by lockTaskPackages.collectAsStateWithLifecycle() var packageName by rememberSaveable { mutableStateOf("") } LaunchedEffect(Unit) { packageName = chosenPackage.receive() } LazyColumn { items(packages, { it.name }) { ApplicationItem(it) { setLockTaskPackage(it.name, false) } } 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) } } } } @RequiresApi(28) @Composable private fun LockTaskFeatures( getLockTaskFeatures: () -> Int, setLockTaskFeature: (Int) -> String? ) { val context = LocalContext.current var flags by remember { mutableIntStateOf(getLockTaskFeatures()) } var errorMessage by remember { mutableStateOf(null) } Column( Modifier .fillMaxWidth() .verticalScroll(rememberScrollState()) ) { 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 } } } data class CaCertInfo( val hash: String, 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( 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) } val caCerts by caCertificates.collectAsStateWithLifecycle() var selectedCaCert by remember { mutableStateOf(null) } val getCertLauncher = rememberLauncherForActivityResult( ActivityResultContracts.OpenDocument()) { uri -> if(uri != null) { selectedCaCert = parseCert(uri) dialog = 1 } } val exportCertLauncher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument()) { uri -> if(uri != null) exportCert(uri, selectedCaCert!!) } LaunchedEffect(Unit) { getCerts() } 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({ context.popToast(R.string.select_ca_cert) getCertLauncher.launch(arrayOf("*/*")) }) { Icon(Icons.Default.Add, stringResource(R.string.install)) } }, contentWindowInsets = WindowInsets.ime ) { paddingValues -> LazyColumn( Modifier .fillMaxSize() .padding(paddingValues), horizontalAlignment = Alignment.CenterHorizontally ) { items(caCerts, { it.hash }) { cert -> Column( Modifier .fillMaxWidth() .clickable { selectedCaCert = cert } .animateItem() .padding(vertical = 10.dp, horizontal = 8.dp) ) { Text(cert.hash.substring(0..7)) } HorizontalDivider() } item { Spacer(Modifier.height(40.dp)) } } 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 = { uninstallCert(cert) dialog = 0 }, modifier = Modifier.fillMaxWidth(0.49F), colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) ) { Text(stringResource(R.string.uninstall)) } FilledTonalButton( onClick = { exportCertLauncher.launch(cert.hash.substring(0..7) + ".0") }, modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.export)) } } } }, confirmButton = { if (dialog == 1) { TextButton({ context.showOperationResultToast(installCert(cert)) dialog = 0 }) { Text(stringResource(R.string.install)) } } else { TextButton({ dialog = 0 }) { Text(stringResource(R.string.confirm)) } } }, 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 } ) } } } @Serializable object SecurityLogging @RequiresApi(24) @Composable fun SecurityLoggingScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current 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, onNavigateUp) { SwitchItem( R.string.enable, getState = { Privilege.DPM.isSecurityLoggingEnabled(Privilege.DAR) }, onCheckedChange = { Privilege.DPM.setSecurityLoggingEnabled(Privilege.DAR, 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)) } } Notes(R.string.info_security_log) Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { val logs = Privilege.DPM.retrievePreRebootSecurityLogs(Privilege.DAR) if(logs == null) { context.popToast(R.string.no_logs) 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)) } Notes(R.string.info_pre_reboot_security_log) } } @Serializable object DisableAccountManagement @Composable 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) { Column(modifier = Modifier.animateContentSize()) { for(i in list) { ListItem(i) { setMdAccount(i, false) } } } var inputText by remember{ mutableStateOf("") } OutlinedTextField( value = inputText, onValueChange = { inputText = it }, label = { Text(stringResource(R.string.account_type)) }, trailingIcon = { IconButton( onClick = { setMdAccount(inputText, true) inputText = "" }, 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)) Notes(R.string.info_disable_account_management) } } data class FrpPolicyInfo( val supported: Boolean, val usePolicy: Boolean, val enabled: Boolean, val accounts: List ) @Serializable object FrpPolicy @RequiresApi(30) @Composable 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 supported by remember { mutableStateOf(false) } val accountList = remember { mutableStateListOf() } var inputAccount by remember { mutableStateOf("") } LaunchedEffect(Unit) { 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, 0.dp) { if (!supported) { Column( Modifier .fillMaxWidth() .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 { SwitchItem(R.string.use_policy, usePolicy, { usePolicy = 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)) 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() ) } Button( onClick = { focusMgr.clearFocus() setFrpPolicy(FrpPolicyInfo(true, usePolicy, enabled, accountList)) }, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp) ) { Text(stringResource(R.string.apply)) } } Notes(R.string.info_frp_policy, HorizontalPadding) } } @Serializable object WipeData @Composable 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(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, 0.dp) { FullWidthCheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { flag = flag xor WIPE_EXTERNAL_STORAGE } 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() dialog = 1 }, colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), modifier = Modifier.fillMaxWidth() ) { Text("WipeData") } } if (VERSION.SDK_INT >= 34 && privilege.device) { Button( onClick = { focusMgr.clearFocus() dialog = 2 }, colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), modifier = Modifier.fillMaxWidth() ) { Text("WipeDevice") } } } if (dialog != 0) { 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 = { dialog = 0 }, 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 = { wipeData(dialog == 2, flag, reason) }, colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error), modifier = Modifier.animateContentSize(), enabled = timer == 0 ) { Text(stringResource(R.string.confirm) + timerText) } }, dismissButton = { TextButton(onClick = { dialog = 0 }) { Text(stringResource(R.string.cancel)) } } ) } } 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( 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) { FullWidthRadioButtonItem(R.string.none, policyType == -1) { policyType = -1 } FullWidthRadioButtonItem( R.string.system_update_policy_automatic, policyType == TYPE_INSTALL_AUTOMATIC ) { policyType = TYPE_INSTALL_AUTOMATIC } FullWidthRadioButtonItem( R.string.system_update_policy_install_windowed, policyType == TYPE_INSTALL_WINDOWED ) { policyType = TYPE_INSTALL_WINDOWED } FullWidthRadioButtonItem( R.string.system_update_policy_postpone, 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 ) { 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) ) } Text(stringResource(R.string.minutes_in_one_day), color = colorScheme.onSurfaceVariant, style = typography.bodyMedium) } } Button( onClick = { setPolicy(SystemUpdatePolicyInfo( policyType, windowedPolicyStart.toIntOrNull() ?: 0, windowedPolicyEnd.toIntOrNull() ?: 0 )) context.showOperationResultToast(true) }, modifier = Modifier .fillMaxWidth() .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) { Column(Modifier.padding(HorizontalPadding)) { 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)) } } } } } @Serializable object InstallSystemUpdate @SuppressLint("NewApi") @Composable 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) { Button( onClick = { getFileLauncher.launch("application/zip") }, modifier = Modifier .fillMaxWidth() .padding(top = 8.dp) ) { Text(stringResource(R.string.select_ota_package)) } 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) } ErrorDialog(errorMessage) { errorMessage = null } }