From 38ef06e12a74f34e739ca9ac70a70e41c0919558 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 24 Aug 2025 18:12:31 +0800 Subject: [PATCH] Fine-grained Dhizuku server permissions Add app icon to fastlane metadate --- .../com/bintianqi/owndroid/DhizukuServer.kt | 33 +++++-- .../com/bintianqi/owndroid/dpm/Permissions.kt | 83 ++++++++++++++---- .../metadata/android/en-US/images/icon.png | Bin 0 -> 4776 bytes 3 files changed, 90 insertions(+), 26 deletions(-) create mode 100644 fastlane/metadata/android/en-US/images/icon.png diff --git a/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt b/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt index 069308f..b196238 100644 --- a/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt +++ b/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt @@ -11,8 +11,14 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.LaunchedEffect @@ -21,6 +27,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -60,8 +67,16 @@ class MyDhizukuService(context: Context, admin: ComponentName, client: IDhizukuC 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") + val requiredPermission = when (func) { + "remote_transact", "remote_process" -> func + "bind_user_service", "unbind_user_service" -> "user_service" + "get_delegated_scopes", "set_delegated_scopes" -> "delegated_scopes" + else -> "other" + } + val hasPermission = clients.find { + callingUid == it.uid && signature == it.signature && requiredPermission in it.permissions + } != null + Log.d(TAG, "UID $callingUid, PID $callingPid, required permission: $requiredPermission, has permission: $hasPermission") return hasPermission } @@ -91,9 +106,12 @@ class DhizukuActivity : ComponentActivity() { val label = appInfo.loadLabel(packageManager).toString() fun close(grantPermission: Boolean) { val file = filesDir.resolve(DHIZUKU_CLIENTS_FILE) - val clients = Json.decodeFromString>(file.readText()) + val json = Json { ignoreUnknownKeys = true } + val clients = json.decodeFromString>(file.readText()) val index = clients.indexOfFirst { it.uid == uid } - val clientInfo = DhizukuClientInfo(uid, getPackageSignature(packageInfo), grantPermission) + val clientInfo = DhizukuClientInfo( + uid, getPackageSignature(packageInfo), if (grantPermission) DhizukuPermissions else emptyList() + ) if (index == -1) clients += clientInfo else clients[index] = clientInfo file.writeText(Json.encodeToString(clients)) @@ -121,9 +139,9 @@ class DhizukuActivity : ComponentActivity() { confirmButton = { var time by remember { mutableIntStateOf(3) } LaunchedEffect(Unit) { - (1..3).forEach { + for (i in 2 downTo 0) { delay(1000) - time -= 1 + time = i } } TextButton({ @@ -152,10 +170,11 @@ class DhizukuActivity : ComponentActivity() { } } +val DhizukuPermissions = listOf("remote_transact", "remote_process", "user_service", "delegated_scopes", "other") @Serializable data class DhizukuClientInfo( val uid: Int, val signature: String?, - val allow: Boolean + val permissions: List = emptyList() ) \ No newline at end of file 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 6f0202f..e2ba97b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -10,6 +10,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.annotation.Keep import androidx.annotation.RequiresApi import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -35,6 +37,7 @@ 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.ArrowDropDown import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.MoreVert @@ -44,6 +47,7 @@ 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.Card import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -55,10 +59,10 @@ 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 +import androidx.compose.material3.TriStateCheckbox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -72,11 +76,13 @@ 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.draw.rotate 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.state.ToggleableState import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp @@ -84,6 +90,7 @@ 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.DhizukuPermissions import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.IUserService import com.bintianqi.owndroid.MyAdminComponent @@ -509,7 +516,8 @@ fun DhizukuServerSettingsScreen(onNavigateUp: () -> Unit) { LaunchedEffect(enabled) { if (enabled) { clients.clear() - clients.addAll(Json.decodeFromString>(file.readText())) + val json = Json { ignoreUnknownKeys = true } + clients.addAll(json.decodeFromString>(file.readText())) } } MyLazyScaffold(R.string.dhizuku_server, onNavigateUp) { @@ -524,25 +532,62 @@ fun DhizukuServerSettingsScreen(onNavigateUp: () -> Unit) { writeList() } else { val info = pm.getApplicationInfo(name, 0) - Row( - Modifier - .fillMaxWidth() - .padding(HorizontalPadding, 8.dp), - Arrangement.SpaceBetween, Alignment.CenterVertically + var expand by remember { mutableStateOf(false) } + Card( + Modifier.fillMaxWidth().padding(HorizontalPadding, 8.dp) ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Image( - rememberDrawablePainter(info.loadIcon(pm)), null, - Modifier - .padding(end = 16.dp) - .size(50.dp) - ) - Text(info.loadLabel(pm).toString(), style = typography.titleLarge) + Row( + Modifier.fillMaxWidth().padding(8.dp, 8.dp, 0.dp, 8.dp), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + rememberDrawablePainter(info.loadIcon(pm)), null, + Modifier + .padding(end = 16.dp) + .size(50.dp) + ) + Column { + Text(info.loadLabel(pm).toString(), style = typography.titleLarge) + Text(name, Modifier.alpha(0.7F), style = typography.bodyMedium) + } + } + val ts = when (DhizukuPermissions.filter { it !in client.permissions }.size) { + 0 -> ToggleableState.On + DhizukuPermissions.size -> ToggleableState.Off + else -> ToggleableState.Indeterminate + } + Row(verticalAlignment = Alignment.CenterVertically) { + TriStateCheckbox(ts, { + clients[index] = when (ts) { + ToggleableState.On, ToggleableState.Indeterminate -> client.copy(permissions = emptyList()) + ToggleableState.Off -> client.copy(permissions = DhizukuPermissions) + } + }) + val degrees by animateFloatAsState(if(expand) 180F else 0F) + IconButton({ expand = !expand }) { + Icon(Icons.Default.ArrowDropDown, null, Modifier.rotate(degrees)) + } + } + } + AnimatedVisibility(expand, Modifier.padding(8.dp, 0.dp, 8.dp, 8.dp)) { + Column { + mapOf( + "remote_transact" to "Remote transact", "remote_process" to "Remote process", + "user_service" to "User service", "delegated_scopes" to "Delegated scopes", + "other" to context.getString(R.string.other) + ).forEach { (k, v) -> + Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) { + Text(v) + Checkbox(k in client.permissions, { + val newPermissions = if (it) client.permissions.plus(k) else client.permissions.minus(k) + clients[index] = client.copy(permissions = newPermissions) + writeList() + }) + } + } + } } - Switch(client.allow, { - clients[index] = client.copy(allow = it) - writeList() - }) } } } diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d2309df42b39e4142ec8207c07843e131922f7e9 GIT binary patch literal 4776 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