Files
OwnDroid/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt
2025-03-22 09:45:55 +08:00

1128 lines
45 KiB
Kotlin

package com.bintianqi.owndroid.dpm
import android.app.PendingIntent
import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT
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.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build.VERSION
import android.os.Looper
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.LazyItemScope
import androidx.compose.foundation.lazy.items
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.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.LinearProgressIndicator
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.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
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.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.bintianqi.owndroid.AppInfo
import com.bintianqi.owndroid.AppInstallerActivity
import com.bintianqi.owndroid.AppInstallerViewModel
import com.bintianqi.owndroid.ChoosePackageContract
import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.getInstalledAppsFlags
import com.bintianqi.owndroid.installedApps
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.ErrorDialog
import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem
import com.bintianqi.owndroid.ui.FunctionItem
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.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.serialization.Serializable
import java.util.concurrent.Executors
fun PackageManager.retrieveAppInfo(packageName: String) =
getApplicationInfo(packageName, getInstalledAppsFlags).retrieveAppInfo(this)
fun ApplicationInfo.retrieveAppInfo(pm: PackageManager) =
installedApps.value.find { it.name == packageName } ?: AppInfo(packageName, loadLabel(pm).toString(), loadIcon(pm), flags)
val String.isValidPackageName
get() = Regex("""^(?:[a-zA-Z]\w*\.)+[a-zA-Z]\w*$""").matches(this)
@Composable
fun LazyItemScope.ApplicationItem(info: AppInfo, onClear: () -> Unit) {
Row(
Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp).animateItem(),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberDrawablePainter(info.icon), contentDescription = null,
modifier = 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)
}
}
IconButton(onClear) {
Icon(Icons.Default.Clear, null)
}
}
}
@Composable
fun PackageNameTextField(value: String, modifier: Modifier = Modifier, onValueChange: (String) -> Unit) {
val launcher = rememberLauncherForActivityResult(ChoosePackageContract()) {
if(it != null) onValueChange(it)
}
val fm = LocalFocusManager.current
OutlinedTextField(
value, onValueChange, Modifier.fillMaxWidth().then(modifier),
label = { Text(stringResource(R.string.package_name)) },
trailingIcon = {
IconButton({
launcher.launch(null)
}) {
Icon(Icons.AutoMirrored.Default.List, null)
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { fm.clearFocus() }
)
}
@Serializable object ApplicationsFeatures
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ApplicationsFeaturesScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit, onSwitchView: () -> Unit) {
val context = LocalContext.current
val sb = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
Modifier.nestedScroll(sb.nestedScrollConnection),
topBar = {
LargeTopAppBar(
{ Text(stringResource(R.string.applications)) },
navigationIcon = { NavIcon(onNavigateUp) },
actions = {
IconButton(onSwitchView) {
Icon(painterResource(R.drawable.android_fill0), null)
}
},
scrollBehavior = sb
)
}
) { paddingValues ->
Column(
Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(bottom = 80.dp)
) {
val dpm = context.getDPM()
val receiver = context.getReceiver()
val deviceOwner = context.isDeviceOwner
val profileOwner = context.isProfileOwner
if(VERSION.SDK_INT >= 24) FunctionItem(R.string.suspend, icon = R.drawable.block_fill0) { onNavigate(Suspend) }
FunctionItem(R.string.hide, icon = R.drawable.visibility_off_fill0) { onNavigate(Hide) }
FunctionItem(R.string.block_uninstall, icon = R.drawable.delete_forever_fill0) { onNavigate(BlockUninstall) }
if(VERSION.SDK_INT >= 30 && (deviceOwner || (VERSION.SDK_INT >= 33 && profileOwner))) {
FunctionItem(R.string.disable_user_control, icon = R.drawable.do_not_touch_fill0) { onNavigate(DisableUserControl) }
}
if(VERSION.SDK_INT >= 23) {
FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager()) }
}
if(VERSION.SDK_INT >= 28) {
FunctionItem(R.string.disable_metered_data, icon = R.drawable.money_off_fill0) { onNavigate(DisableMeteredData) }
}
if(VERSION.SDK_INT >= 28) {
FunctionItem(R.string.clear_app_storage, icon = R.drawable.mop_fill0) { onNavigate(ClearAppStorage) }
}
FunctionItem(R.string.install_app, icon = R.drawable.install_mobile_fill0) {
context.startActivity(Intent(context, AppInstallerActivity::class.java))
}
FunctionItem(R.string.uninstall_app, icon = R.drawable.delete_fill0) { onNavigate(UninstallApp) }
if(VERSION.SDK_INT >= 28 && deviceOwner) {
FunctionItem(R.string.keep_uninstalled_packages, icon = R.drawable.delete_fill0) { onNavigate(KeepUninstalledPackages) }
}
if(VERSION.SDK_INT >= 28) FunctionItem(R.string.install_existing_app, icon = R.drawable.install_mobile_fill0) {
onNavigate(InstallExistingApp)
}
if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) {
FunctionItem(R.string.cross_profile_apps, icon = R.drawable.work_fill0) { onNavigate(CrossProfilePackages) }
}
if(profileOwner) {
FunctionItem(R.string.cross_profile_widget, icon = R.drawable.widgets_fill0) { onNavigate(CrossProfileWidgetProviders) }
}
if(VERSION.SDK_INT >= 34 && deviceOwner) {
FunctionItem(R.string.credential_manager_policy, icon = R.drawable.license_fill0) { onNavigate(CredentialManagerPolicy) }
}
FunctionItem(R.string.permitted_accessibility_services, icon = R.drawable.settings_accessibility_fill0) {
onNavigate(PermittedAccessibilityServices)
}
FunctionItem(R.string.permitted_ime, icon = R.drawable.keyboard_fill0) { onNavigate(PermittedInputMethods) }
FunctionItem(R.string.enable_system_app, icon = R.drawable.enable_fill0) { onNavigate(EnableSystemApp) }
if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(R.string.set_default_dialer, icon = R.drawable.call_fill0) { onNavigate(SetDefaultDialer) }
}
}
}
}
@Serializable data class ApplicationDetails(val packageName: String)
@Composable
fun ApplicationDetailsScreen(param: ApplicationDetails, onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
val packageName = param.packageName
val context = LocalContext.current
val pm = context.packageManager
val dpm = context.getDPM()
val receiver = context.getReceiver()
var dialog by remember { mutableIntStateOf(0) } // 1: clear storage, 2: uninstall
val info = pm.getApplicationInfo(packageName, getInstalledAppsFlags)
MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) {
Column(Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Image(rememberDrawablePainter(info.loadIcon(pm)), null, Modifier.size(50.dp))
Text(info.loadLabel(pm).toString(), Modifier.padding(top = 4.dp))
Text(info.packageName, Modifier.alpha(0.7F).padding(bottom = 8.dp), style = typography.bodyMedium)
}
FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager(packageName)) }
if(VERSION.SDK_INT >= 24) SwitchItem(
R.string.suspend, icon = R.drawable.block_fill0,
getState = { dpm.isPackageSuspended(receiver, packageName) },
onCheckedChange = { dpm.setPackagesSuspended(receiver, arrayOf(packageName), it) }
)
SwitchItem(
R.string.hide, icon = R.drawable.visibility_off_fill0,
getState = { dpm.isApplicationHidden(receiver, packageName) },
onCheckedChange = { dpm.setApplicationHidden(receiver, packageName, it) }
)
SwitchItem(
R.string.block_uninstall, icon = R.drawable.delete_forever_fill0,
getState = { dpm.isUninstallBlocked(receiver, packageName) },
onCheckedChange = { dpm.setUninstallBlocked(receiver, packageName, it) }
)
if(VERSION.SDK_INT >= 30) SwitchItem(
R.string.disable_user_control, icon = R.drawable.do_not_touch_fill0,
getState = { packageName in dpm.getUserControlDisabledPackages(receiver) },
onCheckedChange = { state ->
dpm.setUserControlDisabledPackages(receiver,
dpm.getUserControlDisabledPackages(receiver).let { if(state) it.plus(packageName) else it.minus(packageName) }
)
}
)
if(VERSION.SDK_INT >= 28) SwitchItem(
R.string.disable_metered_data, icon = R.drawable.money_off_fill0,
getState = { packageName in dpm.getMeteredDataDisabledPackages(receiver) },
onCheckedChange = { state ->
dpm.setMeteredDataDisabledPackages(receiver,
dpm.getMeteredDataDisabledPackages(receiver).let { if(state) it.plus(packageName) else it.minus(packageName) }
)
}
)
if(VERSION.SDK_INT >= 28) SwitchItem(
R.string.keep_after_uninstall, icon = R.drawable.delete_fill0,
getState = { dpm.getKeepUninstalledPackages(receiver)?.contains(packageName) == true },
onCheckedChange = { state ->
dpm.setKeepUninstalledPackages(receiver,
dpm.getKeepUninstalledPackages(receiver)?.let { if(state) it.plus(packageName) else it.minus(packageName) } ?: listOf(packageName)
)
}
)
if(VERSION.SDK_INT >= 28) FunctionItem(R.string.clear_app_storage, icon = R.drawable.mop_fill0) { dialog = 1 }
FunctionItem(R.string.uninstall, icon = R.drawable.delete_fill0) { dialog = 2 }
}
if(dialog == 1 && VERSION.SDK_INT >= 28) ClearAppStorageDialog(packageName) { dialog = 0 }
if(dialog == 2) UninstallAppDialog(packageName) { dialog = 0 }
}
@Serializable object Suspend
@RequiresApi(24)
@Composable
fun SuspendScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var packageName by remember { mutableStateOf("") }
val packages = remember { mutableStateListOf<AppInfo>() }
fun refresh() {
val pm = context.packageManager
packages.clear()
pm.getInstalledApplications(getInstalledAppsFlags).filter { dpm.isPackageSuspended(receiver, it.packageName) }.forEach {
packages += it.retrieveAppInfo(pm)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.suspend, onNavigateUp) {
items(packages, { it.name }) {
ApplicationItem(it) {
dpm.setPackagesSuspended(receiver, arrayOf(it.name), false)
refresh()
}
}
item {
Column(Modifier.padding(horizontal = HorizontalPadding)) {
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it }
Button(
{
if(dpm.setPackagesSuspended(receiver, arrayOf(packageName), true).isEmpty()) packageName = ""
else context.showOperationResultToast(false)
refresh()
},
Modifier.fillMaxWidth(),
packageName.isValidPackageName
) {
Text(stringResource(R.string.suspend))
}
Notes(R.string.info_suspend_app)
}
}
}
}
@Serializable object Hide
@Composable
fun HideScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var packageName by remember { mutableStateOf("") }
val packages = remember { mutableStateListOf<AppInfo>() }
fun refresh() {
val pm = context.packageManager
packages.clear()
pm.getInstalledApplications(getInstalledAppsFlags).filter { dpm.isApplicationHidden(receiver, it.packageName) }.forEach {
packages += it.retrieveAppInfo(pm)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.hide, onNavigateUp) {
items(packages, { it.name }) {
ApplicationItem(it) {
dpm.setApplicationHidden(receiver, it.name, false)
refresh()
}
}
item {
Column(Modifier.padding(horizontal = HorizontalPadding)) {
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it }
Button(
{
if(dpm.setApplicationHidden(receiver, packageName, true)) packageName = ""
else context.showOperationResultToast(false)
refresh()
},
Modifier.fillMaxWidth(),
packageName.isValidPackageName
) {
Text(stringResource(R.string.hide))
}
}
}
}
}
@Serializable object BlockUninstall
@Composable
fun BlockUninstallScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var packageName by remember { mutableStateOf("") }
val packages = remember { mutableStateListOf<AppInfo>() }
fun refresh() {
val pm = context.packageManager
packages.clear()
pm.getInstalledApplications(getInstalledAppsFlags).filter { dpm.isUninstallBlocked(receiver, it.packageName) }.forEach {
packages += it.retrieveAppInfo(pm)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.block_uninstall, onNavigateUp) {
items(packages, { it.name }) {
ApplicationItem(it) {
dpm.setUninstallBlocked(receiver, it.name, false)
refresh()
}
}
item {
Column(Modifier.padding(horizontal = HorizontalPadding)) {
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it }
Button(
{
dpm.setUninstallBlocked(receiver, packageName, true)
packageName = ""
refresh()
},
Modifier.fillMaxWidth(),
packageName.isValidPackageName
) {
Text(stringResource(R.string.block_uninstall))
}
}
}
}
}
@Serializable object DisableUserControl
@RequiresApi(30)
@Composable
fun DisableUserControlScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val packages = remember { mutableStateListOf<AppInfo>() }
fun refresh() {
val pm = context.packageManager
packages.clear()
dpm.getUserControlDisabledPackages(receiver).forEach {
packages += pm.retrieveAppInfo(it)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.disable_user_control, onNavigateUp) {
items(packages, { it.name }) {
ApplicationItem(it) {
dpm.setUserControlDisabledPackages(receiver, packages.minus(it).map { it.name })
refresh()
}
}
item {
var packageName by remember { mutableStateOf("") }
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it }
Button(
{
dpm.setUserControlDisabledPackages(receiver, packages.map { it.name } + packageName)
refresh()
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 8.dp),
) {
Text(stringResource(R.string.add))
}
Notes(R.string.info_disable_user_control, HorizontalPadding)
}
}
}
@Serializable data class PermissionsManager(val packageName: String? = null)
@RequiresApi(23)
@Composable
fun PermissionsManagerScreen(onNavigateUp: () -> Unit, param: PermissionsManager) {
val packageNameParam = param.packageName
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var packageName by remember { mutableStateOf(packageNameParam ?: "") }
var selectedPermission by remember { mutableStateOf<PermissionItem?>(null) }
val statusMap = remember { mutableStateMapOf<String, Int>() }
LaunchedEffect(packageName) {
if(packageName.isValidPackageName) {
permissionList().forEach { statusMap[it.permission] = dpm.getPermissionGrantState(receiver, packageName, it.permission) }
} else {
statusMap.clear()
}
}
MyLazyScaffold(R.string.permissions, onNavigateUp) {
item {
if(packageNameParam == null) {
PackageNameTextField(packageName, Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
Spacer(Modifier.padding(vertical = 4.dp))
}
}
items(permissionList(), { it.permission }) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(packageName.isValidPackageName) {
selectedPermission = it
}
.padding(8.dp)
) {
Icon(painterResource(it.icon), null, Modifier.padding(horizontal = 12.dp))
Column {
val state = when(statusMap[it.permission]) {
PERMISSION_GRANT_STATE_DEFAULT -> R.string.default_stringres
PERMISSION_GRANT_STATE_GRANTED -> R.string.granted
PERMISSION_GRANT_STATE_DENIED -> R.string.denied
else -> R.string.unknown
}
Text(stringResource(it.label))
Text(stringResource(state), Modifier.alpha(0.7F), style = typography.bodyMedium)
}
}
}
item {
Spacer(Modifier.padding(vertical = 30.dp))
}
}
if(selectedPermission != null) {
fun changeState(state: Int) {
dpm.setPermissionGrantState(receiver, packageName, selectedPermission!!.permission, state)
statusMap[selectedPermission!!.permission] = dpm.getPermissionGrantState(receiver, packageName, selectedPermission!!.permission)
selectedPermission = null
}
@Composable
fun GrantPermissionItem(label: Int, status: Int) {
val selected = statusMap[selectedPermission!!.permission] == 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)
}
}
AlertDialog(
onDismissRequest = { selectedPermission = null },
confirmButton = { TextButton({ selectedPermission = null }) { Text(stringResource(R.string.cancel)) } },
title = { Text(stringResource(selectedPermission!!.label)) },
text = {
Column {
Text(selectedPermission!!.permission)
Spacer(Modifier.padding(vertical = 4.dp))
if(!(VERSION.SDK_INT >= 31 && selectedPermission!!.profileOwnerRestricted && context.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)
}
}
)
}
}
@Serializable object DisableMeteredData
@RequiresApi(28)
@Composable
fun DisableMeteredDataScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
var packageName by remember { mutableStateOf("") }
val packages = remember { mutableStateListOf<AppInfo>() }
fun refresh() {
val pm = context.packageManager
packages.clear()
dpm.getMeteredDataDisabledPackages(receiver).forEach {
packages += pm.retrieveAppInfo(it)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.disable_metered_data, onNavigateUp) {
items(packages, { it.name }) {
ApplicationItem(it) {
dpm.setMeteredDataDisabledPackages(receiver, packages.minus(it).map { it.name })
refresh()
}
}
item {
PackageNameTextField(packageName, Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
Button(
{
if(dpm.setMeteredDataDisabledPackages(receiver, packages.map { it.name } + packageName).isEmpty()) {
packageName = ""
} else {
context.showOperationResultToast(false)
}
refresh()
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
}
}
}
}
@Serializable object ClearAppStorage
@RequiresApi(28)
@Composable
fun ClearAppStorageScreen(onNavigateUp: () -> Unit) {
var dialog by remember { mutableStateOf(false) }
var packageName by remember { mutableStateOf("") }
MyScaffold(R.string.clear_app_storage, onNavigateUp) {
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it }
Button(
{ dialog = true },
Modifier.fillMaxWidth(),
packageName.isValidPackageName
) {
Text(stringResource(R.string.clear))
}
}
if(dialog) ClearAppStorageDialog(packageName) { dialog = false }
}
@RequiresApi(28)
@Composable
private fun ClearAppStorageDialog(packageName: String, onClose: () -> Unit) {
val context = LocalContext.current
var clearing by remember { mutableStateOf(false) }
AlertDialog(
title = { Text(stringResource(R.string.clear_app_storage)) },
text = {
if(clearing) LinearProgressIndicator(Modifier.fillMaxWidth())
},
confirmButton = {
TextButton(
{
clearing = true
context.getDPM().clearApplicationUserData(
context.getReceiver(), packageName, Executors.newSingleThreadExecutor()
) { _, it ->
Looper.prepare()
context.showOperationResultToast(it)
onClose()
}
},
enabled = !clearing,
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(onClose, enabled = !clearing) { Text(stringResource(R.string.cancel)) }
},
onDismissRequest = onClose
)
}
@Serializable object UninstallApp
@Composable
fun UninstallAppScreen(onNavigateUp: () -> Unit) {
var dialog by remember { mutableStateOf(false) }
var packageName by remember { mutableStateOf("") }
MyScaffold(R.string.uninstall_app, onNavigateUp) {
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it }
Button(
{ dialog = true },
Modifier.fillMaxWidth(),
packageName.isValidPackageName
) {
Text(stringResource(R.string.uninstall))
}
}
if(dialog) UninstallAppDialog(packageName) { dialog = false }
}
@Composable
private fun UninstallAppDialog(packageName: String, onClose: () -> Unit) {
val context = LocalContext.current
var uninstalling by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
AlertDialog(
title = { Text(stringResource(R.string.uninstall)) },
text = {
if(errorMessage != null) Text(errorMessage!!)
if(uninstalling) LinearProgressIndicator(Modifier.fillMaxWidth())
},
confirmButton = {
TextButton(
{
uninstalling = true
uninstallPackage(context, packageName) {
uninstalling = false
if(it == null) onClose() else errorMessage = it
}
},
enabled = !uninstalling,
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(onClose, enabled = !uninstalling) { Text(stringResource(R.string.cancel)) }
},
onDismissRequest = onClose
)
}
@Serializable object KeepUninstalledPackages
@RequiresApi(28)
@Composable
fun KeepUninstalledPackagesScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val packages = remember { mutableStateListOf<AppInfo>() }
fun refresh() {
val pm = context.packageManager
packages.clear()
dpm.getKeepUninstalledPackages(receiver)?.forEach {
packages += pm.retrieveAppInfo(it)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.keep_uninstalled_packages, onNavigateUp) {
items(packages, { it.name }) {
ApplicationItem(it) {
dpm.setKeepUninstalledPackages(receiver, packages.minus(it).map { it.name })
refresh()
}
}
item {
var packageName by remember { mutableStateOf("") }
PackageNameTextField(packageName, Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
Button(
{
dpm.setKeepUninstalledPackages(receiver, packages.map { it.name } + packageName)
packageName = ""
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 8.dp),
packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
}
Notes(R.string.info_keep_uninstalled_apps, HorizontalPadding)
}
}
}
@Serializable object InstallExistingApp
@RequiresApi(28)
@Composable
fun InstallExistingAppScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
MyScaffold(R.string.install_existing_app, onNavigateUp) {
var packageName by remember { mutableStateOf("") }
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it }
Button(
{
context.showOperationResultToast(
context.getDPM().installExistingPackage(context.getReceiver(), packageName)
)
},
Modifier.fillMaxWidth(),
packageName.isValidPackageName
) {
Text(stringResource(R.string.install))
}
Notes(R.string.info_install_existing_app)
}
}
@Serializable object CrossProfilePackages
@RequiresApi(30)
@Composable
fun CrossProfilePackagesScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val packages = remember { mutableStateListOf<AppInfo>() }
fun refresh() {
val pm = context.packageManager
packages.clear()
dpm.getCrossProfilePackages(receiver).forEach {
packages += pm.retrieveAppInfo(it)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.cross_profile_apps, onNavigateUp) {
items(packages, { it.name }) {
ApplicationItem(it) {
dpm.setCrossProfilePackages(receiver, packages.minus(it).map { it.name }.toSet())
refresh()
}
}
item {
var packageName by remember { mutableStateOf("") }
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it }
Button(
{
dpm.setCrossProfilePackages(receiver, packages.map { it.name }.toSet() + packageName)
packageName = ""
refresh()
},
Modifier.fillMaxWidth(),
packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
}
}
}
}
@Serializable object CrossProfileWidgetProviders
@Composable
fun CrossProfileWidgetProvidersScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val packages = remember { mutableStateListOf<AppInfo>() }
fun refresh() {
val pm = context.packageManager
packages.clear()
dpm.getCrossProfileWidgetProviders(receiver).forEach {
packages += pm.retrieveAppInfo(it)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.cross_profile_widget, onNavigateUp) {
items(packages, { it.name }) {
ApplicationItem(it) {
dpm.removeCrossProfileWidgetProvider(receiver, it.name)
refresh()
}
}
item {
var packageName by remember { mutableStateOf("") }
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it }
Button(
{
dpm.addCrossProfileWidgetProvider(receiver, packageName)
packageName = ""
refresh()
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
}
}
}
}
@Serializable object CredentialManagerPolicy
@RequiresApi(34)
@Composable
fun CredentialManagerPolicyScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val pm = context.packageManager
val dpm = context.getDPM()
var policyType by remember{ mutableIntStateOf(-1) }
val packages = remember { mutableStateListOf<AppInfo>() }
fun refresh() {
val policy = dpm.credentialManagerPolicy
policyType = policy?.policyType ?: -1
packages.clear()
policy?.packageNames?.forEach {
packages += pm.retrieveAppInfo(it)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.credential_manager_policy, onNavigateUp) {
item {
mapOf(
-1 to R.string.none,
PackagePolicy.PACKAGE_POLICY_BLOCKLIST to R.string.blacklist,
PackagePolicy.PACKAGE_POLICY_ALLOWLIST to R.string.whitelist,
PackagePolicy.PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM to R.string.whitelist_and_system_app
).forEach { (key, value) ->
FullWidthRadioButtonItem(value, policyType == key) { policyType = key }
}
Spacer(Modifier.padding(vertical = 4.dp))
}
items(packages, { it.name }) {
ApplicationItem(it) { packages -= it }
}
item {
Column(Modifier.padding(horizontal = HorizontalPadding)) {
var packageName by remember { mutableStateOf("") }
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it }
Button(
{
packages += pm.retrieveAppInfo(packageName)
},
Modifier.fillMaxWidth(),
enabled = packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
}
Button(
{
try {
if(policyType != -1 && packages.isNotEmpty()) {
dpm.credentialManagerPolicy = PackagePolicy(policyType, packages.map { it.name }.toSet())
} else {
dpm.credentialManagerPolicy = null
}
context.showOperationResultToast(true)
} catch(_: IllegalArgumentException) {
context.showOperationResultToast(false)
} finally {
refresh()
}
},
Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.apply))
}
}
}
}
}
@Serializable object PermittedAccessibilityServices
@Composable
fun PermittedAccessibilityServicesScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val pm = context.packageManager
val dpm = context.getDPM()
val receiver = context.getReceiver()
val packages = remember { mutableStateListOf<AppInfo>() }
var allowAll by remember { mutableStateOf(true) }
fun refresh() {
packages.clear()
val list = dpm.getPermittedAccessibilityServices(receiver)
allowAll = list == null
list?.forEach {
packages += pm.retrieveAppInfo(it)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.permitted_accessibility_services, onNavigateUp) {
item {
SwitchItem(R.string.allow_all, state = allowAll, onCheckedChange = { allowAll = it })
}
items(packages, { it.name }) {
ApplicationItem(it) { packages -= it }
}
item {
var packageName by remember { mutableStateOf("") }
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it }
Button(
{
packages += pm.retrieveAppInfo(packageName)
packageName = ""
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
}
Button(
{
val result = dpm.setPermittedAccessibilityServices(receiver, if(allowAll) null else packages.map { it.name })
context.showOperationResultToast(result)
refresh()
},
Modifier.fillMaxWidth().padding(top = 8.dp).padding(horizontal = HorizontalPadding)
) {
Text(stringResource(R.string.apply))
}
Notes(R.string.system_accessibility_always_allowed, HorizontalPadding)
}
}
}
@Serializable object PermittedInputMethods
@Composable
fun PermittedInputMethodsScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
val pm = context.packageManager
val dpm = context.getDPM()
val receiver = context.getReceiver()
val packages = remember { mutableStateListOf<AppInfo>() }
var allowAll by remember { mutableStateOf(true) }
fun refresh() {
packages.clear()
val list = dpm.getPermittedInputMethods(receiver)
allowAll = list == null
list?.forEach {
packages += pm.retrieveAppInfo(it)
}
}
LaunchedEffect(Unit) { refresh() }
MyLazyScaffold(R.string.permitted_ime, onNavigateUp) {
item {
SwitchItem(R.string.allow_all, state = allowAll, onCheckedChange = { allowAll = it })
}
items(packages, { it.name }) {
ApplicationItem(it) { packages -= it }
}
item {
var packageName by remember { mutableStateOf("") }
PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it }
Button(
{
packages += pm.retrieveAppInfo(packageName)
packageName = ""
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
}
Button(
{
val result = dpm.setPermittedInputMethods(receiver, if(allowAll) null else packages.map { it.name })
context.showOperationResultToast(result)
refresh()
},
Modifier.fillMaxWidth().padding(top = 8.dp).padding(horizontal = HorizontalPadding)
) {
Text(stringResource(R.string.apply))
}
Notes(R.string.system_ime_always_allowed, HorizontalPadding)
}
}
}
@Serializable object EnableSystemApp
@Composable
fun EnableSystemAppScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
MyScaffold(R.string.enable_system_app, onNavigateUp) {
var packageName by remember { mutableStateOf("") }
Spacer(Modifier.padding(vertical = 4.dp))
PackageNameTextField(packageName, Modifier.padding(bottom = 8.dp)) { packageName = it }
Button(
{
context.getDPM().enableSystemApp(context.getReceiver(), packageName)
packageName = ""
context.showOperationResultToast(true)
},
Modifier.fillMaxWidth(),
packageName.isValidPackageName
) {
Text(stringResource(R.string.enable))
}
}
}
@Serializable object SetDefaultDialer
@RequiresApi(34)
@Composable
fun SetDefaultDialerScreen(onNavigateUp: () -> Unit) {
val context = LocalContext.current
var errorMessage by remember { mutableStateOf<String?>(null) }
MyScaffold(R.string.set_default_dialer, onNavigateUp) {
var packageName by remember { mutableStateOf("") }
Spacer(Modifier.padding(vertical = 4.dp))
PackageNameTextField(packageName, Modifier.padding(bottom = 8.dp)) { packageName = it }
Button(
{
try {
context.getDPM().setDefaultDialerApplication(packageName)
context.showOperationResultToast(true)
} catch(e: Exception) {
errorMessage = e.message
}
},
Modifier.fillMaxWidth(),
packageName.isValidPackageName
) {
Text(stringResource(R.string.set))
}
}
ErrorDialog(errorMessage) { errorMessage = null }
}
private fun uninstallPackage(context: Context, packageName: String, onComplete: (String?) -> Unit) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
if(statusExtra == PackageInstaller.STATUS_PENDING_USER_ACTION) {
@SuppressWarnings("UnsafeIntentLaunch")
context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?)
} else {
context.unregisterReceiver(this)
if(statusExtra == PackageInstaller.STATUS_SUCCESS) {
onComplete(null)
} else {
onComplete(parsePackageInstallerMessage(context, intent))
}
}
}
}
ContextCompat.registerReceiver(
context, receiver, IntentFilter(AppInstallerViewModel.ACTION), null,
null, ContextCompat.RECEIVER_EXPORTED
)
val pi = if(VERSION.SDK_INT >= 34) {
PendingIntent.getBroadcast(
context, 0, Intent(AppInstallerViewModel.ACTION),
PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
).intentSender
} else {
PendingIntent.getBroadcast(context, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender
}
context.getPackageInstaller().uninstall(packageName, pi)
}