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

@@ -95,6 +95,7 @@ dependencies {
implementation(libs.androidx.fragment)
implementation(libs.hiddenApiBypass)
implementation(libs.libsu)
implementation(libs.libsu.service)
implementation(libs.serialization)
implementation(kotlin("reflect"))
}

View File

@@ -1,6 +1,5 @@
package com.bintianqi.owndroid;
import android.accounts.Account;
import android.os.Bundle;
interface IUserService {

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))
}
}
}

View File

@@ -67,6 +67,7 @@
<string name="features">功能</string>
<string name="default_str">默认</string>
<string name="timeout">超时</string>
<string name="continue_str">继续</string>
<!--Permissions-->
<string name="profile_owner">Profile owner</string>
@@ -685,11 +686,14 @@
<string name="info_max_failed_password_other_user">如果设置为大于零的值,将会在输入一定次数的错误的锁屏密码后清除当前用户的所有数据</string>
<string name="info_password_history_length">设置后用户将无法输入与历史记录中任何密码相同的新密码。当前密码将保留直到用户设置新密码为止因此更改不会立即生效。值为0表示不做限制。</string>
<string name="info_required_strong_auth_timeout">如果用户在这段时间内没有使用强认证密码、PIN或图案解锁设备则要求使用强认证解锁设备。值为0表示OwnDroid不参与控制超时。一般来说最少1小时最多72小时。</string>
<string name="info_deactivate">OwnDroid will lost its privilege</string>
<string name="info_deactivate">OwnDroid将会丢失特权</string>
<string name="info_force_activate">强行激活可能无法在你的设备上使用,它甚至有可能会损坏你的设备,你应该仅在其他方法无法使用时尝试此方法。</string>
<string name="choose_work_mode">选择一个工作模式</string>
<string name="recommended">推荐</string>
<string name="activate_method">激活方法</string>
<string name="adb_command">ADB命令</string>
<string name="owndroid_warning">此应用使用Device owner和Profile owner特权。这些特权十分危险请谨慎使用。如果操作不当可能会造成严重损失。开发者将不会对此负责。</string>
<string name="root_force_activate">Root (强行激活)</string>
<string name="owndroid_warning">此应用使用Device owner或Profile owner特权。这些特权十分危险请谨慎使用。如果操作不当可能会造成严重损失。开发者将不会对此负责。</string>
<string name="please_reboot">请重启你的设备</string>
</resources>

View File

@@ -72,6 +72,7 @@
<string name="features">Features</string>
<string name="default_str">Default</string>
<string name="timeout">Timeout</string>
<string name="continue_str">Continue</string>
<!--Permissions-->
<string name="profile_owner">Profile owner</string>
@@ -720,10 +721,13 @@
<string name="info_password_history_length">After setting this, the user will not be able to enter a new password that is the same as any password in the history. Note that the current password will remain until the user has set a new one, so the change does not take place immediately.\nA value of 0 means there is no restriction.</string>
<string name="info_required_strong_auth_timeout">Determine for how long the user will be able to use secondary, non strong auth for authentication, since last strong method authentication (password, pin or pattern) was used. After the returned timeout the user is required to use strong authentication method.\nA value of 0 means the admin is not participating in controlling the timeout. The minimum and maximum timeouts are platform-defined and are typically 1 hour and 72 hours, respectively.</string>
<string name="info_deactivate">OwnDroid will lost its privilege</string>
<string name="info_force_activate">Force activation may not work and it may damage your device. This method should only be used when you can\'t use other methods.</string>
<string name="choose_work_mode">Choose a work mode</string>
<string name="recommended">Recommended</string>
<string name="activate_method">Activate method</string>
<string name="adb_command">ADB command</string>
<string name="owndroid_warning">This app uses Device owner and Profile owner privileges. These privileges are extremely dangerous, please use them with caution. If used improperly, they may result in severe losses. The developers will not be responsible for this.</string>
<string name="root_force_activate">Root (force activate)</string>
<string name="owndroid_warning">This app uses Device owner or Profile owner privileges. These privileges are extremely dangerous, please use them with caution. If used improperly, they may result in severe losses. The developers will not be responsible for this.</string>
<string name="please_reboot">Please reboot your device</string>
</resources>