From 8991eade06524e176927f3aa2239c89334b42952 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 14 Feb 2026 14:23:46 +0800 Subject: [PATCH] feat: enhanced app permission management (#229) --- app/build.gradle.kts | 9 +- .../com/bintianqi/owndroid/LockTaskService.kt | 8 +- .../com/bintianqi/owndroid/MainActivity.kt | 23 +- .../com/bintianqi/owndroid/MyRepository.kt | 2 +- .../com/bintianqi/owndroid/MyViewModel.kt | 26 +- .../com/bintianqi/owndroid/PackageChooser.kt | 4 - .../main/java/com/bintianqi/owndroid/Utils.kt | 4 + .../bintianqi/owndroid/dpm/Applications.kt | 248 ++++++++++++++---- app/src/main/res/drawable/cancel_fill0.xml | 9 + app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 11 files changed, 248 insertions(+), 89 deletions(-) create mode 100644 app/src/main/res/drawable/cancel_fill0.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 77dcbdb..cd5a8f9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,10 +62,11 @@ android { dependenciesInfo { includeInApk = false } - composeCompiler { - includeSourceInformation = false - includeTraceMarkers = false - } +} + +composeCompiler { + includeSourceInformation = false + includeTraceMarkers = false } kotlin { diff --git a/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt index 301117d..c9cec0c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt +++ b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt @@ -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) diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index fec0baa..adbe753 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -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 { - PermissionsManagerScreen( - vm.packagePermissions, vm::getPackagePermissions, vm::setPackagePermission, - ::navigateUp, it.toRoute(), vm.chosenPackage, ::chooseSinglePackage + composable { + AppPermissionsManagerScreen( + vm::getPackagePermissions, vm::setPackagePermission, ::navigateUp, it.toRoute() + ) + } + composable { + PermissionManagerScreen(::navigate, ::navigateUp) + } + composable { + PermissionDetailScreen( + it.toRoute(), vm::getPermissionPackages, vm::setPackagePermission, ::navigateUp ) } composable { diff --git a/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt b/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt index 6c99053..5c10ab2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index bdb23b3..78d9515 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -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()) - fun getPackagePermissions(name: String) { - if (name.isValidPackageName) { - packagePermissions.value = runtimePermissions.associate { - it.id to DPM.getPermissionGrantState(DAR, name, it.id) - } - } else { - packagePermissions.value = emptyMap() + fun getPackagePermissions(name: String): Map { + return runtimePermissions.associate { + it.id to DPM.getPermissionGrantState(DAR, name, it.id) } } 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> { + 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 diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index 4f34429..618c1c2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -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 diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index c545970..5ca0123 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -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 diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 1f501dd..096e2a9 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -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>, getPackagePermissions: (String) -> Unit, +fun AppPermissionsManagerScreen( + getPackagePermissions: (String) -> Map, setPackagePermission: (String, String, Int) -> Boolean, onNavigateUp: () -> Unit, - param: PermissionsManager, chosenPackage: Channel, 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(null) } + val permissions = remember { mutableStateMapOf() } 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,45 +400,190 @@ 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)) } - @Composable - fun GrantPermissionItem(label: Int, status: Int) { - val selected = permissions[permission.id] == status - Row( - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(if (selected) colorScheme.primaryContainer else Color.Transparent) - .clickable { changeState(status) } - .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) + ) { selectedPermission = null } +} + +@Composable +fun PackagePermissionDialog( + permission: PermissionItem, currentState: Int, isProfileOwner: Boolean, onSet: (Int) -> Unit, + onClose: () -> Unit +) { + @Composable + 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 { 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) + } + } + AlertDialog( + 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 && isProfileOwner)) { + GrantPermissionItem(R.string.granted, PERMISSION_GRANT_STATE_GRANTED) + } + GrantPermissionItem(R.string.denied, PERMISSION_GRANT_STATE_DENIED) + GrantPermissionItem(R.string.default_stringres, PERMISSION_GRANT_STATE_DEFAULT) } } - AlertDialog( - onDismissRequest = { selectedPermission = -1 }, - confirmButton = { TextButton({ selectedPermission = -1 }) { 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)) { - GrantPermissionItem(R.string.granted, PERMISSION_GRANT_STATE_GRANTED) + ) +} + +@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>, + 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>() } + var selectedPackage by remember { mutableStateOf?>(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 + ) } - GrantPermissionItem(R.string.denied, PERMISSION_GRANT_STATE_DENIED) - GrantPermissionItem(R.string.default_stringres, PERMISSION_GRANT_STATE_DEFAULT) } } - ) + 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 diff --git a/app/src/main/res/drawable/cancel_fill0.xml b/app/src/main/res/drawable/cancel_fill0.xml new file mode 100644 index 0000000..0f07315 --- /dev/null +++ b/app/src/main/res/drawable/cancel_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7d0321b..7b4fef9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -359,6 +359,8 @@ 启用锁定 清除当前配置 权限 + 用户应用 + 系统应用 未安装 阻止卸载 禁止用户控制 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eebaca9..f29202e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -393,6 +393,8 @@ Enable lockdown Clear current config Permissions + User apps + System apps Not installed Block uninstall Enable system app