diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1b571d6..fa093e6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -92,6 +92,7 @@ dependencies { implementation(libs.shizuku.provider) implementation(libs.shizuku.api) implementation(libs.dhizuku.api) + implementation(libs.dhizuku.server.api) implementation(libs.androidx.fragment) implementation(libs.hiddenApiBypass) implementation(libs.libsu) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fe428b9..b19edd8 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,3 +20,7 @@ # If you keep the line number information, uncomment this to # hide the original source file name. # -renamesourcefileattribute SourceFile + +-dontwarn android.app.ActivityThread +-dontwarn android.app.ContextImpl +-dontwarn android.app.LoadedApk diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d7a8975..7f3188f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -68,6 +68,17 @@ android:permission="com.bintianqi.owndroid.MyPermission" android:exported="true" android:theme="@android:style/Theme.NoDisplay" /> + + + + + + + + diff --git a/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt b/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt new file mode 100644 index 0000000..293ae3e --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt @@ -0,0 +1,147 @@ +package com.bintianqi.owndroid + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.accompanist.drawablepainter.rememberDrawablePainter +import com.rosan.dhizuku.aidl.IDhizukuClient +import com.rosan.dhizuku.aidl.IDhizukuRequestPermissionListener +import com.rosan.dhizuku.server_api.DhizukuProvider +import com.rosan.dhizuku.server_api.DhizukuService +import com.rosan.dhizuku.shared.DhizukuVariables +import kotlinx.coroutines.delay +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +private const val TAG = "DhizukuServer" + +const val DHIZUKU_CLIENTS_FILE = "dhizuku_clients.json" + +class MyDhizukuProvider(): DhizukuProvider() { + override fun onCreateService(client: IDhizukuClient): DhizukuService? { + Log.d(TAG, "Creating MyDhizukuService") + return if (SharedPrefs(context!!).dhizukuServer) MyDhizukuService(context!!, MyAdminComponent, client) else null + } +} + +class MyDhizukuService(context: Context, admin: ComponentName, client: IDhizukuClient) : + DhizukuService(context, admin, client) { + override fun checkCallingPermission(func: String?, callingUid: Int, callingPid: Int): Boolean { + if (!SharedPrefs(mContext).dhizukuServer) return false + val pm = mContext.packageManager + val packageInfo = pm.getPackageInfo( + pm.getNameForUid(callingUid) ?: return false, + if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES else PackageManager.GET_SIGNATURES + ) + val file = mContext.filesDir.resolve(DHIZUKU_CLIENTS_FILE) + val clients = Json.decodeFromString>(file.readText()) + val signature = getPackageSignature(packageInfo) + val hasPermission = DhizukuClientInfo(callingUid, signature, true) in clients + Log.d(TAG, "UID $callingUid, PID $callingPid, has permission: $hasPermission") + return hasPermission + } + + override fun getVersionName() = "1.0" +} + +class DhizukuActivity : ComponentActivity() { + @OptIn(ExperimentalStdlibApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (!SharedPrefs(this).dhizukuServer) { + finish() + return + } + val bundle = intent.extras ?: return + val uid = bundle.getInt(DhizukuVariables.PARAM_CLIENT_UID, -1) + if (uid == -1) return + val binder = bundle.getBinder(DhizukuVariables.PARAM_CLIENT_REQUEST_PERMISSION_BINDER) ?: return + val listener = IDhizukuRequestPermissionListener.Stub.asInterface(binder) + val packageName = packageManager.getPackagesForUid(uid)?.first() ?: return + val packageInfo = packageManager.getPackageInfo( + packageName, + if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES else PackageManager.GET_SIGNATURES + ) + val appInfo = packageManager.getApplicationInfo(packageName, 0) + val icon = appInfo.loadIcon(packageManager) + val label = appInfo.loadLabel(packageManager).toString() + fun close(grantPermission: Boolean) { + val file = filesDir.resolve(DHIZUKU_CLIENTS_FILE) + val clients = Json.decodeFromString>(file.readText()) + val index = clients.indexOfFirst { it.uid == uid } + val clientInfo = DhizukuClientInfo(uid, getPackageSignature(packageInfo), grantPermission) + if (index == -1) clients += clientInfo + else clients[index] = clientInfo + file.writeText(Json.encodeToString(clients)) + finish() + listener.onRequestPermission( + if (grantPermission) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED + ) + } + setContent { + AlertDialog( + icon = { + Image(rememberDrawablePainter(icon), null, Modifier.size(35.dp)) + }, + title = { + Text(stringResource(R.string.request_permission)) + }, + text = { + Text("$label\n($packageName)") + }, + confirmButton = { + var time by remember { mutableIntStateOf(3) } + LaunchedEffect(Unit) { + (1..3).forEach { + delay(1000) + time -= 1 + } + } + TextButton({ + close(true) + }, enabled = time == 0) { + val append = if (time > 0) " (${time}s)" else "" + Text(stringResource(R.string.allow) + append) + } + }, + dismissButton = { + TextButton({ + close(false) + }) { + Text(stringResource(R.string.reject)) + } + }, + onDismissRequest = { + finish() + } + ) + } + } +} + + +@Serializable +data class DhizukuClientInfo( + val uid: Int, + val signature: String?, + val allow: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 96173e6..324c6b0 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -107,6 +107,8 @@ import com.bintianqi.owndroid.dpm.DeleteWorkProfile import com.bintianqi.owndroid.dpm.DeleteWorkProfileScreen import com.bintianqi.owndroid.dpm.DeviceInfo import com.bintianqi.owndroid.dpm.DeviceInfoScreen +import com.bintianqi.owndroid.dpm.DhizukuServerSettings +import com.bintianqi.owndroid.dpm.DhizukuServerSettingsScreen import com.bintianqi.owndroid.dpm.DisableAccountManagement import com.bintianqi.owndroid.dpm.DisableAccountManagementScreen import com.bintianqi.owndroid.dpm.DisableMeteredData @@ -324,6 +326,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { } }, ::navigate) } + composable { DhizukuServerSettingsScreen(::navigateUp) } composable { DelegatedAdminsScreen(::navigateUp, ::navigate) } composable{ AddDelegatedAdminScreen(it.toRoute(), ::navigateUp) } diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index 30b2017..2d7c43e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -64,7 +64,7 @@ class Receiver : DeviceAdminReceiver() { super.onSecurityLogsAvailable(context, intent) if(VERSION.SDK_INT >= 24) { CoroutineScope(Dispatchers.IO).launch { - val events = getManager(context).retrieveSecurityLogs(ComponentName(context, this@Receiver::class.java)) ?: return@launch + val events = getManager(context).retrieveSecurityLogs(MyAdminComponent) ?: return@launch val file = context.filesDir.resolve("SecurityLogs.json") val fileExists = file.exists() file.outputStream().use { diff --git a/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt b/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt index e641e26..f75a5dc 100644 --- a/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt +++ b/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt @@ -23,7 +23,8 @@ 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", false) + var shortcuts by BooleanSharedPref("shortcuts") + var dhizukuServer by BooleanSharedPref("dhizuku_server") } private class BooleanSharedPref(val key: String, val defValue: Boolean = false): ReadWriteProperty { diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index d999f10..ff080fe 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -2,8 +2,10 @@ package com.bintianqi.owndroid import android.content.ClipData import android.content.ClipboardManager +import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.pm.PackageInfo import android.net.Uri import android.os.Build import android.os.Bundle @@ -131,3 +133,13 @@ fun String.hash(): String { val md = MessageDigest.getInstance("SHA-256") return md.digest(this.encodeToByteArray()).toHexString() } + +val MyAdminComponent = ComponentName.unflattenFromString("com.bintianqi.owndroid/.Receiver")!! + + +@OptIn(ExperimentalStdlibApi::class) +fun getPackageSignature(info: PackageInfo): String? { + val signatures = if (Build.VERSION.SDK_INT >= 28) info.signingInfo?.apkContentsSigners else info.signatures + return signatures?.firstOrNull()?.toByteArray() + ?.let { MessageDigest.getInstance("SHA-256").digest(it) }?.toHexString() +} diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index da61b35..bab9bb3 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -24,11 +24,11 @@ import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.toBitmap +import com.bintianqi.owndroid.MyAdminComponent import com.bintianqi.owndroid.R import com.bintianqi.owndroid.Receiver import com.bintianqi.owndroid.SharedPrefs import com.bintianqi.owndroid.ShortcutsReceiverActivity -import com.bintianqi.owndroid.backToHomeStateFlow import com.bintianqi.owndroid.createShortcuts import com.bintianqi.owndroid.myPrivilege import com.rosan.dhizuku.api.Dhizuku @@ -130,7 +130,7 @@ fun Context.getReceiver(): ComponentName { return if(SharedPrefs(this).dhizuku) { Dhizuku.getOwnerComponent() } else { - ComponentName(this, Receiver::class.java) + MyAdminComponent } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index 0f885e9..bb00140 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -19,6 +19,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.annotation.Keep import androidx.annotation.RequiresApi import androidx.annotation.StringRes +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -31,7 +32,10 @@ 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.layout.size +import androidx.compose.foundation.lazy.itemsIndexed 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 @@ -57,6 +61,7 @@ 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.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -84,23 +89,28 @@ import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bintianqi.owndroid.ChoosePackageContract +import com.bintianqi.owndroid.DHIZUKU_CLIENTS_FILE +import com.bintianqi.owndroid.DhizukuClientInfo import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.IUserService +import com.bintianqi.owndroid.MyAdminComponent 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.CircularProgressDialog import com.bintianqi.owndroid.ui.InfoItem +import com.bintianqi.owndroid.ui.MyLazyScaffold import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.MySmallTitleScaffold import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.Notes +import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.updatePrivilege import com.bintianqi.owndroid.useShizuku import com.bintianqi.owndroid.yesOrNo +import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.rosan.dhizuku.api.Dhizuku import com.rosan.dhizuku.api.DhizukuRequestPermissionListener import com.topjohnwu.superuser.Shell @@ -108,6 +118,8 @@ import com.topjohnwu.superuser.ipc.RootService import dalvik.system.DexClassLoader import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.lang.invoke.MethodHandles import java.lang.reflect.Proxy @@ -258,6 +270,14 @@ fun WorkModesScreen( tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground ) } + if ((privilege.device || privilege.profile) && !privilege.dhizuku) Row( + Modifier.padding(top = 20.dp).fillMaxWidth().clickable { onNavigate(DhizukuServerSettings) }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon(painterResource(R.drawable.dhizuku_icon), null, Modifier.padding(8.dp).size(28.dp)) + Text(stringResource(R.string.dhizuku_server), style = typography.titleLarge) + } + 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) @@ -338,7 +358,7 @@ fun WorkModesScreen( if(privilege.device) { dpm.clearDeviceOwnerApp(context.packageName) } else if(VERSION.SDK_INT >= 24) { - dpm.clearProfileOwner(ComponentName(context, Receiver::class.java)) + dpm.clearProfileOwner(MyAdminComponent) } } dialog = 0 @@ -424,10 +444,7 @@ fun activateUsingDhizuku(context: Context, callback: (Boolean, Boolean, String?) if(dpm == null) { context.showOperationResultToast(false) } else { - dpm.transferOwnership( - Dhizuku.getOwnerComponent(), - ComponentName(context, Receiver::class.java), PersistableBundle() - ) + dpm.transferOwnership(Dhizuku.getOwnerComponent(), MyAdminComponent, PersistableBundle()) callback(true, true, null) } } catch (e: Exception) { @@ -542,6 +559,65 @@ fun activateDhizukuMode(context: Context, callback: (Boolean, Boolean, String?) const val ACTIVATE_DEVICE_OWNER_COMMAND = "dpm set-device-owner com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver" +@Serializable object DhizukuServerSettings + +@Composable +fun DhizukuServerSettingsScreen(onNavigateUp: () -> Unit) { + val context = LocalContext.current + val pm = context.packageManager + val sp = SharedPrefs(context) + val file = context.filesDir.resolve(DHIZUKU_CLIENTS_FILE) + var enabled by remember { mutableStateOf(sp.dhizukuServer) } + val clients = remember { mutableStateListOf() } + fun changeEnableState(status: Boolean) { + enabled = status + sp.dhizukuServer = status + } + fun writeList() { + file.writeText(Json.encodeToString(clients)) + } + LaunchedEffect(Unit) { + if (!file.exists()) file.writeText("[]") + } + LaunchedEffect(enabled) { + if (enabled) { + clients.clear() + clients.addAll(Json.decodeFromString>(file.readText())) + } + } + MyLazyScaffold(R.string.dhizuku_server, onNavigateUp) { + item { + SwitchItem(R.string.enable, getState = { sp.dhizukuServer }, onCheckedChange = ::changeEnableState) + Spacer(Modifier.padding(vertical = 8.dp)) + } + if (enabled) itemsIndexed(clients) { index, client -> + val name = pm.getNameForUid(client.uid) + if (name == null) { + clients.dropWhile { it.uid == client.uid } + writeList() + } else { + val info = pm.getApplicationInfo(name, 0) + Row( + Modifier + .fillMaxWidth().padding(8.dp) + .background(colorScheme.surfaceVariant, RoundedCornerShape(8.dp)) + .padding(8.dp), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + rememberDrawablePainter(info.loadIcon(pm)), null, + Modifier.padding(end = 12.dp).size(50.dp) + ) + Text(info.loadLabel(pm).toString(), style = typography.titleLarge) + } + Switch(client.allow, { clients[index] = client.copy(allow = it) }) + } + } + } + } +} + @Serializable object LockScreenInfo @RequiresApi(24) diff --git a/app/src/main/res/drawable/dhizuku_icon.xml b/app/src/main/res/drawable/dhizuku_icon.xml new file mode 100644 index 0000000..7335bd4 --- /dev/null +++ b/app/src/main/res/drawable/dhizuku_icon.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a7f7e4d..4096b7a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -106,6 +106,11 @@ 转移 %1$s 特权将被转移至 %2$s + Dhizuku服务器 + 请求权限 + 允许 + 拒绝 + 创建工作资料成功 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1219145..38b0a81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -112,6 +112,11 @@ Transfer %1$s privilege will be transferred to %2$s + Dhizuku server + Request permission + Allow + Reject + Create work profile success diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7efdefa..3106ebd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,12 +3,13 @@ agp = "8.10.0" kotlin = "2.1.20" navigation-compose = "2.9.0" -composeBom = "2025.05.00" +composeBom = "2025.05.01" accompanist-drawablepainter = "0.35.0-alpha" accompanist-permissions = "0.37.0" shizuku = "13.1.5" -fragment = "1.8.6" +fragment = "1.8.7" dhizuku = "2.5.3" +dhizuku-server = "0.0.5" hiddenApiBypass = "4.3" libsu = "6.0.0" serialization = "1.7.3" @@ -28,6 +29,7 @@ accompanist-permissions = { group = "com.google.accompanist", name = "accompanis shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" } dhizuku-api = { module = "io.github.iamr0s:Dhizuku-API", version.ref = "dhizuku" } +dhizuku-server-api = { group = "io.github.iamr0s", name = "Dhizuku-SERVER_API", version.ref = "dhizuku-server" } hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" } libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" } libsu-service = { module = "com.github.topjohnwu.libsu:service", version.ref = "libsu" }