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() } 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() } 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() } 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() } 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(null) } val statusMap = remember { mutableStateMapOf() } 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() } 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(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() } 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() } 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() } 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() } 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() } 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() } 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(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) }