mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 11:05:59 +00:00
923 lines
40 KiB
Kotlin
923 lines
40 KiB
Kotlin
package com.bintianqi.owndroid.dpm
|
|
|
|
import android.app.admin.DevicePolicyManager
|
|
import android.content.ComponentName
|
|
import android.content.Context
|
|
import android.content.pm.PackageManager
|
|
import android.os.Build.VERSION
|
|
import android.os.PersistableBundle
|
|
import android.widget.Toast
|
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
import androidx.annotation.Keep
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.annotation.StringRes
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
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.padding
|
|
import androidx.compose.foundation.rememberScrollState
|
|
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.KeyboardArrowRight
|
|
import androidx.compose.material.icons.filled.Add
|
|
import androidx.compose.material.icons.filled.Check
|
|
import androidx.compose.material.icons.filled.MoreVert
|
|
import androidx.compose.material.icons.filled.Settings
|
|
import androidx.compose.material.icons.outlined.Edit
|
|
import androidx.compose.material.icons.outlined.Warning
|
|
import androidx.compose.material3.AlertDialog
|
|
import androidx.compose.material3.Button
|
|
import androidx.compose.material3.ButtonDefaults
|
|
import androidx.compose.material3.Checkbox
|
|
import androidx.compose.material3.CircularProgressIndicator
|
|
import androidx.compose.material3.DropdownMenu
|
|
import androidx.compose.material3.DropdownMenuItem
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
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.Text
|
|
import androidx.compose.material3.TextButton
|
|
import androidx.compose.material3.TopAppBar
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableIntStateOf
|
|
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.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.alpha
|
|
import androidx.compose.ui.graphics.Color
|
|
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 androidx.compose.ui.window.Dialog
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
import com.bintianqi.owndroid.ChoosePackageContract
|
|
import com.bintianqi.owndroid.HorizontalPadding
|
|
import com.bintianqi.owndroid.IUserService
|
|
import com.bintianqi.owndroid.R
|
|
import com.bintianqi.owndroid.Receiver
|
|
import com.bintianqi.owndroid.Settings
|
|
import com.bintianqi.owndroid.SharedPrefs
|
|
import com.bintianqi.owndroid.myPrivilege
|
|
import com.bintianqi.owndroid.showOperationResultToast
|
|
import com.bintianqi.owndroid.ui.FunctionItem
|
|
import com.bintianqi.owndroid.ui.InfoItem
|
|
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.updatePrivilege
|
|
import com.bintianqi.owndroid.useShizuku
|
|
import com.bintianqi.owndroid.writeClipBoard
|
|
import com.bintianqi.owndroid.yesOrNo
|
|
import com.rosan.dhizuku.api.Dhizuku
|
|
import com.rosan.dhizuku.api.DhizukuRequestPermissionListener
|
|
import com.topjohnwu.superuser.Shell
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.serialization.Serializable
|
|
|
|
@Serializable object Permissions
|
|
|
|
@Composable
|
|
fun PermissionsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
|
|
val context = LocalContext.current
|
|
val dpm = context.getDPM()
|
|
val receiver = context.getReceiver()
|
|
val privilege by myPrivilege.collectAsStateWithLifecycle()
|
|
var dialog by remember { mutableIntStateOf(0) }
|
|
var bindingShizuku by remember { mutableStateOf(false) }
|
|
val enrollmentSpecificId = if(VERSION.SDK_INT >= 31 && (privilege.device || privilege.profile)) dpm.enrollmentSpecificId else ""
|
|
MyScaffold(R.string.permissions, onNavigateUp, 0.dp) {
|
|
if(VERSION.SDK_INT >= 26) FunctionItem(R.string.delegated_admins) { onNavigate(DelegatedAdmins) }
|
|
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 = 2 }
|
|
}
|
|
if(VERSION.SDK_INT >= 31) {
|
|
FunctionItem(R.string.org_id, icon = R.drawable.corporate_fare_fill0) { dialog = 3 }
|
|
}
|
|
if(enrollmentSpecificId != "") {
|
|
FunctionItem(R.string.enrollment_specific_id, icon = R.drawable.id_card_fill0) { dialog = 1 }
|
|
}
|
|
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) }
|
|
}
|
|
}
|
|
if(bindingShizuku) {
|
|
Dialog(onDismissRequest = { bindingShizuku = false }) {
|
|
CircularProgressIndicator()
|
|
}
|
|
}
|
|
if(dialog != 0) {
|
|
var input by remember { mutableStateOf("") }
|
|
AlertDialog(
|
|
title = {
|
|
Text(stringResource(
|
|
when(dialog){
|
|
1 -> R.string.enrollment_specific_id
|
|
2 -> R.string.org_name
|
|
3 -> R.string.org_id
|
|
4 -> R.string.dhizuku
|
|
else -> R.string.permissions
|
|
}
|
|
))
|
|
},
|
|
text = {
|
|
val focusMgr = LocalFocusManager.current
|
|
LaunchedEffect(Unit) {
|
|
if(dialog == 1 && VERSION.SDK_INT >= 31) input = dpm.enrollmentSpecificId
|
|
}
|
|
Column {
|
|
if(dialog != 4) OutlinedTextField(
|
|
value = input,
|
|
onValueChange = { input = it }, readOnly = dialog == 1,
|
|
label = {
|
|
Text(stringResource(
|
|
when(dialog){
|
|
1 -> R.string.enrollment_specific_id
|
|
2 -> R.string.org_name
|
|
3 -> R.string.org_id
|
|
else -> R.string.permissions
|
|
}
|
|
))
|
|
},
|
|
trailingIcon = {
|
|
if(dialog == 1) IconButton(onClick = { writeClipBoard(context, input) }) {
|
|
Icon(painter = painterResource(R.drawable.content_copy_fill0), contentDescription = stringResource(R.string.copy))
|
|
}
|
|
},
|
|
supportingText = {
|
|
if(dialog == 3) Text(stringResource(R.string.length_6_to_64))
|
|
},
|
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
|
keyboardActions = KeyboardActions { focusMgr.clearFocus() },
|
|
textStyle = typography.bodyLarge,
|
|
modifier = Modifier.fillMaxWidth().padding(bottom = if(dialog == 2) 0.dp else 10.dp)
|
|
)
|
|
if(dialog == 1) Text(stringResource(R.string.info_enrollment_specific_id))
|
|
if(dialog == 3) Text(stringResource(R.string.info_org_id))
|
|
if(dialog == 4) Text(stringResource(R.string.info_dhizuku))
|
|
}
|
|
},
|
|
onDismissRequest = { dialog = 0 },
|
|
dismissButton = {
|
|
if(dialog != 4) TextButton(
|
|
onClick = { dialog = 0 }
|
|
) {
|
|
Text(stringResource(R.string.cancel))
|
|
}
|
|
},
|
|
confirmButton = {
|
|
TextButton(
|
|
onClick = {
|
|
try {
|
|
if(dialog == 2 && VERSION.SDK_INT >= 24) dpm.setOrganizationName(receiver, input)
|
|
if(dialog == 3 && VERSION.SDK_INT >= 31) dpm.setOrganizationId(input)
|
|
dialog = 0
|
|
} catch(_: IllegalStateException) {
|
|
Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show()
|
|
}
|
|
},
|
|
enabled = (dialog == 3 && input.length in 6..64) || dialog != 3
|
|
) {
|
|
Text(stringResource(R.string.confirm))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
@Serializable data class WorkModes(val canNavigateUp: Boolean)
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun WorkModesScreen(
|
|
params: WorkModes, onNavigateUp: () -> Unit, onActivate: () -> Unit, onDeactivate: () -> Unit,
|
|
onNavigate: (Any) -> Unit
|
|
) {
|
|
val context = LocalContext.current
|
|
val coroutine = rememberCoroutineScope()
|
|
val privilege by myPrivilege.collectAsStateWithLifecycle()
|
|
/** 0: none, 1: device owner, 2: circular progress indicator, 3: result, 4: deactivate, 5: command */
|
|
var dialog by remember { mutableIntStateOf(0) }
|
|
Scaffold(
|
|
topBar = {
|
|
TopAppBar(
|
|
{
|
|
if(!params.canNavigateUp) {
|
|
Column {
|
|
Text(stringResource(R.string.app_name))
|
|
Text(stringResource(R.string.choose_work_mode), Modifier.alpha(0.8F), style = typography.bodyLarge)
|
|
}
|
|
}
|
|
},
|
|
navigationIcon = {
|
|
if(params.canNavigateUp) NavIcon(onNavigateUp)
|
|
},
|
|
actions = {
|
|
var expanded by remember { mutableStateOf(false) }
|
|
if(privilege.device || privilege.profile) Box {
|
|
IconButton({ expanded = true }) {
|
|
Icon(Icons.Default.MoreVert, null)
|
|
}
|
|
DropdownMenu(expanded, { expanded = false }) {
|
|
DropdownMenuItem(
|
|
{ Text(stringResource(R.string.deactivate)) },
|
|
{
|
|
expanded = false
|
|
dialog = 4
|
|
}
|
|
)
|
|
if(!privilege.dhizuku && VERSION.SDK_INT >= 28) DropdownMenuItem(
|
|
{ Text(stringResource(R.string.transfer_ownership)) },
|
|
{
|
|
expanded = false
|
|
onNavigate(TransferOwnership)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
if(!params.canNavigateUp) IconButton({ onNavigate(Settings) }) {
|
|
Icon(Icons.Default.Settings, null)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
) { paddingValues ->
|
|
var operationSucceed by remember { mutableStateOf(false) }
|
|
var resultText by remember { mutableStateOf("") }
|
|
fun handleResult(succeeded: Boolean, activateSucceeded: Boolean, output: String?) {
|
|
if(succeeded) {
|
|
operationSucceed = activateSucceeded
|
|
resultText = output ?: ""
|
|
dialog = 3
|
|
updatePrivilege(context)
|
|
handlePrivilegeChange(context)
|
|
} else {
|
|
context.showOperationResultToast(false)
|
|
}
|
|
}
|
|
Column(Modifier.fillMaxSize().padding(paddingValues)) {
|
|
if(!privilege.profile && (VERSION.SDK_INT >= 28 || !privilege.dhizuku)) Row(
|
|
Modifier
|
|
.fillMaxWidth().clickable(!privilege.device || privilege.dhizuku) { dialog = 1 }
|
|
.background(if(privilege.device) colorScheme.primaryContainer else Color.Transparent)
|
|
.padding(HorizontalPadding, 10.dp),
|
|
Arrangement.SpaceBetween, Alignment.CenterVertically
|
|
) {
|
|
Column {
|
|
Text(stringResource(R.string.device_owner), style = typography.titleLarge)
|
|
if(!privilege.device || privilege.dhizuku) Text(
|
|
stringResource(R.string.recommended), color = colorScheme.primary, style = typography.labelLarge
|
|
)
|
|
}
|
|
Icon(
|
|
if(privilege.device) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null,
|
|
tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground
|
|
)
|
|
}
|
|
if(privilege.profile) Row(
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.background(if(privilege.device) colorScheme.primaryContainer else Color.Transparent)
|
|
.padding(HorizontalPadding, 10.dp),
|
|
Arrangement.SpaceBetween, Alignment.CenterVertically
|
|
) {
|
|
Column {
|
|
Text(stringResource(R.string.profile_owner), style = typography.titleLarge)
|
|
}
|
|
Icon(
|
|
if(privilege.device) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null,
|
|
tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground
|
|
)
|
|
}
|
|
if(privilege.dhizuku || !(privilege.device || privilege.profile)) Row(
|
|
Modifier
|
|
.fillMaxWidth()
|
|
.clickable(!privilege.dhizuku) {
|
|
dialog = 2
|
|
activateDhizukuMode(context, ::handleResult)
|
|
}
|
|
.background(if(privilege.dhizuku) colorScheme.primaryContainer else Color.Transparent)
|
|
.padding(HorizontalPadding, 10.dp),
|
|
Arrangement.SpaceBetween, Alignment.CenterVertically
|
|
) {
|
|
Text(stringResource(R.string.dhizuku), style = typography.titleLarge)
|
|
Icon(
|
|
if(privilege.dhizuku) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null,
|
|
tint = if(privilege.dhizuku) colorScheme.primary else colorScheme.onBackground
|
|
)
|
|
}
|
|
if(
|
|
privilege.work || (VERSION.SDK_INT < 24 ||
|
|
context.getDPM().isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE))
|
|
) Row(
|
|
Modifier
|
|
.fillMaxWidth().clickable(!privilege.work) { onNavigate(CreateWorkProfile) }
|
|
.background(if(privilege.device) colorScheme.primaryContainer else Color.Transparent)
|
|
.padding(HorizontalPadding, 10.dp),
|
|
Arrangement.SpaceBetween, Alignment.CenterVertically
|
|
) {
|
|
Column {
|
|
Text(stringResource(R.string.work_profile), style = typography.titleLarge)
|
|
}
|
|
Icon(
|
|
if(privilege.work) Icons.Default.Check else Icons.AutoMirrored.Default.KeyboardArrowRight, null,
|
|
tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground
|
|
)
|
|
}
|
|
Column(Modifier.padding(HorizontalPadding, 20.dp)) {
|
|
Row(Modifier.padding(bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
Icon(Icons.Outlined.Warning, null, Modifier.padding(end = 4.dp), colorScheme.error)
|
|
Text(stringResource(R.string.warning), color = colorScheme.error, style = typography.labelLarge)
|
|
}
|
|
Text(stringResource(R.string.owndroid_warning))
|
|
}
|
|
}
|
|
if(dialog == 1) AlertDialog(
|
|
title = { Text(stringResource(R.string.activate_method)) },
|
|
text = {
|
|
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
|
if(!privilege.dhizuku) Button({
|
|
dialog = 2
|
|
coroutine.launch {
|
|
activateUsingShizuku(context, ::handleResult)
|
|
}
|
|
}) {
|
|
Text(stringResource(R.string.shizuku))
|
|
}
|
|
if(!privilege.dhizuku) Button({
|
|
dialog = 2
|
|
activateUsingRoot(context, ::handleResult)
|
|
}) {
|
|
Text("Root")
|
|
}
|
|
if(VERSION.SDK_INT >= 28) Button({
|
|
dialog = 2
|
|
activateUsingDhizuku(context, ::handleResult)
|
|
}) {
|
|
Text(stringResource(R.string.dhizuku))
|
|
}
|
|
Button({ dialog = 5 }) { Text(stringResource(R.string.adb_command)) }
|
|
}
|
|
},
|
|
confirmButton = {
|
|
TextButton({ dialog = 0 }) { Text(stringResource(R.string.cancel)) }
|
|
},
|
|
onDismissRequest = { dialog = 0 }
|
|
)
|
|
if(dialog == 2) Dialog({}) {
|
|
CircularProgressIndicator()
|
|
}
|
|
if(dialog == 3) AlertDialog(
|
|
title = { Text(stringResource(if(operationSucceed) R.string.succeeded else R.string.failed)) },
|
|
text = {
|
|
Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
|
|
Text(resultText)
|
|
}
|
|
},
|
|
confirmButton = {
|
|
TextButton({
|
|
dialog = 0
|
|
if(operationSucceed && !params.canNavigateUp) onActivate()
|
|
}) {
|
|
Text(stringResource(R.string.confirm))
|
|
}
|
|
},
|
|
onDismissRequest = {
|
|
dialog = 0
|
|
if(operationSucceed && !params.canNavigateUp) onActivate()
|
|
}
|
|
)
|
|
if(dialog == 4) AlertDialog(
|
|
title = { Text(stringResource(R.string.deactivate)) },
|
|
text = { Text(stringResource(R.string.info_deactivate)) },
|
|
confirmButton = {
|
|
TextButton(
|
|
{
|
|
if(privilege.dhizuku) {
|
|
SharedPrefs(context).dhizuku = false
|
|
} else {
|
|
val dpm = context.getDPM()
|
|
if(privilege.device) {
|
|
dpm.clearDeviceOwnerApp(context.packageName)
|
|
} else if(VERSION.SDK_INT >= 24) {
|
|
dpm.clearProfileOwner(ComponentName(context, Receiver::class.java))
|
|
}
|
|
}
|
|
dialog = 0
|
|
updatePrivilege(context)
|
|
handlePrivilegeChange(context)
|
|
onDeactivate()
|
|
},
|
|
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
|
|
) { Text(stringResource(R.string.confirm)) }
|
|
},
|
|
dismissButton = {
|
|
TextButton({ dialog = 0 }) { Text(stringResource(R.string.cancel)) }
|
|
},
|
|
onDismissRequest = { dialog = 0 }
|
|
)
|
|
if(dialog == 5) AlertDialog(
|
|
text = {
|
|
SelectionContainer {
|
|
Text(ACTIVATE_DEVICE_OWNER_COMMAND)
|
|
}
|
|
},
|
|
confirmButton = {
|
|
TextButton({ dialog = 0 }) { Text(stringResource(R.string.confirm)) }
|
|
},
|
|
onDismissRequest = { dialog = 0 }
|
|
)
|
|
}
|
|
}
|
|
|
|
fun activateUsingShizuku(context: Context, callback: (Boolean, Boolean, String?) -> Unit) {
|
|
useShizuku(context) { service ->
|
|
try {
|
|
val result = IUserService.Stub.asInterface(service).execute(ACTIVATE_DEVICE_OWNER_COMMAND)
|
|
if (result == null) {
|
|
callback(false, false, null)
|
|
} else {
|
|
callback(
|
|
true, result.getInt("code", -1) == 0,
|
|
result.getString("output") + "\n" + result.getString("error")
|
|
)
|
|
}
|
|
} catch (e: Exception) {
|
|
callback(false, false, null)
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun activateUsingRoot(context: Context, callback: (Boolean, Boolean, String?) -> Unit) {
|
|
Shell.getShell { shell ->
|
|
if(shell.isRoot) {
|
|
val result = Shell.cmd(ACTIVATE_DEVICE_OWNER_COMMAND).exec()
|
|
val output = result.out.joinToString("\n") + "\n" + result.err.joinToString("\n")
|
|
callback(true, result.isSuccess, output)
|
|
} else {
|
|
callback(true, false, context.getString(R.string.permission_denied))
|
|
}
|
|
}
|
|
}
|
|
|
|
@RequiresApi(28)
|
|
fun activateUsingDhizuku(context: Context, callback: (Boolean, Boolean, String?) -> Unit) {
|
|
fun doTransfer() {
|
|
try {
|
|
val dpm = binderWrapperDevicePolicyManager(context)
|
|
if(dpm == null) {
|
|
context.showOperationResultToast(false)
|
|
} else {
|
|
dpm.transferOwnership(
|
|
Dhizuku.getOwnerComponent(),
|
|
ComponentName(context, Receiver::class.java), PersistableBundle()
|
|
)
|
|
callback(true, true, null)
|
|
}
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
callback(true, false, null)
|
|
}
|
|
}
|
|
if(Dhizuku.init(context)) {
|
|
if(Dhizuku.isPermissionGranted()) {
|
|
doTransfer()
|
|
} else {
|
|
Dhizuku.requestPermission(object : DhizukuRequestPermissionListener() {
|
|
override fun onRequestPermission(grantResult: Int) {
|
|
if(grantResult == PackageManager.PERMISSION_GRANTED) doTransfer()
|
|
else callback(false, false, null)
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
callback(true, false, context.getString(R.string.failed_to_init_dhizuku))
|
|
}
|
|
}
|
|
|
|
fun activateDhizukuMode(context: Context, callback: (Boolean, Boolean, String?) -> Unit) {
|
|
fun onSucceed() {
|
|
SharedPrefs(context).dhizuku = true
|
|
callback(true, true, null)
|
|
}
|
|
if(Dhizuku.init(context)) {
|
|
if(Dhizuku.isPermissionGranted()) {
|
|
onSucceed()
|
|
} else {
|
|
Dhizuku.requestPermission(object : DhizukuRequestPermissionListener() {
|
|
override fun onRequestPermission(grantResult: Int) {
|
|
if(grantResult == PackageManager.PERMISSION_GRANTED) onSucceed()
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
callback(true, false, context.getString(R.string.failed_to_init_dhizuku))
|
|
}
|
|
}
|
|
|
|
const val ACTIVATE_DEVICE_OWNER_COMMAND = "dpm set-device-owner com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver"
|
|
|
|
@Serializable object LockScreenInfo
|
|
|
|
@RequiresApi(24)
|
|
@Composable
|
|
fun LockScreenInfoScreen(onNavigateUp: () -> Unit) {
|
|
val context = LocalContext.current
|
|
val dpm = context.getDPM()
|
|
val receiver = context.getReceiver()
|
|
val focusMgr = LocalFocusManager.current
|
|
var infoText by remember { mutableStateOf(dpm.deviceOwnerLockScreenInfo?.toString() ?: "") }
|
|
MyScaffold(R.string.lock_screen_info, onNavigateUp) {
|
|
OutlinedTextField(
|
|
value = infoText,
|
|
label = { Text(stringResource(R.string.lock_screen_info)) },
|
|
onValueChange = { infoText = it },
|
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
|
keyboardActions = KeyboardActions { focusMgr.clearFocus() },
|
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
|
|
)
|
|
Button(
|
|
onClick = {
|
|
focusMgr.clearFocus()
|
|
dpm.setDeviceOwnerLockScreenInfo(receiver,infoText)
|
|
context.showOperationResultToast(true)
|
|
},
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Text(text = stringResource(R.string.apply))
|
|
}
|
|
Button(
|
|
onClick = {
|
|
focusMgr.clearFocus()
|
|
dpm.setDeviceOwnerLockScreenInfo(receiver, null)
|
|
context.showOperationResultToast(true)
|
|
},
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Text(text = stringResource(R.string.reset))
|
|
}
|
|
Spacer(Modifier.padding(vertical = 10.dp))
|
|
Notes(R.string.info_lock_screen_info)
|
|
}
|
|
}
|
|
|
|
@Keep
|
|
@Suppress("InlinedApi")
|
|
enum class DelegatedScope(val id: String, @StringRes val string: Int, val requiresApi: Int = 0) {
|
|
AppRestrictions(DevicePolicyManager.DELEGATION_APP_RESTRICTIONS, R.string.manage_application_restrictions),
|
|
BlockUninstall(DevicePolicyManager.DELEGATION_BLOCK_UNINSTALL, R.string.block_uninstall),
|
|
CertInstall(DevicePolicyManager.DELEGATION_CERT_INSTALL, R.string.manage_certificates),
|
|
CertSelection(DevicePolicyManager.DELEGATION_CERT_SELECTION, R.string.select_keychain_certificates, 29),
|
|
EnableSystemApp(DevicePolicyManager.DELEGATION_ENABLE_SYSTEM_APP, R.string.enable_system_app),
|
|
InstallExistingPackage(DevicePolicyManager.DELEGATION_INSTALL_EXISTING_PACKAGE, R.string.install_existing_packages, 28),
|
|
KeepUninstalledPackages(DevicePolicyManager.DELEGATION_KEEP_UNINSTALLED_PACKAGES, R.string.manage_uninstalled_packages, 28),
|
|
NetworkLogging(DevicePolicyManager.DELEGATION_NETWORK_LOGGING, R.string.network_logging, 29),
|
|
PackageAccess(DevicePolicyManager.DELEGATION_PACKAGE_ACCESS, R.string.change_package_state),
|
|
PermissionGrant(DevicePolicyManager.DELEGATION_PERMISSION_GRANT, R.string.grant_permissions),
|
|
SecurityLogging(DevicePolicyManager.DELEGATION_SECURITY_LOGGING, R.string.security_logging, 31)
|
|
}
|
|
|
|
@Serializable object DelegatedAdmins
|
|
|
|
@RequiresApi(26)
|
|
@Composable
|
|
fun DelegatedAdminsScreen(onNavigateUp: () -> Unit, onNavigate: (AddDelegatedAdmin) -> Unit) {
|
|
val context = LocalContext.current
|
|
val dpm = context.getDPM()
|
|
val receiver = context.getReceiver()
|
|
val packages = remember { mutableStateMapOf<String, MutableList<DelegatedScope>>() }
|
|
fun refresh() {
|
|
val list = mutableMapOf<String, MutableList<DelegatedScope>>()
|
|
DelegatedScope.entries.forEach { ds ->
|
|
if(VERSION.SDK_INT >= ds.requiresApi) {
|
|
dpm.getDelegatePackages(receiver, ds.id)?.forEach { pkg ->
|
|
if(list[pkg] != null) {
|
|
list[pkg]!!.add(ds)
|
|
} else {
|
|
list[pkg] = mutableListOf(ds)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
packages.clear()
|
|
packages.putAll(list)
|
|
}
|
|
LaunchedEffect(Unit) { refresh() }
|
|
MyScaffold(R.string.delegated_admins, onNavigateUp, 0.dp) {
|
|
packages.forEach { (pkg, scopes) ->
|
|
Row(
|
|
Modifier.fillMaxWidth().padding(vertical = 8.dp).padding(start = 14.dp, end = 8.dp),
|
|
Arrangement.SpaceBetween
|
|
) {
|
|
Column {
|
|
Text(pkg, style = typography.titleMedium)
|
|
Text(
|
|
scopes.size.toString() + " " + stringResource(R.string.delegated_scope),
|
|
color = colorScheme.onSurfaceVariant, style = typography.bodyMedium
|
|
)
|
|
}
|
|
IconButton({ onNavigate(AddDelegatedAdmin(pkg, scopes)) }) {
|
|
Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
|
|
}
|
|
}
|
|
}
|
|
if(packages.isEmpty()) Text(
|
|
stringResource(R.string.none),
|
|
color = colorScheme.onSurfaceVariant,
|
|
modifier = Modifier.align(Alignment.CenterHorizontally).padding(vertical = 4.dp)
|
|
)
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable { onNavigate(AddDelegatedAdmin()) }
|
|
.padding(12.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(Icons.Default.Add, null, modifier = Modifier.padding(end = 12.dp))
|
|
Text(stringResource(R.string.add_delegated_admin), style = typography.titleMedium)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Serializable data class AddDelegatedAdmin(val pkg: String = "", val scopes: List<DelegatedScope> = emptyList())
|
|
|
|
@RequiresApi(26)
|
|
@Composable
|
|
fun AddDelegatedAdminScreen(data: AddDelegatedAdmin, onNavigateUp: () -> Unit) {
|
|
val updateMode = data.pkg.isNotEmpty()
|
|
val fm = LocalFocusManager.current
|
|
val context = LocalContext.current
|
|
var input by remember { mutableStateOf(data.pkg) }
|
|
val scopes = remember { mutableStateListOf(*data.scopes.toTypedArray()) }
|
|
val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result ->
|
|
result?.let { input = it }
|
|
}
|
|
MySmallTitleScaffold(if(updateMode) R.string.place_holder else R.string.add_delegated_admin, onNavigateUp, 0.dp) {
|
|
OutlinedTextField(
|
|
value = input, onValueChange = { input = it },
|
|
label = { Text(stringResource(R.string.package_name)) },
|
|
trailingIcon = {
|
|
if(!updateMode) IconButton({ choosePackage.launch(null) }) {
|
|
Icon(painterResource(R.drawable.list_fill0), null)
|
|
}
|
|
},
|
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
|
|
keyboardActions = KeyboardActions { fm.clearFocus() },
|
|
readOnly = updateMode,
|
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = HorizontalPadding)
|
|
)
|
|
DelegatedScope.entries.filter { VERSION.SDK_INT >= it.requiresApi }.forEach { scope ->
|
|
val checked = scope in scopes
|
|
Row(
|
|
Modifier.fillMaxWidth().clickable { if(!checked) scopes += scope else scopes -= scope }.padding(vertical = 4.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Checkbox(checked, { if(it) scopes += scope else scopes -= scope }, modifier = Modifier.padding(horizontal = 4.dp))
|
|
Column {
|
|
Text(stringResource(scope.string))
|
|
Text(scope.id, style = typography.bodyMedium, color = colorScheme.onSurfaceVariant)
|
|
}
|
|
}
|
|
}
|
|
Button(
|
|
onClick = {
|
|
context.getDPM().setDelegatedScopes(context.getReceiver(), input, scopes.map { it.id })
|
|
onNavigateUp()
|
|
},
|
|
modifier = Modifier.fillMaxWidth().padding(HorizontalPadding, vertical = 4.dp),
|
|
enabled = input.isNotBlank() && (!updateMode || scopes.toList() != data.scopes)
|
|
) {
|
|
Text(stringResource(if(updateMode) R.string.update else R.string.add))
|
|
}
|
|
if(updateMode) Button(
|
|
onClick = {
|
|
context.getDPM().setDelegatedScopes(context.getReceiver(), input, emptyList())
|
|
onNavigateUp()
|
|
},
|
|
modifier = Modifier.fillMaxWidth().padding(HorizontalPadding),
|
|
colors = ButtonDefaults.buttonColors(colorScheme.error, colorScheme.onError)
|
|
) {
|
|
Text(stringResource(R.string.delete))
|
|
}
|
|
}
|
|
}
|
|
|
|
@Serializable object DeviceInfo
|
|
|
|
@Composable
|
|
fun DeviceInfoScreen(onNavigateUp: () -> Unit) {
|
|
val context = LocalContext.current
|
|
val dpm = context.getDPM()
|
|
val privilege by myPrivilege.collectAsStateWithLifecycle()
|
|
var dialog by remember { mutableIntStateOf(0) }
|
|
MyScaffold(R.string.device_info, onNavigateUp, 0.dp) {
|
|
if(VERSION.SDK_INT>=34 && (privilege.device || privilege.org)) {
|
|
InfoItem(R.string.financed_device, dpm.isDeviceFinanced.yesOrNo)
|
|
}
|
|
if(VERSION.SDK_INT >= 33) {
|
|
val dpmRole = dpm.devicePolicyManagementRoleHolderPackage
|
|
InfoItem(R.string.dpmrh, dpmRole ?: stringResource(R.string.none))
|
|
}
|
|
val encryptionStatus = mutableMapOf(
|
|
DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE to R.string.es_inactive,
|
|
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE to R.string.es_active,
|
|
DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED to R.string.es_unsupported
|
|
)
|
|
if(VERSION.SDK_INT >= 23) { encryptionStatus[DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY] = R.string.es_active_default_key }
|
|
if(VERSION.SDK_INT >= 24) { encryptionStatus[DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER] = R.string.es_active_per_user }
|
|
InfoItem(R.string.encryption_status, encryptionStatus[dpm.storageEncryptionStatus] ?: R.string.unknown)
|
|
if(VERSION.SDK_INT >= 28) {
|
|
InfoItem(R.string.support_device_id_attestation, dpm.isDeviceIdAttestationSupported.yesOrNo, true) { dialog = 1 }
|
|
}
|
|
if (VERSION.SDK_INT >= 30) {
|
|
InfoItem(R.string.support_unique_device_attestation, dpm.isUniqueDeviceAttestationSupported.yesOrNo, true) { dialog = 2 }
|
|
}
|
|
val adminList = dpm.activeAdmins
|
|
if(adminList != null) {
|
|
InfoItem(R.string.activated_device_admin, adminList.joinToString("\n") { it.flattenToShortString() })
|
|
}
|
|
}
|
|
if(dialog != 0) AlertDialog(
|
|
text = { Text(stringResource(if(dialog == 1) R.string.info_device_id_attestation else R.string.info_unique_device_attestation)) },
|
|
confirmButton = { TextButton(onClick = { dialog = 0 }) { Text(stringResource(R.string.confirm)) } },
|
|
onDismissRequest = { dialog = 0 }
|
|
)
|
|
}
|
|
|
|
@Serializable object SupportMessage
|
|
|
|
@RequiresApi(24)
|
|
@Composable
|
|
fun SupportMessageScreen(onNavigateUp: () -> Unit) {
|
|
val context = LocalContext.current
|
|
val dpm = context.getDPM()
|
|
val receiver = context.getReceiver()
|
|
var shortMsg by remember { mutableStateOf("") }
|
|
var longMsg by remember { mutableStateOf("") }
|
|
val refreshMsg = {
|
|
shortMsg = dpm.getShortSupportMessage(receiver)?.toString() ?: ""
|
|
longMsg = dpm.getLongSupportMessage(receiver)?.toString() ?: ""
|
|
}
|
|
LaunchedEffect(Unit) { refreshMsg() }
|
|
MyScaffold(R.string.support_messages, onNavigateUp) {
|
|
OutlinedTextField(
|
|
value = shortMsg,
|
|
label = { Text(stringResource(R.string.short_support_msg)) },
|
|
onValueChange = { shortMsg = it },
|
|
minLines = 2,
|
|
modifier = Modifier.fillMaxWidth().padding(bottom = 2.dp)
|
|
)
|
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
|
Button(
|
|
onClick = {
|
|
dpm.setShortSupportMessage(receiver, shortMsg)
|
|
refreshMsg()
|
|
context.showOperationResultToast(true)
|
|
},
|
|
modifier = Modifier.fillMaxWidth(0.49F)
|
|
) {
|
|
Text(text = stringResource(R.string.apply))
|
|
}
|
|
Button(
|
|
onClick = {
|
|
dpm.setShortSupportMessage(receiver, null)
|
|
refreshMsg()
|
|
context.showOperationResultToast(true)
|
|
},
|
|
modifier = Modifier.fillMaxWidth(0.96F)
|
|
) {
|
|
Text(text = stringResource(R.string.reset))
|
|
}
|
|
}
|
|
Notes(R.string.info_short_support_message)
|
|
Spacer(Modifier.padding(vertical = 8.dp))
|
|
OutlinedTextField(
|
|
value = longMsg,
|
|
label = { Text(stringResource(R.string.long_support_msg)) },
|
|
onValueChange = { longMsg = it },
|
|
minLines = 3,
|
|
modifier = Modifier.fillMaxWidth().padding(bottom = 2.dp)
|
|
)
|
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
|
Button(
|
|
onClick = {
|
|
dpm.setLongSupportMessage(receiver, longMsg)
|
|
refreshMsg()
|
|
context.showOperationResultToast(true)
|
|
},
|
|
modifier = Modifier.fillMaxWidth(0.49F)
|
|
) {
|
|
Text(text = stringResource(R.string.apply))
|
|
}
|
|
Button(
|
|
onClick = {
|
|
dpm.setLongSupportMessage(receiver, null)
|
|
refreshMsg()
|
|
context.showOperationResultToast(true)
|
|
},
|
|
modifier = Modifier.fillMaxWidth(0.96F)
|
|
) {
|
|
Text(text = stringResource(R.string.reset))
|
|
}
|
|
}
|
|
Notes(R.string.info_long_support_message)
|
|
}
|
|
}
|
|
|
|
@Serializable object TransferOwnership
|
|
|
|
@RequiresApi(28)
|
|
@Composable
|
|
fun TransferOwnershipScreen(onNavigateUp: () -> Unit, onTransferred: () -> Unit) {
|
|
val context = LocalContext.current
|
|
val privilege by myPrivilege.collectAsStateWithLifecycle()
|
|
val focusMgr = LocalFocusManager.current
|
|
var input by remember { mutableStateOf("") }
|
|
val componentName = ComponentName.unflattenFromString(input)
|
|
var dialog by remember { mutableStateOf(false) }
|
|
MyScaffold(R.string.transfer_ownership, onNavigateUp) {
|
|
OutlinedTextField(
|
|
value = input, onValueChange = { input = it }, label = { Text(stringResource(R.string.target_component_name)) },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
isError = input != "" && componentName == null,
|
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
|
keyboardActions = KeyboardActions(onNext = { focusMgr.clearFocus() })
|
|
)
|
|
Spacer(Modifier.padding(vertical = 5.dp))
|
|
Button(
|
|
onClick = { dialog = true },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
enabled = componentName != null
|
|
) {
|
|
Text(stringResource(R.string.transfer))
|
|
}
|
|
Spacer(Modifier.padding(vertical = 10.dp))
|
|
Notes(R.string.info_transfer_ownership)
|
|
}
|
|
if(dialog) AlertDialog(
|
|
text = {
|
|
Text(stringResource(
|
|
R.string.transfer_ownership_warning,
|
|
stringResource(if(privilege.device) R.string.device_owner else R.string.profile_owner),
|
|
ComponentName.unflattenFromString(input)!!.packageName
|
|
))
|
|
},
|
|
confirmButton = {
|
|
TextButton(
|
|
onClick = {
|
|
val dpm = context.getDPM()
|
|
val receiver = context.getReceiver()
|
|
try {
|
|
dpm.transferOwnership(receiver, componentName!!, null)
|
|
context.showOperationResultToast(true)
|
|
updatePrivilege(context)
|
|
dialog = false
|
|
onTransferred()
|
|
} catch(e: Exception) {
|
|
e.printStackTrace()
|
|
context.showOperationResultToast(false)
|
|
}
|
|
},
|
|
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
|
|
) {
|
|
Text(stringResource(R.string.confirm))
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = { dialog = false }) {
|
|
Text(stringResource(R.string.cancel))
|
|
}
|
|
},
|
|
onDismissRequest = { dialog = false }
|
|
)
|
|
}
|