mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 19:15:58 +00:00
1891 lines
84 KiB
Kotlin
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)
|
|
}
|
|
}
|