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 { dependenciesInfo {
includeInApk = false includeInApk = false
} }
composeCompiler { }
composeCompiler {
includeSourceInformation = false includeSourceInformation = false
includeTraceMarkers = false includeTraceMarkers = false
}
} }
kotlin { kotlin {

View File

@@ -7,12 +7,9 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -50,10 +47,7 @@ class LockTaskService: Service() {
.setOngoing(true) .setOngoing(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.build() .build()
ServiceCompat.startForeground( startForeground(NotificationType.LockTaskMode.id, notification)
this, NotificationType.LockTaskMode.id, notification,
if (Build.VERSION.SDK_INT < 34) 0 else ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
)
coroutineScope.launch { coroutineScope.launch {
val am = getSystemService(ActivityManager::class.java) val am = getSystemService(ActivityManager::class.java)
delay(3000) 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.PasswordScreen
import com.bintianqi.owndroid.dpm.PermissionPolicy import com.bintianqi.owndroid.dpm.PermissionPolicy
import com.bintianqi.owndroid.dpm.PermissionPolicyScreen import com.bintianqi.owndroid.dpm.PermissionPolicyScreen
import com.bintianqi.owndroid.dpm.PermissionsManager import com.bintianqi.owndroid.dpm.AppPermissionsManager
import com.bintianqi.owndroid.dpm.PermissionsManagerScreen 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.PermittedAccessibilityServices
import com.bintianqi.owndroid.dpm.PermittedAsAndImPackages import com.bintianqi.owndroid.dpm.PermittedAsAndImPackages
import com.bintianqi.owndroid.dpm.PermittedInputMethods 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 ::navigateToAppGroups, vm.appGroups, R.string.info_disable_user_control
) )
} }
composable<PermissionsManager> { composable<AppPermissionsManager> {
PermissionsManagerScreen( AppPermissionsManagerScreen(
vm.packagePermissions, vm::getPackagePermissions, vm::setPackagePermission, vm::getPackagePermissions, vm::setPackagePermission, ::navigateUp, it.toRoute()
::navigateUp, it.toRoute(), vm.chosenPackage, ::chooseSinglePackage )
}
composable<PermissionManager> {
PermissionManagerScreen(::navigate, ::navigateUp)
}
composable<PermissionDetail> {
PermissionDetailScreen(
it.toRoute(), vm::getPermissionPackages, vm::setPackagePermission, ::navigateUp
) )
} }
composable<DisableMeteredData> { composable<DisableMeteredData> {

View File

@@ -272,6 +272,6 @@ class MyRepository(val dbHelper: MyDbHelper) {
return list return list
} }
fun deleteAllCrossProfileIntentFilters() { 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.getPackageInstaller
import com.bintianqi.owndroid.dpm.globalSettings import com.bintianqi.owndroid.dpm.globalSettings
import com.bintianqi.owndroid.dpm.handlePrivilegeChange import com.bintianqi.owndroid.dpm.handlePrivilegeChange
import com.bintianqi.owndroid.dpm.isValidPackageName
import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage
import com.bintianqi.owndroid.dpm.runtimePermissions import com.bintianqi.owndroid.dpm.runtimePermissions
import com.bintianqi.owndroid.dpm.secureSettings import com.bintianqi.owndroid.dpm.secureSettings
@@ -280,20 +279,23 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
getUcdPackages() getUcdPackages()
} }
val packagePermissions = MutableStateFlow(emptyMap<String, Int>()) fun getPackagePermissions(name: String): Map<String, Int> {
fun getPackagePermissions(name: String) { return runtimePermissions.associate {
if (name.isValidPackageName) {
packagePermissions.value = runtimePermissions.associate {
it.id to DPM.getPermissionGrantState(DAR, name, it.id) it.id to DPM.getPermissionGrantState(DAR, name, it.id)
} }
} else {
packagePermissions.value = emptyMap()
}
} }
fun setPackagePermission(name: String, permission: String, status: Int): Boolean { fun setPackagePermission(name: String, permission: String, status: Int): Boolean {
val result = DPM.setPermissionGrantState(DAR, name, permission, status) return DPM.setPermissionGrantState(DAR, name, permission, status)
getPackagePermissions(name) }
return result 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 // Metered data disabled packages

View File

@@ -1,7 +1,6 @@
package com.bintianqi.owndroid package com.bintianqi.owndroid
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import androidx.compose.foundation.ExperimentalFoundationApi 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.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
@@ -179,3 +180,6 @@ fun registerPackageRemovedReceiver(
} }
fun parsePackageNames(input: String) = input.split('\n').filter { it.isNotEmpty() } 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.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED
import android.app.admin.PackagePolicy import android.app.admin.PackagePolicy
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri import android.net.Uri
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Looper import android.os.Looper
@@ -83,6 +84,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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))) { 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.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) { if(VERSION.SDK_INT >= 28) {
FunctionItem(R.string.disable_metered_data, icon = R.drawable.money_off_fill0) { onNavigate(DisableMeteredData) } FunctionItem(R.string.disable_metered_data, icon = R.drawable.money_off_fill0) { onNavigate(DisableMeteredData) }
} }
@@ -298,7 +302,7 @@ fun ApplicationDetailsScreen(
.alpha(0.7F) .alpha(0.7F)
.padding(bottom = 8.dp), style = typography.bodyMedium) .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( if(VERSION.SDK_INT >= 24) SwitchItem(
R.string.suspend, icon = R.drawable.block_fill0, state = status.suspend, R.string.suspend, icon = R.drawable.block_fill0, state = status.suspend,
onCheckedChange = { vm.adSetPackageSuspended(packageName, it) } onCheckedChange = { vm.adSetPackageSuspended(packageName, it) }
@@ -353,40 +357,29 @@ fun ApplicationDetailsScreen(
@Serializable object DisableUserControl @Serializable object DisableUserControl
@Serializable data class PermissionsManager(val packageName: String? = null) @Serializable data class AppPermissionsManager(val packageName: String)
@Composable @Composable
fun PermissionsManagerScreen( fun AppPermissionsManagerScreen(
packagePermissions: MutableStateFlow<Map<String, Int>>, getPackagePermissions: (String) -> Unit, getPackagePermissions: (String) -> Map<String, Int>,
setPackagePermission: (String, String, Int) -> Boolean, onNavigateUp: () -> Unit, 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() val privilege by Privilege.status.collectAsStateWithLifecycle()
var packageName by rememberSaveable { mutableStateOf(packageNameParam ?: "") } var selectedPermission by remember { mutableStateOf<PermissionItem?>(null) }
var selectedPermission by rememberSaveable { mutableIntStateOf(-1) } val permissions = remember { mutableStateMapOf<String, Int>() }
val permissions by packagePermissions.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
packageName = chosenPackage.receive() permissions.putAll(getPackagePermissions(param.packageName))
}
LaunchedEffect(packageName) {
getPackagePermissions(packageName)
} }
MyLazyScaffold(R.string.permissions, onNavigateUp) { MyLazyScaffold(R.string.permissions, onNavigateUp) {
item { items(runtimePermissions) {
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 ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
selectedPermission = index selectedPermission = it
} }
.padding(8.dp) .padding(8.dp)
) { ) {
@@ -407,37 +400,50 @@ fun PermissionsManagerScreen(
Spacer(Modifier.height(BottomPadding)) Spacer(Modifier.height(BottomPadding))
} }
} }
if(selectedPermission != -1) { if(selectedPermission != null) PackagePermissionDialog(
val permission = runtimePermissions[selectedPermission] selectedPermission!!, permissions[selectedPermission!!.id]!!, privilege.profile,
fun changeState(state: Int) { {
val result = setPackagePermission(packageName, permission.id, state) val result = setPackagePermission(param.packageName, selectedPermission!!.id, it)
if (result) selectedPermission = -1 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 @Composable
fun GrantPermissionItem(label: Int, status: Int) { fun GrantPermissionItem(label: Int, stateId: Int) {
val selected = permissions[permission.id] == status val selected = currentState == stateId
Row( Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(if (selected) colorScheme.primaryContainer else Color.Transparent) .background(if (selected) colorScheme.primaryContainer else Color.Transparent)
.clickable { changeState(status) } .clickable { onSet(stateId) }
.padding(vertical = 16.dp, horizontal = 12.dp), .padding(vertical = 16.dp, horizontal = 12.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically, Arrangement.SpaceBetween, Alignment.CenterVertically,
) { ) {
Text(stringResource(label), color = if(selected) colorScheme.primary else Color.Unspecified) Text(
if(selected) Icon(Icons.Outlined.CheckCircle, null, tint = colorScheme.primary) stringResource(label),
color = if(selected) colorScheme.primary else Color.Unspecified
)
if (selected) Icon(Icons.Outlined.CheckCircle, null, tint = colorScheme.primary)
} }
} }
AlertDialog( AlertDialog(
onDismissRequest = { selectedPermission = -1 }, onDismissRequest = onClose,
confirmButton = { TextButton({ selectedPermission = -1 }) { Text(stringResource(R.string.cancel)) } }, confirmButton = { TextButton(onClose) { Text(stringResource(R.string.cancel)) } },
title = { Text(stringResource(permission.label)) }, title = { Text(stringResource(permission.label)) },
text = { text = {
Column { Column {
Text(permission.id) Text(permission.id)
Spacer(Modifier.padding(vertical = 4.dp)) 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.granted, PERMISSION_GRANT_STATE_GRANTED)
} }
GrantPermissionItem(R.string.denied, PERMISSION_GRANT_STATE_DENIED) 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 @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="enable_lockdown">启用锁定</string>
<string name="clear_current_config">清除当前配置</string> <string name="clear_current_config">清除当前配置</string>
<string name="permissions">权限</string> <string name="permissions">权限</string>
<string name="user_apps">用户应用</string>
<string name="system_apps">系统应用</string>
<string name="not_installed">未安装</string> <string name="not_installed">未安装</string>
<string name="block_uninstall">阻止卸载</string> <string name="block_uninstall">阻止卸载</string>
<string name="disable_user_control">禁止用户控制</string> <string name="disable_user_control">禁止用户控制</string>

View File

@@ -393,6 +393,8 @@
<string name="enable_lockdown">Enable lockdown</string> <string name="enable_lockdown">Enable lockdown</string>
<string name="clear_current_config">Clear current config</string> <string name="clear_current_config">Clear current config</string>
<string name="permissions">Permissions</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="not_installed">Not installed</string>
<string name="block_uninstall">Block uninstall</string> <string name="block_uninstall">Block uninstall</string>
<string name="enable_system_app">Enable system app</string> <string name="enable_system_app">Enable system app</string>