feat: enhanced app permission management (#229)

This commit is contained in:
BinTianqi
2026-02-14 14:23:46 +08:00
parent a38bffded5
commit 8991eade06
11 changed files with 248 additions and 89 deletions

View File

@@ -62,10 +62,11 @@ android {
dependenciesInfo {
includeInApk = false
}
composeCompiler {
}
composeCompiler {
includeSourceInformation = false
includeTraceMarkers = false
}
}
kotlin {

View File

@@ -7,12 +7,9 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -50,10 +47,7 @@ class LockTaskService: Service() {
.setOngoing(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.build()
ServiceCompat.startForeground(
this, NotificationType.LockTaskMode.id, notification,
if (Build.VERSION.SDK_INT < 34) 0 else ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
)
startForeground(NotificationType.LockTaskMode.id, notification)
coroutineScope.launch {
val am = getSystemService(ActivityManager::class.java)
delay(3000)

View File

@@ -165,8 +165,12 @@ import com.bintianqi.owndroid.dpm.PasswordInfoScreen
import com.bintianqi.owndroid.dpm.PasswordScreen
import com.bintianqi.owndroid.dpm.PermissionPolicy
import com.bintianqi.owndroid.dpm.PermissionPolicyScreen
import com.bintianqi.owndroid.dpm.PermissionsManager
import com.bintianqi.owndroid.dpm.PermissionsManagerScreen
import com.bintianqi.owndroid.dpm.AppPermissionsManager
import com.bintianqi.owndroid.dpm.AppPermissionsManagerScreen
import com.bintianqi.owndroid.dpm.PermissionDetail
import com.bintianqi.owndroid.dpm.PermissionDetailScreen
import com.bintianqi.owndroid.dpm.PermissionManager
import com.bintianqi.owndroid.dpm.PermissionManagerScreen
import com.bintianqi.owndroid.dpm.PermittedAccessibilityServices
import com.bintianqi.owndroid.dpm.PermittedAsAndImPackages
import com.bintianqi.owndroid.dpm.PermittedInputMethods
@@ -566,10 +570,17 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
::navigateToAppGroups, vm.appGroups, R.string.info_disable_user_control
)
}
composable<PermissionsManager> {
PermissionsManagerScreen(
vm.packagePermissions, vm::getPackagePermissions, vm::setPackagePermission,
::navigateUp, it.toRoute(), vm.chosenPackage, ::chooseSinglePackage
composable<AppPermissionsManager> {
AppPermissionsManagerScreen(
vm::getPackagePermissions, vm::setPackagePermission, ::navigateUp, it.toRoute()
)
}
composable<PermissionManager> {
PermissionManagerScreen(::navigate, ::navigateUp)
}
composable<PermissionDetail> {
PermissionDetailScreen(
it.toRoute(), vm::getPermissionPackages, vm::setPackagePermission, ::navigateUp
)
}
composable<DisableMeteredData> {

View File

@@ -272,6 +272,6 @@ class MyRepository(val dbHelper: MyDbHelper) {
return list
}
fun deleteAllCrossProfileIntentFilters() {
dbHelper.writableDatabase.delete("cross_profile_intent_filters", null, null);
dbHelper.writableDatabase.delete("cross_profile_intent_filters", null, null)
}
}

View File

@@ -107,7 +107,6 @@ import com.bintianqi.owndroid.dpm.doUserOperationWithContext
import com.bintianqi.owndroid.dpm.getPackageInstaller
import com.bintianqi.owndroid.dpm.globalSettings
import com.bintianqi.owndroid.dpm.handlePrivilegeChange
import com.bintianqi.owndroid.dpm.isValidPackageName
import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage
import com.bintianqi.owndroid.dpm.runtimePermissions
import com.bintianqi.owndroid.dpm.secureSettings
@@ -280,20 +279,23 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
getUcdPackages()
}
val packagePermissions = MutableStateFlow(emptyMap<String, Int>())
fun getPackagePermissions(name: String) {
if (name.isValidPackageName) {
packagePermissions.value = runtimePermissions.associate {
fun getPackagePermissions(name: String): Map<String, Int> {
return runtimePermissions.associate {
it.id to DPM.getPermissionGrantState(DAR, name, it.id)
}
} else {
packagePermissions.value = emptyMap()
}
}
fun setPackagePermission(name: String, permission: String, status: Int): Boolean {
val result = DPM.setPermissionGrantState(DAR, name, permission, status)
getPackagePermissions(name)
return result
return DPM.setPermissionGrantState(DAR, name, permission, status)
}
fun getPermissionPackages(permission: String): List<Pair<AppInfo, Int>> {
return PM.getInstalledPackages(
getInstalledAppsFlags or PackageManager.GET_PERMISSIONS
).filter {
it.requestedPermissions?.contains(permission) ?: false
}.map {
getAppInfo(it.packageName) to
DPM.getPermissionGrantState(DAR, it.packageName, permission)
}
}
// Metered data disabled packages

View File

@@ -1,7 +1,6 @@
package com.bintianqi.owndroid
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Build
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -276,6 +275,3 @@ fun AppChooserScreen(
}
}
}
val getInstalledAppsFlags =
if(Build.VERSION.SDK_INT >= 24) PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_UNINSTALLED_PACKAGES else 0

View File

@@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.widget.Toast
@@ -179,3 +180,6 @@ fun registerPackageRemovedReceiver(
}
fun parsePackageNames(input: String) = input.split('\n').filter { it.isNotEmpty() }
val getInstalledAppsFlags =
if(Build.VERSION.SDK_INT >= 24) PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_UNINSTALLED_PACKAGES else 0

View File

@@ -5,6 +5,7 @@ import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED
import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED
import android.app.admin.PackagePolicy
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.os.Build.VERSION
import android.os.Looper
@@ -83,6 +84,7 @@ 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
@@ -223,7 +225,9 @@ fun ApplicationsFeaturesScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Un
if(VERSION.SDK_INT >= 30 && (privilege.device || (VERSION.SDK_INT >= 33 && privilege.profile))) {
FunctionItem(R.string.disable_user_control, icon = R.drawable.do_not_touch_fill0) { onNavigate(DisableUserControl) }
}
FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager()) }
FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) {
onNavigate(PermissionManager)
}
if(VERSION.SDK_INT >= 28) {
FunctionItem(R.string.disable_metered_data, icon = R.drawable.money_off_fill0) { onNavigate(DisableMeteredData) }
}
@@ -298,7 +302,7 @@ fun ApplicationDetailsScreen(
.alpha(0.7F)
.padding(bottom = 8.dp), style = typography.bodyMedium)
}
FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager(packageName)) }
FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(AppPermissionsManager(packageName)) }
if(VERSION.SDK_INT >= 24) SwitchItem(
R.string.suspend, icon = R.drawable.block_fill0, state = status.suspend,
onCheckedChange = { vm.adSetPackageSuspended(packageName, it) }
@@ -353,40 +357,29 @@ fun ApplicationDetailsScreen(
@Serializable object DisableUserControl
@Serializable data class PermissionsManager(val packageName: String? = null)
@Serializable data class AppPermissionsManager(val packageName: String)
@Composable
fun PermissionsManagerScreen(
packagePermissions: MutableStateFlow<Map<String, Int>>, getPackagePermissions: (String) -> Unit,
fun AppPermissionsManagerScreen(
getPackagePermissions: (String) -> Map<String, Int>,
setPackagePermission: (String, String, Int) -> Boolean, onNavigateUp: () -> Unit,
param: PermissionsManager, chosenPackage: Channel<String>, onChoosePackage: () -> Unit
param: AppPermissionsManager
) {
val packageNameParam = param.packageName
val context = LocalContext.current
val privilege by Privilege.status.collectAsStateWithLifecycle()
var packageName by rememberSaveable { mutableStateOf(packageNameParam ?: "") }
var selectedPermission by rememberSaveable { mutableIntStateOf(-1) }
val permissions by packagePermissions.collectAsStateWithLifecycle()
var selectedPermission by remember { mutableStateOf<PermissionItem?>(null) }
val permissions = remember { mutableStateMapOf<String, Int>() }
LaunchedEffect(Unit) {
packageName = chosenPackage.receive()
}
LaunchedEffect(packageName) {
getPackagePermissions(packageName)
permissions.putAll(getPackagePermissions(param.packageName))
}
MyLazyScaffold(R.string.permissions, onNavigateUp) {
item {
if(packageNameParam == null) {
PackageNameTextField(packageName, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
Spacer(Modifier.padding(vertical = 4.dp))
}
}
itemsIndexed(runtimePermissions, { _, it -> it.id }) { index, it ->
items(runtimePermissions) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
selectedPermission = index
selectedPermission = it
}
.padding(8.dp)
) {
@@ -407,37 +400,50 @@ fun PermissionsManagerScreen(
Spacer(Modifier.height(BottomPadding))
}
}
if(selectedPermission != -1) {
val permission = runtimePermissions[selectedPermission]
fun changeState(state: Int) {
val result = setPackagePermission(packageName, permission.id, state)
if (result) selectedPermission = -1
if(selectedPermission != null) PackagePermissionDialog(
selectedPermission!!, permissions[selectedPermission!!.id]!!, privilege.profile,
{
val result = setPackagePermission(param.packageName, selectedPermission!!.id, it)
if (!result) context.showOperationResultToast(false)
selectedPermission = null
permissions.putAll(getPackagePermissions(param.packageName))
}
) { selectedPermission = null }
}
@Composable
fun PackagePermissionDialog(
permission: PermissionItem, currentState: Int, isProfileOwner: Boolean, onSet: (Int) -> Unit,
onClose: () -> Unit
) {
@Composable
fun GrantPermissionItem(label: Int, status: Int) {
val selected = permissions[permission.id] == status
fun GrantPermissionItem(label: Int, stateId: Int) {
val selected = currentState == stateId
Row(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(if (selected) colorScheme.primaryContainer else Color.Transparent)
.clickable { changeState(status) }
.clickable { onSet(stateId) }
.padding(vertical = 16.dp, horizontal = 12.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically,
) {
Text(stringResource(label), color = if(selected) colorScheme.primary else Color.Unspecified)
if(selected) Icon(Icons.Outlined.CheckCircle, null, tint = colorScheme.primary)
Text(
stringResource(label),
color = if(selected) colorScheme.primary else Color.Unspecified
)
if (selected) Icon(Icons.Outlined.CheckCircle, null, tint = colorScheme.primary)
}
}
AlertDialog(
onDismissRequest = { selectedPermission = -1 },
confirmButton = { TextButton({ selectedPermission = -1 }) { Text(stringResource(R.string.cancel)) } },
onDismissRequest = onClose,
confirmButton = { TextButton(onClose) { Text(stringResource(R.string.cancel)) } },
title = { Text(stringResource(permission.label)) },
text = {
Column {
Text(permission.id)
Spacer(Modifier.padding(vertical = 4.dp))
if(!(VERSION.SDK_INT >= 31 && permission.profileOwnerRestricted && privilege.profile)) {
if(!(VERSION.SDK_INT >= 31 && permission.profileOwnerRestricted && isProfileOwner)) {
GrantPermissionItem(R.string.granted, PERMISSION_GRANT_STATE_GRANTED)
}
GrantPermissionItem(R.string.denied, PERMISSION_GRANT_STATE_DENIED)
@@ -445,7 +451,139 @@ fun PermissionsManagerScreen(
}
}
)
}
@Serializable object PermissionManager
@Composable
fun PermissionManagerScreen(onNavigate: (PermissionDetail) -> Unit, onNavigateUp: () -> Unit) {
MyLazyScaffold(R.string.permissions, onNavigateUp) {
items(runtimePermissions) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
onNavigate(PermissionDetail(it.id))
}
.padding(8.dp, 12.dp)
) {
Icon(painterResource(it.icon), null, Modifier.padding(horizontal = 12.dp))
Text(stringResource(it.label))
}
}
item {
Spacer(Modifier.height(BottomPadding))
}
}
}
@Serializable class PermissionDetail(val permission: String)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PermissionDetailScreen(
param: PermissionDetail, getPermissionPackages: (String) -> List<Pair<AppInfo, Int>>,
setPackagePermission: (String, String, Int) -> Boolean, onNavigateUp: () -> Unit
) {
val context = LocalContext.current
val privilege by Privilege.status.collectAsStateWithLifecycle()
val permissionItem = runtimePermissions.find { it.id == param.permission }!!
val packagesList = remember { mutableStateListOf<Pair<AppInfo, Int>>() }
var selectedPackage by remember { mutableStateOf<Pair<String, Int>?>(null) }
var showUserApps by remember { mutableStateOf(true) }
var showSystemApps by remember { mutableStateOf(false) }
val displayedPackagesList = packagesList.filter {
(showUserApps && it.first.flags and ApplicationInfo.FLAG_SYSTEM == 0) ||
(showSystemApps && it.first.flags and ApplicationInfo.FLAG_SYSTEM != 0)
}
LaunchedEffect(Unit) {
packagesList.addAll(getPermissionPackages(param.permission))
}
Scaffold(
topBar = {
TopAppBar(
{ Text(stringResource(permissionItem.label)) },
navigationIcon = { NavIcon(onNavigateUp) },
actions = {
var menu by remember { mutableStateOf(false) }
Box {
IconButton({ menu = true }) {
Icon(painterResource(R.drawable.filter_alt_fill0), null)
}
DropdownMenu(menu, { menu = false }) {
DropdownMenuItem(
{
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(showUserApps, { showUserApps = it })
Text(stringResource(R.string.user_apps))
}
},
{ showUserApps = !showUserApps }
)
DropdownMenuItem(
{
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(showSystemApps, { showSystemApps = it })
Text(stringResource(R.string.system_apps))
}
},
{ showSystemApps = !showSystemApps }
)
}
}
}
)
},
contentWindowInsets = adaptiveInsets()
) { paddingValues ->
LazyColumn(Modifier.padding(paddingValues)) {
items(displayedPackagesList) { (info, grantState) ->
Row(
Modifier
.fillMaxWidth()
.clickable { selectedPackage = info.name to grantState }
.padding(horizontal = 8.dp, vertical = 6.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) {
Image(
rememberDrawablePainter(info.icon), null,
Modifier
.padding(start = 12.dp, end = 18.dp)
.size(30.dp)
)
Column {
Text(info.label)
Text(info.name, Modifier.alpha(0.8F), style = typography.bodyMedium)
}
}
if (grantState != 0) {
Icon(
painterResource(
if (grantState == 1) R.drawable.check_circle_fill0
else R.drawable.cancel_fill0
),
null
)
}
}
}
item {
Spacer(Modifier.height(BottomPadding))
}
}
}
if (selectedPackage != null) PackagePermissionDialog(
permissionItem, selectedPackage!!.second, privilege.profile,
{
val result = setPackagePermission(selectedPackage!!.first, param.permission, it)
if (!result) context.showOperationResultToast(false)
selectedPackage = null
packagesList.clear()
packagesList.addAll(getPermissionPackages(param.permission))
}
) { selectedPackage = null }
}
@Serializable object DisableMeteredData

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m336,680 l144,-144 144,144 56,-56 -144,-144 144,-144 -56,-56 -144,144 -144,-144 -56,56 144,144 -144,144 56,56ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -359,6 +359,8 @@
<string name="enable_lockdown">启用锁定</string>
<string name="clear_current_config">清除当前配置</string>
<string name="permissions">权限</string>
<string name="user_apps">用户应用</string>
<string name="system_apps">系统应用</string>
<string name="not_installed">未安装</string>
<string name="block_uninstall">阻止卸载</string>
<string name="disable_user_control">禁止用户控制</string>

View File

@@ -393,6 +393,8 @@
<string name="enable_lockdown">Enable lockdown</string>
<string name="clear_current_config">Clear current config</string>
<string name="permissions">Permissions</string>
<string name="user_apps">User apps</string>
<string name="system_apps">System apps</string>
<string name="not_installed">Not installed</string>
<string name="block_uninstall">Block uninstall</string>
<string name="enable_system_app">Enable system app</string>