Force activation using root (#114)

Fix some UI bugs, close #121
This commit is contained in:
BinTianqi
2025-05-05 12:49:18 +08:00
parent 16a57206ea
commit 2ce92999a4
12 changed files with 156 additions and 30 deletions

View File

@@ -534,7 +534,6 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) {
}
) {
Column(Modifier.fillMaxSize().padding(it).verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 6.dp))
if(privilege.device || privilege.profile) {
HomePageItem(R.string.system, R.drawable.android_fill0) { onNavigate(SystemManager) }
HomePageItem(R.string.network, R.drawable.wifi_fill0) { onNavigate(Network) }

View File

@@ -47,6 +47,7 @@ import androidx.compose.runtime.mutableFloatStateOf
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
@@ -107,9 +108,9 @@ fun AppChooserScreen(params: ApplicationsList, onChoosePackage: (String?) -> Uni
val coroutine = rememberCoroutineScope()
val context = LocalContext.current
var progress by remember { mutableFloatStateOf(1F) }
var system by remember { mutableStateOf(false) }
var query by remember { mutableStateOf("") }
var searchMode by remember { mutableStateOf(false) }
var system by rememberSaveable { mutableStateOf(false) }
var query by rememberSaveable { mutableStateOf("") }
var searchMode by rememberSaveable { mutableStateOf(false) }
val filteredPackages = packages.filter {
system == (it.flags and ApplicationInfo.FLAG_SYSTEM != 0) &&
(query.isEmpty() || (searchInString(query, it.label) || searchInString(query, it.name)))
@@ -189,7 +190,10 @@ fun AppChooserScreen(params: ApplicationsList, onChoosePackage: (String?) -> Uni
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onChoosePackage(it.name) }
.clickable {
focusMgr.clearFocus()
onChoosePackage(it.name)
}
.padding(horizontal = 8.dp, vertical = 10.dp)
.animateItem()
) {

View File

@@ -23,7 +23,7 @@ class SharedPrefs(context: Context) {
var biometricsUnlock by BooleanSharedPref("lock.biometrics")
var lockWhenLeaving by BooleanSharedPref("lock.onleave")
var applicationsListView by BooleanSharedPref("applications.list_view", true)
var shortcuts by BooleanSharedPref("shortcuts", true)
var shortcuts by BooleanSharedPref("shortcuts", false)
}
private class BooleanSharedPref(val key: String, val defValue: Boolean = false): ReadWriteProperty<SharedPrefs, Boolean> {

View File

@@ -115,7 +115,7 @@ fun LazyItemScope.ApplicationItem(info: AppInfo, onClear: () -> Unit) {
Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp).animateItem(),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(Modifier.fillMaxWidth(0.87F), verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberDrawablePainter(info.icon), contentDescription = null,
modifier = Modifier.padding(start = 12.dp, end = 18.dp).size(30.dp)

View File

@@ -1,10 +1,19 @@
package com.bintianqi.owndroid.dpm
import android.annotation.SuppressLint
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Build.VERSION
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
import android.os.PersistableBundle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.annotation.Keep
@@ -15,6 +24,8 @@ 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.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -37,7 +48,6 @@ 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
@@ -71,7 +81,7 @@ 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.core.os.bundleOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.ChoosePackageContract
import com.bintianqi.owndroid.HorizontalPadding
@@ -82,6 +92,7 @@ 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.CircularProgressDialog
import com.bintianqi.owndroid.ui.InfoItem
import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.MySmallTitleScaffold
@@ -93,12 +104,16 @@ import com.bintianqi.owndroid.yesOrNo
import com.rosan.dhizuku.api.Dhizuku
import com.rosan.dhizuku.api.DhizukuRequestPermissionListener
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ipc.RootService
import dalvik.system.DexClassLoader
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import java.lang.invoke.MethodHandles
import java.lang.reflect.Proxy
@Serializable data class WorkModes(val canNavigateUp: Boolean)
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun WorkModesScreen(
params: WorkModes, onNavigateUp: () -> Unit, onActivate: () -> Unit, onDeactivate: () -> Unit,
@@ -107,7 +122,7 @@ fun WorkModesScreen(
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 */
/** 0: none, 1: device owner, 2: circular progress indicator, 3: result, 4: deactivate, 5: command, 6: force activating warning */
var dialog by remember { mutableIntStateOf(0) }
Scaffold(
topBar = {
@@ -160,6 +175,7 @@ fun WorkModesScreen(
)
}
) { paddingValues ->
var navigateUpOnSucceed by remember { mutableStateOf(true) }
var operationSucceed by remember { mutableStateOf(false) }
var resultText by remember { mutableStateOf("") }
fun handleResult(succeeded: Boolean, activateSucceeded: Boolean, output: String?) {
@@ -253,7 +269,7 @@ fun WorkModesScreen(
if(dialog == 1) AlertDialog(
title = { Text(stringResource(R.string.activate_method)) },
text = {
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
FlowRow(Modifier.fillMaxWidth()) {
if(!privilege.dhizuku) Button({
dialog = 2
coroutine.launch {
@@ -262,19 +278,28 @@ fun WorkModesScreen(
}) {
Text(stringResource(R.string.shizuku))
}
Spacer(Modifier.padding(horizontal = 2.dp))
if(!privilege.dhizuku) Button({
dialog = 2
activateUsingRoot(context, ::handleResult)
}) {
Text("Root")
}
Spacer(Modifier.padding(horizontal = 2.dp))
if(VERSION.SDK_INT >= 28) Button({
dialog = 2
activateUsingDhizuku(context, ::handleResult)
}) {
Text(stringResource(R.string.dhizuku))
}
Spacer(Modifier.padding(horizontal = 2.dp))
Button({ dialog = 5 }) { Text(stringResource(R.string.adb_command)) }
Spacer(Modifier.padding(horizontal = 2.dp))
if (VERSION.SDK_INT == 35) Button({
dialog = 6
}) {
Text(stringResource(R.string.root_force_activate))
}
}
},
confirmButton = {
@@ -282,9 +307,7 @@ fun WorkModesScreen(
},
onDismissRequest = { dialog = 0 }
)
if(dialog == 2) Dialog({}) {
CircularProgressIndicator()
}
if(dialog == 2) CircularProgressDialog { }
if(dialog == 3) AlertDialog(
title = { Text(stringResource(if(operationSucceed) R.string.succeeded else R.string.failed)) },
text = {
@@ -295,15 +318,12 @@ fun WorkModesScreen(
confirmButton = {
TextButton({
dialog = 0
if(operationSucceed && !params.canNavigateUp) onActivate()
if(navigateUpOnSucceed && operationSucceed && !params.canNavigateUp) onActivate()
}) {
Text(stringResource(R.string.confirm))
}
},
onDismissRequest = {
dialog = 0
if(operationSucceed && !params.canNavigateUp) onActivate()
}
onDismissRequest = {}
)
if(dialog == 4) AlertDialog(
title = { Text(stringResource(R.string.deactivate)) },
@@ -345,6 +365,23 @@ fun WorkModesScreen(
},
onDismissRequest = { dialog = 0 }
)
if (dialog == 6) AlertDialog(
title = { Text(stringResource(R.string.warning)) },
text = { Text(stringResource(R.string.info_force_activate)) },
confirmButton = {
TextButton({
dialog = 2
navigateUpOnSucceed = false
forceActivateUsingRoot(context, ::handleResult)
}) {
Text(stringResource(R.string.continue_str))
}
},
dismissButton = {
TextButton({ dialog = 0 }) { Text(stringResource(R.string.cancel)) }
},
onDismissRequest = { dialog = 0 }
)
}
}
@@ -414,6 +451,75 @@ fun activateUsingDhizuku(context: Context, callback: (Boolean, Boolean, String?)
}
}
fun forceActivateUsingRoot(context: Context, callback: (Boolean, Boolean, String?) -> Unit) {
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val handler = Handler(Looper.getMainLooper()) { msg ->
RootService.unbind(this)
val data = msg.data
val output = if (data.getBoolean("succeed")) context.getString(R.string.please_reboot) else null
callback(!data.getBoolean("error"), data.getBoolean("succeed"), output)
return@Handler true
}
val msg = Message()
msg.replyTo = Messenger(handler)
Messenger(service).send(msg)
}
override fun onServiceDisconnected(name: ComponentName?) {}
}
val intent = Intent(context, ForceActivateService::class.java)
RootService.bind(intent, connection)
}
@RequiresApi(26)
class ForceActivateService(): RootService() {
override fun onBind(intent: Intent): IBinder = messenger.binder
val handler = Handler(Looper.getMainLooper()) { msg ->
val replyMessage = Message()
try {
replyMessage.data = activateDeviceOwnerAsRoot(getReceiver()).apply { putBoolean("error", false) }
} catch (e: Exception) {
e.printStackTrace()
replyMessage.data = bundleOf("error" to true, "succeed" to false)
}
msg.replyTo.send(replyMessage)
return@Handler false
}
val messenger = Messenger(handler)
}
@SuppressLint("PrivateApi")
@RequiresApi(26)
fun activateDeviceOwnerAsRoot(cn: ComponentName): Bundle {
val dcl = DexClassLoader(
"/system/framework/services.jar", "/data/local/tmp", null, ClassLoader.getSystemClassLoader()
)
val ppp = dcl.loadClass("com.android.server.devicepolicy.PolicyPathProvider")
val pppProxy = Proxy.newProxyInstance(ppp.classLoader, arrayOf(ppp)) { obj, method, args ->
method.isAccessible = true
val mh = MethodHandles.lookup().`in`(ppp).unreflectSpecial(method, ppp).bindTo(obj)
return@newProxyInstance if (args == null) {
mh.invokeWithArguments()
} else {
mh.invokeWithArguments(*args)
}
}
val od = dcl.loadClass("com.android.server.devicepolicy.OwnersData")
val odIns = od.getConstructor(ppp).apply { isAccessible = true }.newInstance(pppProxy)
val oi = dcl.loadClass("com.android.server.devicepolicy.OwnersData\$OwnerInfo")
val oiIns = oi.constructors[0].apply { isAccessible = true }.newInstance(cn, null, null, true)
od.getField("mDeviceOwner").apply { isAccessible = true }.set(odIns, oiIns)
od.getField("mDeviceOwnerUserId").apply { isAccessible = true }.set(odIns, 0)
val setDoResult = od.getMethod("writeDeviceOwner").apply { isAccessible = true }.invoke(odIns) as Boolean
return if (setDoResult) {
val proc = Runtime.getRuntime().exec("dpm set-active-admin ${cn.flattenToShortString()}")
proc.waitFor()
bundleOf("succeed" to true)
} else {
bundleOf("succeed" to false)
}
}
fun activateDhizukuMode(context: Context, callback: (Boolean, Boolean, String?) -> Unit) {
fun onSucceed() {
SharedPrefs(context).dhizuku = true

View File

@@ -33,7 +33,6 @@ import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
@@ -62,14 +61,13 @@ 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.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.myPrivilege
import com.bintianqi.owndroid.parseTimestamp
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CircularProgressDialog
import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem
import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.InfoItem
@@ -393,9 +391,7 @@ fun CreateUserScreen(onNavigateUp: () -> Unit) {
},
onDismissRequest = { createdUserSerialNumber = -1 }
)
if(creating) Dialog({}, DialogProperties(false, false)) {
CircularProgressIndicator()
}
if(creating) CircularProgressDialog { }
}
}

View File

@@ -24,7 +24,9 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -53,6 +55,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.zhCN
@@ -378,3 +381,12 @@ fun ErrorDialog(message: String?, onDismiss: () -> Unit) {
onDismissRequest = onDismiss
)
}
@Composable
fun CircularProgressDialog(onDismiss: () -> Unit) {
Dialog(onDismiss) {
Card {
CircularProgressIndicator(Modifier.padding(16.dp))
}
}
}