From 80c1ddb36c21baf7e1672c3bc6baf9ccbc87f465 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Wed, 17 Sep 2025 18:29:52 +0800 Subject: [PATCH] Optimize Applications and PackageChooser --- app/proguard-rules.pro | 2 + app/src/main/AndroidManifest.xml | 5 - .../com/bintianqi/owndroid/DhizukuServer.kt | 1 - .../com/bintianqi/owndroid/MainActivity.kt | 144 +++- .../com/bintianqi/owndroid/MyViewModel.kt | 365 ++++++++ .../com/bintianqi/owndroid/PackageChooser.kt | 74 +- .../java/com/bintianqi/owndroid/Settings.kt | 30 +- .../main/java/com/bintianqi/owndroid/Utils.kt | 9 - .../bintianqi/owndroid/dpm/Applications.kt | 796 +++++------------- .../java/com/bintianqi/owndroid/dpm/DPM.kt | 1 - .../com/bintianqi/owndroid/dpm/Network.kt | 63 +- .../com/bintianqi/owndroid/dpm/Permissions.kt | 31 +- .../java/com/bintianqi/owndroid/dpm/System.kt | 68 +- 13 files changed, 787 insertions(+), 802 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index b19edd8..48b55e8 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -24,3 +24,5 @@ -dontwarn android.app.ActivityThread -dontwarn android.app.ContextImpl -dontwarn android.app.LoadedApk + +-keep class com.bintianqi.owndroid.MyViewModel { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f71f941..bdbe7d4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,11 +60,6 @@ - Unit) { val lifecycleOwner = LocalLifecycleOwner.current fun navigateUp() { navController.navigateUp() } fun navigate(destination: Any) { navController.navigate(destination) } + fun choosePackage() { + navController.navigate(ApplicationsList(false)) + } LaunchedEffect(Unit) { if(!Privilege.status.value.activated) { navController.navigate(WorkModes(false)) { @@ -306,7 +302,9 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { DhizukuServerSettingsScreen(::navigateUp) } composable { DelegatedAdminsScreen(::navigateUp, ::navigate) } - composable{ AddDelegatedAdminScreen(it.toRoute(), ::navigateUp) } + composable{ + AddDelegatedAdminScreen(vm.chosenPackage, ::choosePackage, it.toRoute(), ::navigateUp) + } composable { DeviceInfoScreen(::navigateUp) } composable { LockScreenInfoScreen(::navigateUp) } composable { SupportMessageScreen(::navigateUp) } @@ -331,7 +329,9 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { PermissionPolicyScreen(::navigateUp) } composable { MtePolicyScreen(::navigateUp) } composable { NearbyStreamingPolicyScreen(::navigateUp) } - composable { LockTaskModeScreen(::navigateUp) } + composable { + LockTaskModeScreen(vm.chosenPackage, ::choosePackage, ::navigateUp) + } composable { CaCertScreen(::navigateUp) } composable { SecurityLoggingScreen(::navigateUp) } composable { DisableAccountManagementScreen(::navigateUp) } @@ -346,12 +346,16 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { AddNetworkScreen(it.arguments!!, ::navigateUp) } composable { WifiSecurityLevelScreen(::navigateUp) } composable { WifiSsidPolicyScreen(::navigateUp) } - composable { NetworkStatsScreen(::navigateUp, ::navigate) } + composable { + NetworkStatsScreen(vm.chosenPackage, ::choosePackage, ::navigateUp, ::navigate) + } composable(mapOf(serializableNavTypePair>())) { NetworkStatsViewerScreen(it.toRoute(), ::navigateUp) } composable { PrivateDnsScreen(::navigateUp) } - composable { AlwaysOnVpnPackageScreen(::navigateUp) } + composable { + AlwaysOnVpnPackageScreen(vm.chosenPackage, ::choosePackage, ::navigateUp) + } composable { RecommendedGlobalProxyScreen(::navigateUp) } composable { NetworkLoggingScreen(::navigateUp) } composable { WifiAuthKeypairScreen(::navigateUp) } @@ -368,14 +372,25 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { DeleteWorkProfileScreen(::navigateUp) } composable { - AppChooserScreen(it.toRoute(), { dest -> - if(dest == null) navigateUp() else navigate(ApplicationDetails(dest)) + val canSwitchView = (it.toRoute() as ApplicationsList).canSwitchView + AppChooserScreen( + canSwitchView, vm.installedPackages, vm.refreshPackagesProgress, { name -> + if (canSwitchView) { + if (name == null) { + navigateUp() + } else { + navigate(ApplicationDetails(name)) + } + } else { + if (name != null) vm.chosenPackage.trySend(name) + navigateUp() + } }, { SP.applicationsListView = false navController.navigate(ApplicationsFeatures) { popUpTo(Home) } - }) + }, vm::refreshPackageList) } composable { ApplicationsFeaturesScreen(::navigateUp, ::navigate) { @@ -385,24 +400,78 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { } } } - composable { ApplicationDetailsScreen(it.toRoute(), ::navigateUp, ::navigate) } - composable { SuspendScreen(::navigateUp) } - composable { HideScreen(::navigateUp) } - composable { BlockUninstallScreen(::navigateUp) } - composable { DisableUserControlScreen(::navigateUp) } - composable { PermissionsManagerScreen(::navigateUp, it.toRoute()) } - composable { DisableMeteredDataScreen(::navigateUp) } - composable { ClearAppStorageScreen(::navigateUp) } - composable { UninstallAppScreen(::navigateUp) } - composable { KeepUninstalledPackagesScreen(::navigateUp) } - composable { InstallExistingAppScreen(::navigateUp) } - composable { CrossProfilePackagesScreen(::navigateUp) } - composable { CrossProfileWidgetProvidersScreen(::navigateUp) } - composable { CredentialManagerPolicyScreen(::navigateUp) } - composable { PermittedAccessibilityServicesScreen(::navigateUp) } - composable { PermittedInputMethodsScreen(::navigateUp) } - composable { EnableSystemAppScreen(::navigateUp) } - composable { SetDefaultDialerScreen(::navigateUp) } + composable { + ApplicationDetailsScreen(it.toRoute(), vm, ::navigateUp, ::navigate) + } + composable { + PackageFunctionScreen(R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged, + vm::setPackageSuspended, ::navigateUp, vm.chosenPackage, ::choosePackage, + R.string.info_suspend_app) + } + composable { + PackageFunctionScreen(R.string.hide, vm.hiddenPackages, vm::getHiddenPackages, + vm::setPackageHidden, ::navigateUp, vm.chosenPackage, ::choosePackage) + } + composable { + PackageFunctionScreenWithoutResult(R.string.block_uninstall, vm.ubPackages, + vm::getUbPackages, vm::setPackageUb, ::navigateUp, vm.chosenPackage, ::choosePackage) + } + composable { + PackageFunctionScreenWithoutResult(R.string.disable_user_control, vm.ucdPackages, + vm::getUcdPackages, vm::setPackageUcd, ::navigateUp, vm.chosenPackage, + ::choosePackage, R.string.info_disable_user_control) + } + composable { + PermissionsManagerScreen(vm.packagePermissions, vm::getPackagePermissions, + vm::setPackagePermission, ::navigateUp, it.toRoute(), vm.chosenPackage, ::choosePackage) + } + composable { + PackageFunctionScreen(R.string.disable_metered_data, vm.mddPackages, + vm::getMddPackages, vm::setPackageMdd, ::navigateUp, vm.chosenPackage, ::choosePackage) + } + composable { + ClearAppStorageScreen(vm.chosenPackage, ::choosePackage, vm::clearAppData, ::navigateUp) + } + composable { + UninstallAppScreen(vm.chosenPackage, ::choosePackage, vm::uninstallPackage, ::navigateUp) + } + composable { + PackageFunctionScreenWithoutResult(R.string.keep_uninstalled_packages, vm.kuPackages, + vm::getKuPackages, vm::setPackageKu, ::navigateUp, vm.chosenPackage, + ::choosePackage, R.string.info_keep_uninstalled_apps) + } + composable { + InstallExistingAppScreen(vm.chosenPackage, ::choosePackage, + vm::installExistingApp, ::navigateUp) + } + composable { + PackageFunctionScreenWithoutResult(R.string.cross_profile_apps, vm.cpPackages, + vm::getCpPackages, vm::setPackageCp, ::navigateUp, vm.chosenPackage, ::choosePackage) + } + composable { + PackageFunctionScreen(R.string.cross_profile_widget, vm.cpwProviders, + vm::getCpwProviders, vm::setCpwProvider, ::navigateUp, vm.chosenPackage, ::choosePackage) + } + composable { + CredentialManagerPolicyScreen(vm.chosenPackage, ::choosePackage, + vm.cmPackages, vm::getCmPolicy, vm::setCmPackage, vm::setCmPolicy, ::navigateUp) + } + composable { + PermittedAsAndImPackages(R.string.permitted_accessibility_services, + R.string.system_accessibility_always_allowed, vm.chosenPackage, ::choosePackage, + vm.pasPackages, vm::getPasPackages, vm::setPasPackage, vm::setPasPolicy, ::navigateUp) + } + composable { + PermittedAsAndImPackages(R.string.permitted_ime, R.string.system_ime_always_allowed, + vm.chosenPackage, ::choosePackage, vm.pimPackages, vm::getPimPackages, + vm::setPimPackage, vm::setPimPolicy, ::navigateUp) + } + composable { + EnableSystemAppScreen(vm.chosenPackage, ::choosePackage, vm::enableSystemApp, ::navigateUp) + } + composable { + SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp) + } composable { UserRestrictionScreen(::navigateUp) { @@ -497,7 +566,10 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) { }, contentWindowInsets = WindowInsets.ime ) { - Column(Modifier.fillMaxSize().padding(it).verticalScroll(rememberScrollState())) { + Column(Modifier + .fillMaxSize() + .padding(it) + .verticalScroll(rememberScrollState())) { if(privilege.device || privilege.profile) { HomePageItem(R.string.system, R.drawable.android_fill0) { onNavigate(SystemManager) } HomePageItem(R.string.network, R.drawable.wifi_fill0) { onNavigate(Network) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index db9be42..3ea5e76 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -1,10 +1,41 @@ package com.bintianqi.owndroid import android.app.Application +import android.app.PendingIntent +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 androidx.annotation.RequiresApi +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toDrawable import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.application +import androidx.lifecycle.viewModelScope +import com.bintianqi.owndroid.Privilege.DAR +import com.bintianqi.owndroid.Privilege.DPM +import com.bintianqi.owndroid.dpm.AppStatus +import com.bintianqi.owndroid.dpm.getPackageInstaller +import com.bintianqi.owndroid.dpm.isValidPackageName +import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage +import com.bintianqi.owndroid.dpm.permissionList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.util.concurrent.Executors class MyViewModel(application: Application): AndroidViewModel(application) { + val PM = application.packageManager val theme = MutableStateFlow(ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme)) fun changeTheme(newTheme: ThemeSettings) { theme.value = newTheme @@ -12,6 +43,340 @@ class MyViewModel(application: Application): AndroidViewModel(application) { SP.darkTheme = newTheme.darkTheme SP.blackTheme = newTheme.blackTheme } + + val chosenPackage = Channel(1, BufferOverflow.DROP_LATEST) + + val installedPackages = MutableStateFlow(emptyList()) + val refreshPackagesProgress = MutableStateFlow(0F) + fun refreshPackageList() { + viewModelScope.launch(Dispatchers.IO) { + installedPackages.value = emptyList() + val apps = PM.getInstalledApplications(getInstalledAppsFlags) + apps.forEachIndexed { index, info -> + installedPackages.update { + it + getAppInfo(info) + } + refreshPackagesProgress.value = (index + 1).toFloat() / apps.size + } + } + } + fun getAppInfo(info: ApplicationInfo) = + AppInfo(info.packageName, info.loadLabel(PM).toString(), info.loadIcon(PM), info.flags) + fun getAppInfo(name: String): AppInfo { + return try { + getAppInfo(PM.getApplicationInfo(name, getInstalledAppsFlags)) + } catch (_: PackageManager.NameNotFoundException) { + AppInfo(name, "???", Color.Transparent.toArgb().toDrawable(), 0) + } + } + + val suspendedPackages = MutableStateFlow(emptyList()) + @RequiresApi(24) + fun getSuspendedPackaged() { + val packages = PM.getInstalledApplications(getInstalledAppsFlags).filter { + DPM.isPackageSuspended(DAR, it.packageName) + } + suspendedPackages.value = packages.map { getAppInfo(it) } + } + @RequiresApi(24) + fun setPackageSuspended(name: String, status: Boolean): Boolean { + val result = DPM.setPackagesSuspended(DAR, arrayOf(name), status) + getSuspendedPackaged() + return result.isEmpty() + } + + val hiddenPackages = MutableStateFlow(emptyList()) + fun getHiddenPackages() { + viewModelScope.launch { + hiddenPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter { + DPM.isApplicationHidden(DAR, it.packageName) + }.map { getAppInfo(it) } + } + } + fun setPackageHidden(name: String, status: Boolean): Boolean { + val result = DPM.setApplicationHidden(DAR, name, status) + getHiddenPackages() + return result + } + + // Uninstall blocked packages + val ubPackages = MutableStateFlow(emptyList()) + fun getUbPackages() { + viewModelScope.launch { + ubPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter { + DPM.isUninstallBlocked(DAR, it.packageName) + }.map { getAppInfo(it) } + } + } + fun setPackageUb(name: String, status: Boolean) { + DPM.setUninstallBlocked(DAR, name, status) + getUbPackages() + } + + // User control disabled packages + val ucdPackages = MutableStateFlow(emptyList()) + @RequiresApi(30) + fun getUcdPackages() { + ucdPackages.value = DPM.getUserControlDisabledPackages(DAR).map { + getAppInfo(it) + } + } + @RequiresApi(30) + fun setPackageUcd(name: String, status: Boolean) { + DPM.setUserControlDisabledPackages( + DAR, + ucdPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) } + ) + getUcdPackages() + } + + val packagePermissions = MutableStateFlow(emptyMap()) + @RequiresApi(23) + fun getPackagePermissions(name: String) { + if (name.isValidPackageName) { + packagePermissions.value = permissionList().associate { + it.permission to DPM.getPermissionGrantState(DAR, name, it.permission) + } + } else { + packagePermissions.value = emptyMap() + } + } + @RequiresApi(23) + fun setPackagePermission(name: String, permission: String, status: Int): Boolean { + val result = DPM.setPermissionGrantState(DAR, name, permission, status) + getPackagePermissions(name) + return result + } + + // Metered data disabled packages + val mddPackages = MutableStateFlow(emptyList()) + @RequiresApi(28) + fun getMddPackages() { + mddPackages.value = DPM.getMeteredDataDisabledPackages(DAR).map { getAppInfo(it) } + } + @RequiresApi(28) + fun setPackageMdd(name: String, status: Boolean): Boolean { + val result = DPM.setMeteredDataDisabledPackages( + DAR, mddPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) } + ) + getMddPackages() + return result.isEmpty() + } + + // Keep uninstalled packages + val kuPackages = MutableStateFlow(emptyList()) + @RequiresApi(28) + fun getKuPackages() { + kuPackages.value = DPM.getKeepUninstalledPackages(DAR)?.map { getAppInfo(it) } ?: emptyList() + } + @RequiresApi(28) + fun setPackageKu(name: String, status: Boolean) { + DPM.setKeepUninstalledPackages( + DAR, kuPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) } + ) + getKuPackages() + } + + // Cross profile packages + val cpPackages = MutableStateFlow(emptyList()) + @RequiresApi(30) + fun getCpPackages() { + cpPackages.value = DPM.getCrossProfilePackages(DAR).map { getAppInfo(it) } + } + @RequiresApi(30) + fun setPackageCp(name: String, status: Boolean) { + DPM.setCrossProfilePackages( + DAR, + cpPackages.value.map { it.name }.toSet().run { if (status) plus(name) else minus(name) } + ) + getCpPackages() + } + + // Cross-profile widget providers + val cpwProviders = MutableStateFlow(emptyList()) + fun getCpwProviders() { + cpwProviders.value = DPM.getCrossProfileWidgetProviders(DAR).map { getAppInfo(it) } + } + fun setCpwProvider(name: String, status: Boolean): Boolean { + val result = if (status) { + DPM.addCrossProfileWidgetProvider(DAR, name) + } else { + DPM.removeCrossProfileWidgetProvider(DAR, name) + } + getCpwProviders() + return result + } + + @RequiresApi(28) + fun clearAppData(name: String, callback: (Boolean) -> Unit) { + DPM.clearApplicationUserData(DAR, name, Executors.newSingleThreadExecutor()) { _, result -> + callback(result) + } + } + + fun uninstallPackage(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( + application, receiver, IntentFilter(AppInstallerViewModel.ACTION), null, + null, ContextCompat.RECEIVER_EXPORTED + ) + val pi = if(VERSION.SDK_INT >= 34) { + PendingIntent.getBroadcast( + application, 0, Intent(AppInstallerViewModel.ACTION), + PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE + ).intentSender + } else { + PendingIntent.getBroadcast(application, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender + } + application.getPackageInstaller().uninstall(packageName, pi) + } + + @RequiresApi(28) + fun installExistingApp(name: String): Boolean { + return DPM.installExistingPackage(DAR, name) + } + + // Credential manager policy + val cmPackages = MutableStateFlow(emptyList()) + @RequiresApi(34) + fun getCmPolicy(): Int { + return DPM.credentialManagerPolicy?.let { policy -> + cmPackages.value = policy.packageNames.map { getAppInfo(it) } + policy.policyType + } ?: -1 + } + fun setCmPackage(name: String, status: Boolean) { + cmPackages.update { list -> + if (status) list + getAppInfo(name) else list.dropWhile { it.name == name } + } + } + @RequiresApi(34) + fun setCmPolicy(type: Int) { + DPM.credentialManagerPolicy = if (type != -1 && cmPackages.value.isNotEmpty()) { + PackagePolicy(type, cmPackages.value.map { it.name }.toSet()) + } else null + getCmPolicy() + } + + // Permitted input method + val pimPackages = MutableStateFlow(emptyList()) + fun getPimPackages(): Boolean { + return DPM.getPermittedInputMethods(DAR).let { packages -> + pimPackages.value = packages?.map { getAppInfo(it) } ?: emptyList() + packages == null + } + } + fun setPimPackage(name: String, status: Boolean) { + pimPackages.update { packages -> + if (status) packages + getAppInfo(name) else packages.dropWhile { it.name == name } + } + } + fun setPimPolicy(allowAll: Boolean): Boolean { + val result = DPM.setPermittedInputMethods( + DAR, if (allowAll) null else pimPackages.value.map { it.name }) + getPimPackages() + return result + } + + // Permitted accessibility services + val pasPackages = MutableStateFlow(emptyList()) + fun getPasPackages(): Boolean { + return DPM.getPermittedAccessibilityServices(DAR).let { packages -> + pasPackages.value = packages?.map { getAppInfo(it) } ?: emptyList() + packages == null + } + } + fun setPasPackage(name: String, status: Boolean) { + pasPackages.update { packages -> + if (status) packages + getAppInfo(name) else packages.dropWhile { it.name == name } + } + } + fun setPasPolicy(allowAll: Boolean): Boolean { + val result = DPM.setPermittedAccessibilityServices( + DAR, if (allowAll) null else pasPackages.value.map { it.name }) + getPasPackages() + return result + } + + fun enableSystemApp(name: String) { + DPM.enableSystemApp(DAR, name) + } + + val appStatus = MutableStateFlow(AppStatus(false, false, false, false, false, false)) + fun getAppStatus(name: String) { + appStatus.value = AppStatus( + if (VERSION.SDK_INT >= 24) DPM.isPackageSuspended(DAR, name) else false, + DPM.isApplicationHidden(DAR, name), + DPM.isUninstallBlocked(DAR, name), + if (VERSION.SDK_INT >= 30) name in DPM.getUserControlDisabledPackages(DAR) else false, + if (VERSION.SDK_INT >= 28) name in DPM.getMeteredDataDisabledPackages(DAR) else false, + if (VERSION.SDK_INT >= 28) DPM.getKeepUninstalledPackages(DAR)?.contains(name) == true else false + ) + } + // Application details + @RequiresApi(24) + fun adSetPackageSuspended(name: String, status: Boolean) { + DPM.setPackagesSuspended(DAR, arrayOf(name), status) + appStatus.update { it.copy(suspend = DPM.isPackageSuspended(DAR, name)) } + } + fun adSetPackageHidden(name: String, status: Boolean) { + DPM.setApplicationHidden(DAR, name, status) + appStatus.update { it.copy(hide = DPM.isApplicationHidden(DAR, name)) } + } + fun adSetPackageUb(name: String, status: Boolean) { + DPM.setUninstallBlocked(DAR, name, status) + appStatus.update { it.copy(uninstallBlocked = DPM.isUninstallBlocked(DAR, name)) } + } + @RequiresApi(30) + fun adSetPackageUcd(name: String, status: Boolean) { + DPM.setUserControlDisabledPackages(DAR, + DPM.getUserControlDisabledPackages(DAR).run { if (status) plus(name) else minus(name) }) + appStatus.update { + it.copy(userControlDisabled = name in DPM.getUserControlDisabledPackages(DAR)) + } + } + @RequiresApi(28) + fun adSetPackageMdd(name: String, status: Boolean) { + DPM.setMeteredDataDisabledPackages(DAR, + DPM.getMeteredDataDisabledPackages(DAR).run { if (status) plus(name) else minus(name) }) + appStatus.update { + it.copy(meteredDataDisabled = name in DPM.getMeteredDataDisabledPackages(DAR)) + } + } + @RequiresApi(28) + fun adSetPackageKu(name: String, status: Boolean) { + DPM.setKeepUninstalledPackages(DAR, + DPM.getKeepUninstalledPackages(DAR)?.run { if (status) plus(name) else minus(name) } ?: emptyList()) + appStatus.update { + it.copy(keepUninstalled = DPM.getKeepUninstalledPackages(DAR)?.contains(name) == true ) + } + } + + @RequiresApi(34) + fun setDefaultDialer(name: String): Boolean { + return try { + DPM.setDefaultDialerApplication(name) + true + } catch (e: IllegalArgumentException) { + e.printStackTrace() + false + } + } } data class ThemeSettings( diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index c63cf79..10c95ed 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -1,16 +1,9 @@ package com.bintianqi.owndroid -import android.content.Context -import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.os.Build -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable @@ -44,10 +37,8 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -62,35 +53,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import com.google.accompanist.drawablepainter.rememberDrawablePainter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable -class PackageChooserActivity: ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val vm by viewModels() - enableEdgeToEdge() - setContent { - val theme by vm.theme.collectAsStateWithLifecycle() - OwnDroidTheme(theme) { - AppChooserScreen(ApplicationsList(false), { - setResult(0, Intent().putExtra("package", it)) - finish() - }, {}) - } - } - } -} - -val installedApps = MutableStateFlow(emptyList()) - data class AppInfo( val name: String, val label: String, @@ -105,11 +71,14 @@ private fun searchInString(query: String, content: String) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable -fun AppChooserScreen(params: ApplicationsList, onChoosePackage: (String?) -> Unit, onSwitchView: () -> Unit) { - val packages by installedApps.collectAsStateWithLifecycle() - val coroutine = rememberCoroutineScope() +fun AppChooserScreen( + canSwitchView: Boolean, packageList: MutableStateFlow>, + refreshProgress: MutableStateFlow, onChoosePackage: (String?) -> Unit, + onSwitchView: () -> Unit, onRefresh: () -> Unit +) { + val packages by packageList.collectAsStateWithLifecycle() val context = LocalContext.current - var progress by remember { mutableFloatStateOf(1F) } + val progress by refreshProgress.collectAsStateWithLifecycle() var system by rememberSaveable { mutableStateOf(false) } var query by rememberSaveable { mutableStateOf("") } var searchMode by rememberSaveable { mutableStateOf(false) } @@ -119,7 +88,7 @@ fun AppChooserScreen(params: ApplicationsList, onChoosePackage: (String?) -> Uni } val focusMgr = LocalFocusManager.current LaunchedEffect(Unit) { - if(packages.size <= 1) getInstalledApps(coroutine, context) { progress = it } + if(packages.size <= 1) onRefresh() } Scaffold( topBar = { @@ -135,20 +104,17 @@ fun AppChooserScreen(params: ApplicationsList, onChoosePackage: (String?) -> Uni }) { Icon(painter = painterResource(R.drawable.filter_alt_fill0), contentDescription = null) } - IconButton( - { getInstalledApps(coroutine, context) { progress = it } }, - enabled = progress == 1F - ) { + IconButton(onRefresh, enabled = progress == 1F) { Icon(painter = painterResource(R.drawable.refresh_fill0), contentDescription = null) } - if(params.canSwitchView) IconButton(onSwitchView) { + if (canSwitchView) IconButton(onSwitchView) { Icon(Icons.AutoMirrored.Default.List, null) } } }, title = { if(searchMode) { - val fr = FocusRequester() + val fr = remember { FocusRequester() } LaunchedEffect(Unit) { fr.requestFocus() } OutlinedTextField( value = query, @@ -213,23 +179,5 @@ fun AppChooserScreen(params: ApplicationsList, onChoosePackage: (String?) -> Uni } } -fun getInstalledApps(scope: CoroutineScope, context: Context, onProgressUpdated: (Float) -> Unit) { - installedApps.value = emptyList() - scope.launch(Dispatchers.IO) { - val pm = context.packageManager - val apps = pm.getInstalledApplications(getInstalledAppsFlags) - for(pkg in apps) { - val label = pkg.loadLabel(pm).toString() - val icon = pkg.loadIcon(pm) - withContext(Dispatchers.Main) { - installedApps.update { - it + AppInfo(pkg.packageName, label, icon, pkg.flags) - } - onProgressUpdated(installedApps.value.size.toFloat() / apps.size) - } - } - } -} - val getInstalledAppsFlags = if(Build.VERSION.SDK_INT >= 24) PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_UNINSTALLED_PACKAGES else 0 diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 6eb883b..530bccd 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -233,28 +233,38 @@ fun AppLockSettingsScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.app_lo var confirmPassword by remember { mutableStateOf("") } var allowBiometrics by remember { mutableStateOf(SP.biometricsUnlock) } var lockWhenLeaving by remember { mutableStateOf(SP.lockWhenLeaving) } - val fr = FocusRequester() + val fr = remember { FocusRequester() } val alreadySet = !SP.lockPasswordHash.isNullOrEmpty() val isInputLegal = password.length !in 1..3 && (alreadySet || (password.isNotEmpty() && password.isNotBlank())) - Column(Modifier.widthIn(max = 300.dp).align(Alignment.CenterHorizontally)) { + Column(Modifier + .widthIn(max = 300.dp) + .align(Alignment.CenterHorizontally)) { OutlinedTextField( - password, { password = it }, Modifier.fillMaxWidth().padding(vertical = 4.dp), + password, { password = it }, Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), label = { Text(stringResource(R.string.password)) }, supportingText = { Text(stringResource(if(alreadySet) R.string.leave_empty_to_remain_unchanged else R.string.minimum_length_4)) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next), keyboardActions = KeyboardActions { fr.requestFocus() } ) OutlinedTextField( - confirmPassword, { confirmPassword = it }, Modifier.fillMaxWidth().focusRequester(fr), + confirmPassword, { confirmPassword = it }, Modifier + .fillMaxWidth() + .focusRequester(fr), label = { Text(stringResource(R.string.confirm_password)) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done), keyboardActions = KeyboardActions { fm.clearFocus() } ) - if(VERSION.SDK_INT >= 28) Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) { + if(VERSION.SDK_INT >= 28) Row(Modifier + .fillMaxWidth() + .padding(vertical = 6.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) { Text(stringResource(R.string.allow_biometrics)) Switch(allowBiometrics, { allowBiometrics = it }) } - Row(Modifier.fillMaxWidth().padding(bottom = 6.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) { + Row(Modifier + .fillMaxWidth() + .padding(bottom = 6.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) { Text(stringResource(R.string.lock_when_leaving)) Switch(lockWhenLeaving, { lockWhenLeaving = it }) } @@ -302,7 +312,9 @@ fun ApiSettings(onNavigateUp: () -> Unit) { var key by remember { mutableStateOf("") } OutlinedTextField( value = key, onValueChange = { key = it }, label = { Text(stringResource(R.string.api_key)) }, - modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp), readOnly = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), readOnly = true, trailingIcon = { IconButton( onClick = { @@ -316,7 +328,9 @@ fun ApiSettings(onNavigateUp: () -> Unit) { } ) Button( - modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), onClick = { SP.apiKey = key context.showOperationResultToast(true) diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index a19ee32..14fdfff 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -4,13 +4,11 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.ComponentName import android.content.Context -import android.content.Intent import android.content.pm.PackageInfo import android.net.Uri import android.os.Build import android.os.Bundle import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContract import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.compose.runtime.saveable.Saver @@ -107,13 +105,6 @@ inline fun serializableNavTypePair() = Json.encodeToString(value) } -class ChoosePackageContract: ActivityResultContract() { - override fun createIntent(context: Context, input: Nothing?): Intent = - Intent(context, PackageChooserActivity::class.java) - override fun parseResult(resultCode: Int, intent: Intent?): String? = - intent?.getStringExtra("package") -} - fun exportLogs(context: Context, uri: Uri) { context.contentResolver.openOutputStream(uri)?.use { output -> val proc = Runtime.getRuntime().exec("logcat -d") diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index f75fe78..2878e55 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -1,20 +1,12 @@ 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 @@ -26,10 +18,10 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -60,8 +52,6 @@ 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 @@ -70,7 +60,6 @@ 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.graphics.toArgb import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -79,20 +68,15 @@ 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 androidx.core.graphics.drawable.toDrawable +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.MyViewModel import com.bintianqi.owndroid.Privilege 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 @@ -102,19 +86,9 @@ 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.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.Serializable -import java.util.concurrent.Executors - -fun PackageManager.retrieveAppInfo(packageName: String): AppInfo { - return try { - getApplicationInfo(packageName, getInstalledAppsFlags).retrieveAppInfo(this) - } catch (_: PackageManager.NameNotFoundException) { - AppInfo(packageName, "???", Color.Transparent.toArgb().toDrawable(), 0) - } -} - -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) @@ -142,18 +116,16 @@ fun LazyItemScope.ApplicationItem(info: AppInfo, onClear: () -> Unit) { } @Composable -fun PackageNameTextField(value: String, modifier: Modifier = Modifier, onValueChange: (String) -> Unit) { - val launcher = rememberLauncherForActivityResult(ChoosePackageContract()) { - if(it != null) onValueChange(it) - } +fun PackageNameTextField( + value: String, onChoosePackage: () -> Unit, + modifier: Modifier = Modifier, onValueChange: (String) -> Unit +) { val fm = LocalFocusManager.current OutlinedTextField( value, onValueChange, Modifier.fillMaxWidth().then(modifier), label = { Text(stringResource(R.string.package_name)) }, trailingIcon = { - IconButton({ - launcher.launch(null) - }) { + IconButton(onChoosePackage) { Icon(Icons.AutoMirrored.Default.List, null) } }, @@ -243,258 +215,102 @@ fun ApplicationsFeaturesScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Un @Serializable data class ApplicationDetails(val packageName: String) +data class AppStatus( + val suspend: Boolean, + val hide: Boolean, + val uninstallBlocked: Boolean, + val userControlDisabled: Boolean, + val meteredDataDisabled: Boolean, + val keepUninstalled: Boolean +) + @Composable -fun ApplicationDetailsScreen(param: ApplicationDetails, onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { +fun ApplicationDetailsScreen( + param: ApplicationDetails, vm: MyViewModel, onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit +) { val packageName = param.packageName - val context = LocalContext.current val privilege by Privilege.status.collectAsStateWithLifecycle() - val pm = context.packageManager var dialog by remember { mutableIntStateOf(0) } // 1: clear storage, 2: uninstall - val info = pm.getApplicationInfo(packageName, getInstalledAppsFlags) + val info = vm.getAppInfo(packageName) + val status by vm.appStatus.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { vm.getAppStatus(packageName) } 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) + Image(rememberDrawablePainter(info.icon), null, Modifier.size(50.dp)) + Text(info.label, Modifier.padding(top = 4.dp)) + Text(info.name, 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 = { Privilege.DPM.isPackageSuspended(Privilege.DAR, packageName) }, - onCheckedChange = { Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(packageName), it) } + R.string.suspend, icon = R.drawable.block_fill0, state = status.suspend, + onCheckedChange = { vm.adSetPackageSuspended(packageName, it) } ) SwitchItem( R.string.hide, icon = R.drawable.visibility_off_fill0, - getState = { Privilege.DPM.isApplicationHidden(Privilege.DAR, packageName) }, - onCheckedChange = { Privilege.DPM.setApplicationHidden(Privilege.DAR, packageName, it) } + state = status.hide, + onCheckedChange = { vm.adSetPackageHidden(packageName, it) } ) SwitchItem( R.string.block_uninstall, icon = R.drawable.delete_forever_fill0, - getState = { Privilege.DPM.isUninstallBlocked(Privilege.DAR, packageName) }, - onCheckedChange = { Privilege.DPM.setUninstallBlocked(Privilege.DAR, packageName, it) } + state = status.uninstallBlocked, + onCheckedChange = { vm.adSetPackageUb(packageName, it) } ) if(VERSION.SDK_INT >= 30) SwitchItem( R.string.disable_user_control, icon = R.drawable.do_not_touch_fill0, - getState = { packageName in Privilege.DPM.getUserControlDisabledPackages(Privilege.DAR) }, - onCheckedChange = { state -> - Privilege.DPM.setUserControlDisabledPackages(Privilege.DAR, - Privilege.DPM.getUserControlDisabledPackages(Privilege.DAR).let { if(state) it.plus(packageName) else it.minus(packageName) } - ) - } + state = status.userControlDisabled, + onCheckedChange = { vm.adSetPackageUcd(packageName, it) } ) if(VERSION.SDK_INT >= 28) SwitchItem( R.string.disable_metered_data, icon = R.drawable.money_off_fill0, - getState = { packageName in Privilege.DPM.getMeteredDataDisabledPackages(Privilege.DAR) }, - onCheckedChange = { state -> - Privilege.DPM.setMeteredDataDisabledPackages(Privilege.DAR, - Privilege.DPM.getMeteredDataDisabledPackages(Privilege.DAR).let { if(state) it.plus(packageName) else it.minus(packageName) } - ) - } + state = status.meteredDataDisabled, + onCheckedChange = { vm.adSetPackageMdd(packageName, it) } ) if(privilege.device && VERSION.SDK_INT >= 28) SwitchItem( R.string.keep_after_uninstall, icon = R.drawable.delete_fill0, - getState = { Privilege.DPM.getKeepUninstalledPackages(Privilege.DAR)?.contains(packageName) == true }, - onCheckedChange = { state -> - Privilege.DPM.setKeepUninstalledPackages(Privilege.DAR, - Privilege.DPM.getKeepUninstalledPackages(Privilege.DAR)?.let { if(state) it.plus(packageName) else it.minus(packageName) } ?: listOf(packageName) - ) - } + state = status.keepUninstalled, + onCheckedChange = { vm.adSetPackageKu(packageName, it) } ) 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 } + if(dialog == 1 && VERSION.SDK_INT >= 28) + ClearAppStorageDialog(packageName, vm::clearAppData) { dialog = 0 } + if(dialog == 2) UninstallAppDialog(packageName, vm::uninstallPackage) { dialog = 0 } } @Serializable object Suspend -@RequiresApi(24) -@Composable -fun SuspendScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - var packageName by remember { mutableStateOf("") } - val packages = remember { mutableStateListOf() } - fun refresh() { - val pm = context.packageManager - packages.clear() - pm.getInstalledApplications(getInstalledAppsFlags).filter { - Privilege.DPM.isPackageSuspended(Privilege.DAR, it.packageName) - }.forEach { - packages += it.retrieveAppInfo(pm) - } - } - LaunchedEffect(Unit) { refresh() } - MyLazyScaffold(R.string.suspend, onNavigateUp) { - items(packages, { it.name }) { - ApplicationItem(it) { - Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(it.name), false) - refresh() - } - } - item { - Column(Modifier.padding(horizontal = HorizontalPadding)) { - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } - Button( - { - if(Privilege.DPM.setPackagesSuspended(Privilege.DAR, 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 - var packageName by remember { mutableStateOf("") } - val packages = remember { mutableStateListOf() } - fun refresh() { - val pm = context.packageManager - packages.clear() - pm.getInstalledApplications(getInstalledAppsFlags).filter { Privilege.DPM.isApplicationHidden(Privilege.DAR, it.packageName) }.forEach { - packages += it.retrieveAppInfo(pm) - } - } - LaunchedEffect(Unit) { refresh() } - MyLazyScaffold(R.string.hide, onNavigateUp) { - items(packages, { it.name }) { - ApplicationItem(it) { - Privilege.DPM.setApplicationHidden(Privilege.DAR, it.name, false) - refresh() - } - } - item { - Column(Modifier.padding(horizontal = HorizontalPadding)) { - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } - Button( - { - if(Privilege.DPM.setApplicationHidden(Privilege.DAR, 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 - var packageName by remember { mutableStateOf("") } - val packages = remember { mutableStateListOf() } - fun refresh() { - val pm = context.packageManager - packages.clear() - pm.getInstalledApplications(getInstalledAppsFlags).filter { Privilege.DPM.isUninstallBlocked(Privilege.DAR, it.packageName) }.forEach { - packages += it.retrieveAppInfo(pm) - } - } - LaunchedEffect(Unit) { refresh() } - MyLazyScaffold(R.string.block_uninstall, onNavigateUp) { - items(packages, { it.name }) { - ApplicationItem(it) { - Privilege.DPM.setUninstallBlocked(Privilege.DAR, it.name, false) - refresh() - } - } - item { - Column(Modifier.padding(horizontal = HorizontalPadding)) { - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } - Button( - { - Privilege.DPM.setUninstallBlocked(Privilege.DAR, 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 packages = remember { mutableStateListOf() } - fun refresh() { - val pm = context.packageManager - packages.clear() - Privilege.DPM.getUserControlDisabledPackages(Privilege.DAR).forEach { - packages += pm.retrieveAppInfo(it) - } - } - LaunchedEffect(Unit) { refresh() } - MyLazyScaffold(R.string.disable_user_control, onNavigateUp) { - items(packages, { it.name }) { info -> - ApplicationItem(info) { - Privilege.DPM.setUserControlDisabledPackages(Privilege.DAR, packages.minus(info).map { it.name }) - refresh() - } - } - item { - var packageName by remember { mutableStateOf("") } - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it } - Button( - { - Privilege.DPM.setUserControlDisabledPackages(Privilege.DAR, 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) { +fun PermissionsManagerScreen( + packagePermissions: MutableStateFlow>, getPackagePermissions: (String) -> Unit, + setPackagePermission: (String, String, Int) -> Boolean, onNavigateUp: () -> Unit, + param: PermissionsManager, chosenPackage: Channel, onChoosePackage: () -> Unit +) { val packageNameParam = param.packageName - val context = LocalContext.current val privilege by Privilege.status.collectAsStateWithLifecycle() var packageName by remember { mutableStateOf(packageNameParam ?: "") } var selectedPermission by remember { mutableStateOf(null) } - val statusMap = remember { mutableStateMapOf() } + val permissions by packagePermissions.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + packageName = chosenPackage.receive() + } LaunchedEffect(packageName) { - if(packageName.isValidPackageName) { - permissionList().forEach { statusMap[it.permission] = Privilege.DPM.getPermissionGrantState(Privilege.DAR, packageName, it.permission) } - } else { - statusMap.clear() - } + getPackagePermissions(packageName) } MyLazyScaffold(R.string.permissions, onNavigateUp) { item { if(packageNameParam == null) { - PackageNameTextField(packageName, Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } Spacer(Modifier.padding(vertical = 4.dp)) } } @@ -510,7 +326,7 @@ fun PermissionsManagerScreen(onNavigateUp: () -> Unit, param: PermissionsManager ) { Icon(painterResource(it.icon), null, Modifier.padding(horizontal = 12.dp)) Column { - val state = when(statusMap[it.permission]) { + val state = when(permissions[it.permission]) { PERMISSION_GRANT_STATE_DEFAULT -> R.string.default_stringres PERMISSION_GRANT_STATE_GRANTED -> R.string.granted PERMISSION_GRANT_STATE_DENIED -> R.string.denied @@ -527,14 +343,12 @@ fun PermissionsManagerScreen(onNavigateUp: () -> Unit, param: PermissionsManager } if(selectedPermission != null) { fun changeState(state: Int) { - val result = Privilege.DPM.setPermissionGrantState(Privilege.DAR, packageName, selectedPermission!!.permission, state) - if (!result) context.showOperationResultToast(false) - statusMap[selectedPermission!!.permission] = Privilege.DPM.getPermissionGrantState(Privilege.DAR, packageName, selectedPermission!!.permission) - selectedPermission = null + val result = setPackagePermission(packageName, selectedPermission!!.permission, state) + if (result) selectedPermission = null } @Composable fun GrantPermissionItem(label: Int, status: Int) { - val selected = statusMap[selectedPermission!!.permission] == status + val selected = permissions[selectedPermission!!.permission] == status Row( Modifier .fillMaxWidth() @@ -569,56 +383,22 @@ fun PermissionsManagerScreen(onNavigateUp: () -> Unit, param: PermissionsManager @Serializable object DisableMeteredData -@RequiresApi(28) -@Composable -fun DisableMeteredDataScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - var packageName by remember { mutableStateOf("") } - val packages = remember { mutableStateListOf() } - fun refresh() { - val pm = context.packageManager - packages.clear() - Privilege.DPM.getMeteredDataDisabledPackages(Privilege.DAR).forEach { - packages += pm.retrieveAppInfo(it) - } - } - LaunchedEffect(Unit) { refresh() } - MyLazyScaffold(R.string.disable_metered_data, onNavigateUp) { - items(packages, { it.name }) { info -> - ApplicationItem(info) { - Privilege.DPM.setMeteredDataDisabledPackages(Privilege.DAR, packages.minus(info).map { it.name }) - refresh() - } - } - item { - PackageNameTextField(packageName, Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } - Button( - { - if(Privilege.DPM.setMeteredDataDisabledPackages(Privilege.DAR, 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) { +fun ClearAppStorageScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, + onClear: (String, (Boolean) -> Unit) -> Unit, onNavigateUp: () -> Unit +) { var dialog by remember { mutableStateOf(false) } var packageName by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + packageName = chosenPackage.receive() + } MyScaffold(R.string.clear_app_storage, onNavigateUp) { - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(vertical = 8.dp)) { packageName = it } Button( { dialog = true }, Modifier.fillMaxWidth(), @@ -627,12 +407,14 @@ fun ClearAppStorageScreen(onNavigateUp: () -> Unit) { Text(stringResource(R.string.clear)) } } - if(dialog) ClearAppStorageDialog(packageName) { dialog = false } + if(dialog) ClearAppStorageDialog(packageName, onClear) { dialog = false } } @RequiresApi(28) @Composable -private fun ClearAppStorageDialog(packageName: String, onClose: () -> Unit) { +private fun ClearAppStorageDialog( + packageName: String, onClear: (String, (Boolean) -> Unit) -> Unit, onClose: () -> Unit +) { val context = LocalContext.current var clearing by remember { mutableStateOf(false) } AlertDialog( @@ -644,9 +426,7 @@ private fun ClearAppStorageDialog(packageName: String, onClose: () -> Unit) { TextButton( { clearing = true - Privilege.DPM.clearApplicationUserData( - Privilege.DAR, packageName, Executors.newSingleThreadExecutor() - ) { _, it -> + onClear(packageName) { Looper.prepare() context.showOperationResultToast(it) onClose() @@ -661,18 +441,28 @@ private fun ClearAppStorageDialog(packageName: String, onClose: () -> Unit) { dismissButton = { TextButton(onClose, enabled = !clearing) { Text(stringResource(R.string.cancel)) } }, - onDismissRequest = onClose + onDismissRequest = { + if (!clearing) onClose() + }, + properties = DialogProperties(false, false) ) } @Serializable object UninstallApp @Composable -fun UninstallAppScreen(onNavigateUp: () -> Unit) { +fun UninstallAppScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, + onUninstall: (String, (String?) -> Unit) -> Unit, onNavigateUp: () -> Unit +) { var dialog by remember { mutableStateOf(false) } var packageName by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + packageName = chosenPackage.receive() + } MyScaffold(R.string.uninstall_app, onNavigateUp) { - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(vertical = 8.dp)) { packageName = it } Button( { dialog = true }, Modifier.fillMaxWidth(), @@ -681,12 +471,16 @@ fun UninstallAppScreen(onNavigateUp: () -> Unit) { Text(stringResource(R.string.uninstall)) } } - if(dialog) UninstallAppDialog(packageName) { dialog = false } + if(dialog) UninstallAppDialog(packageName, onUninstall) { + packageName = "" + dialog = false + } } @Composable -private fun UninstallAppDialog(packageName: String, onClose: () -> Unit) { - val context = LocalContext.current +private fun UninstallAppDialog( + packageName: String, onUninstall: (String, (String?) -> Unit) -> Unit, onClose: () -> Unit +) { var uninstalling by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf(null) } AlertDialog( @@ -699,7 +493,7 @@ private fun UninstallAppDialog(packageName: String, onClose: () -> Unit) { TextButton( { uninstalling = true - uninstallPackage(context, packageName) { + onUninstall(packageName) { uninstalling = false if(it == null) onClose() else errorMessage = it } @@ -713,64 +507,32 @@ private fun UninstallAppDialog(packageName: String, onClose: () -> Unit) { dismissButton = { TextButton(onClose, enabled = !uninstalling) { Text(stringResource(R.string.cancel)) } }, - onDismissRequest = onClose + onDismissRequest = onClose, + properties = DialogProperties(false, false) ) } @Serializable object KeepUninstalledPackages -@RequiresApi(28) -@Composable -fun KeepUninstalledPackagesScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - val packages = remember { mutableStateListOf() } - fun refresh() { - val pm = context.packageManager - packages.clear() - Privilege.DPM.getKeepUninstalledPackages(Privilege.DAR)?.forEach { - packages += pm.retrieveAppInfo(it) - } - } - LaunchedEffect(Unit) { refresh() } - MyLazyScaffold(R.string.keep_uninstalled_packages, onNavigateUp) { - items(packages, { it.name }) { info -> - ApplicationItem(info) { - Privilege.DPM.setKeepUninstalledPackages(Privilege.DAR, packages.minus(info).map { it.name }) - refresh() - } - } - item { - var packageName by remember { mutableStateOf("") } - PackageNameTextField(packageName, Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } - Button( - { - Privilege.DPM.setKeepUninstalledPackages(Privilege.DAR, 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) { +fun InstallExistingAppScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, + onInstall: (String) -> Boolean, onNavigateUp: () -> Unit +) { val context = LocalContext.current + var packageName by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + packageName = chosenPackage.receive() + } MyScaffold(R.string.install_existing_app, onNavigateUp) { - var packageName by remember { mutableStateOf("") } - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(vertical = 8.dp)) { packageName = it } Button( { - context.showOperationResultToast( - Privilege.DPM.installExistingPackage(Privilege.DAR, packageName) - ) + context.showOperationResultToast(onInstall(packageName)) }, Modifier.fillMaxWidth(), packageName.isValidPackageName @@ -783,101 +545,23 @@ fun InstallExistingAppScreen(onNavigateUp: () -> Unit) { @Serializable object CrossProfilePackages -@RequiresApi(30) -@Composable -fun CrossProfilePackagesScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - val packages = remember { mutableStateListOf() } - fun refresh() { - val pm = context.packageManager - packages.clear() - Privilege.DPM.getCrossProfilePackages(Privilege.DAR).forEach { - packages += pm.retrieveAppInfo(it) - } - } - LaunchedEffect(Unit) { refresh() } - MyLazyScaffold(R.string.cross_profile_apps, onNavigateUp) { - items(packages, { it.name }) { info -> - ApplicationItem(info) { - Privilege.DPM.setCrossProfilePackages(Privilege.DAR, packages.minus(info).map { it.name }.toSet()) - refresh() - } - } - item { - var packageName by remember { mutableStateOf("") } - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } - Button( - { - Privilege.DPM.setCrossProfilePackages(Privilege.DAR, 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 packages = remember { mutableStateListOf() } - fun refresh() { - val pm = context.packageManager - packages.clear() - Privilege.DPM.getCrossProfileWidgetProviders(Privilege.DAR).forEach { - packages += pm.retrieveAppInfo(it) - } - } - LaunchedEffect(Unit) { refresh() } - MyLazyScaffold(R.string.cross_profile_widget, onNavigateUp) { - items(packages, { it.name }) { - ApplicationItem(it) { - Privilege.DPM.removeCrossProfileWidgetProvider(Privilege.DAR, it.name) - refresh() - } - } - item { - var packageName by remember { mutableStateOf("") } - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it } - Button( - { - Privilege.DPM.addCrossProfileWidgetProvider(Privilege.DAR, 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 - var policyType by remember{ mutableIntStateOf(-1) } - val packages = remember { mutableStateListOf() } - fun refresh() { - val policy = Privilege.DPM.credentialManagerPolicy - policyType = policy?.policyType ?: -1 - packages.clear() - policy?.packageNames?.forEach { - packages += pm.retrieveAppInfo(it) - } +fun CredentialManagerPolicyScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, + cmPackages: MutableStateFlow>, getCmPolicy: () -> Int, + setCmPackage: (String, Boolean) -> Unit, setCmPolicy: (Int) -> Unit, onNavigateUp: () -> Unit +) { + var policy by remember { mutableIntStateOf(getCmPolicy()) } + val packages by cmPackages.collectAsStateWithLifecycle() + var packageName by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + packageName = chosenPackage.receive() } - LaunchedEffect(Unit) { refresh() } MyLazyScaffold(R.string.credential_manager_policy, onNavigateUp) { item { mapOf( @@ -886,20 +570,21 @@ fun CredentialManagerPolicyScreen(onNavigateUp: () -> Unit) { 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 } + FullWidthRadioButtonItem(value, policy == key) { policy = key } } Spacer(Modifier.padding(vertical = 4.dp)) } items(packages, { it.name }) { - ApplicationItem(it) { packages -= it } + ApplicationItem(it) { setCmPackage(it.name, false) } } item { Column(Modifier.padding(horizontal = HorizontalPadding)) { - var packageName by remember { mutableStateOf("") } - PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(vertical = 8.dp)) { packageName = it } Button( { - packages += pm.retrieveAppInfo(packageName) + setCmPackage(packageName, true) + packageName = "" }, Modifier.fillMaxWidth(), enabled = packageName.isValidPackageName @@ -908,18 +593,7 @@ fun CredentialManagerPolicyScreen(onNavigateUp: () -> Unit) { } Button( { - try { - if(policyType != -1 && packages.isNotEmpty()) { - Privilege.DPM.credentialManagerPolicy = PackagePolicy(policyType, packages.map { it.name }.toSet()) - } else { - Privilege.DPM.credentialManagerPolicy = null - } - context.showOperationResultToast(true) - } catch(_: IllegalArgumentException) { - context.showOperationResultToast(false) - } finally { - refresh() - } + setCmPolicy(policy) }, Modifier.fillMaxWidth() ) { @@ -932,104 +606,54 @@ fun CredentialManagerPolicyScreen(onNavigateUp: () -> Unit) { @Serializable object PermittedAccessibilityServices -@Composable -fun PermittedAccessibilityServicesScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - val pm = context.packageManager - val packages = remember { mutableStateListOf() } - var allowAll by remember { mutableStateOf(true) } - fun refresh() { - packages.clear() - val list = Privilege.DPM.getPermittedAccessibilityServices(Privilege.DAR) - 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 = Privilege.DPM.setPermittedAccessibilityServices(Privilege.DAR, 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) { +fun PermittedAsAndImPackages( + title: Int, note: Int, chosenPackage: Channel, onChoosePackage: () -> Unit, + packagesState: MutableStateFlow>, getPackages: () -> Boolean, + setPackage: (String, Boolean) -> Unit, setPolicy: (Boolean) -> Boolean, onNavigateUp: () -> Unit +) { val context = LocalContext.current - val pm = context.packageManager - val packages = remember { mutableStateListOf() } - var allowAll by remember { mutableStateOf(true) } - fun refresh() { - packages.clear() - val list = Privilege.DPM.getPermittedInputMethods(Privilege.DAR) - allowAll = list == null - list?.forEach { - packages += pm.retrieveAppInfo(it) - } + val packages by packagesState.collectAsStateWithLifecycle() + var packageName by remember { mutableStateOf("") } + var allowAll by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + allowAll = getPackages() + packageName = chosenPackage.receive() } - LaunchedEffect(Unit) { refresh() } - MyLazyScaffold(R.string.permitted_ime, onNavigateUp) { + MyLazyScaffold(title, onNavigateUp) { item { SwitchItem(R.string.allow_all, state = allowAll, onCheckedChange = { allowAll = it }) } - items(packages, { it.name }) { - ApplicationItem(it) { packages -= it } + if (allowAll) items(packages, { it.name }) { + ApplicationItem(it) { setPackage(it.name, false) } } 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)) + if (allowAll) { + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } + Button( + { + setPackage(packageName, true) + packageName = "" + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } } Button( { - val result = Privilege.DPM.setPermittedInputMethods(Privilege.DAR, if(allowAll) null else packages.map { it.name }) - context.showOperationResultToast(result) - refresh() + context.showOperationResultToast(setPolicy(allowAll)) }, Modifier.fillMaxWidth().padding(top = 8.dp).padding(horizontal = HorizontalPadding) ) { Text(stringResource(R.string.apply)) } - Notes(R.string.system_ime_always_allowed, HorizontalPadding) + Spacer(Modifier.height(10.dp)) + Notes(note, HorizontalPadding) } } } @@ -1037,15 +661,22 @@ fun PermittedInputMethodsScreen(onNavigateUp: () -> Unit) { @Serializable object EnableSystemApp @Composable -fun EnableSystemAppScreen(onNavigateUp: () -> Unit) { +fun EnableSystemAppScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, + onEnable: (String) -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current + var packageName by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + packageName = chosenPackage.receive() + } 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 } + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(bottom = 8.dp)) { packageName = it } Button( { - Privilege.DPM.enableSystemApp(Privilege.DAR, packageName) + onEnable(packageName) packageName = "" context.showOperationResultToast(true) }, @@ -1062,21 +693,21 @@ fun EnableSystemAppScreen(onNavigateUp: () -> Unit) { @RequiresApi(34) @Composable -fun SetDefaultDialerScreen(onNavigateUp: () -> Unit) { - val context = LocalContext.current - var errorMessage by remember { mutableStateOf(null) } +fun SetDefaultDialerScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, + onSet: (String) -> Unit, onNavigateUp: () -> Unit +) { + var packageName by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + packageName = chosenPackage.receive() + } 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 } + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(bottom = 8.dp)) { packageName = it } Button( { - try { - Privilege.DPM.setDefaultDialerApplication(packageName) - context.showOperationResultToast(true) - } catch(e: Exception) { - errorMessage = e.message - } + onSet(packageName) }, Modifier.fillMaxWidth(), packageName.isValidPackageName @@ -1084,37 +715,54 @@ fun SetDefaultDialerScreen(onNavigateUp: () -> Unit) { 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)) - } +@Composable +fun PackageFunctionScreenWithoutResult( + title: Int, packagesState: MutableStateFlow>, onGet: () -> Unit, + onSet: (String, Boolean) -> Unit, onNavigateUp: () -> Unit, + chosenPackage: Channel, onChoosePackage: () -> Unit, notes: Int? = null +) { + PackageFunctionScreen( + title, packagesState, onGet, { name, status -> onSet(name, status); null }, + onNavigateUp, chosenPackage, onChoosePackage, notes + ) +} + +@Composable +fun PackageFunctionScreen( + title: Int, packagesState: MutableStateFlow>, onGet: () -> Unit, + onSet: (String, Boolean) -> Boolean?, onNavigateUp: () -> Unit, + chosenPackage: Channel, onChoosePackage: () -> Unit, notes: Int? = null +) { + val packages by packagesState.collectAsStateWithLifecycle() + var packageName by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + onGet() + packageName = chosenPackage.receive() + } + MyLazyScaffold(title, onNavigateUp) { + items(packages, { it.name }) { + ApplicationItem(it) { + onSet(it.name, false) } } + item { + PackageNameTextField(packageName, onChoosePackage, + Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } + Button( + { + if (onSet(packageName, true) != false) { + println("reset") + packageName = "" + } + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 10.dp), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } + if (notes != null) Notes(notes, HorizontalPadding) + } } - 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) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index a934e9f..8d01fb9 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -24,7 +24,6 @@ import com.bintianqi.owndroid.createShortcuts import com.rosan.dhizuku.api.Dhizuku import com.rosan.dhizuku.api.DhizukuBinderWrapper import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index 965ffa1..65da686 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -127,7 +127,6 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.core.os.bundleOf import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.Privilege import com.bintianqi.owndroid.R @@ -153,6 +152,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable @@ -886,7 +886,10 @@ fun NetworkStats.toBucketList(): List { @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(23) @Composable -fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkStatsViewer) -> Unit) { +fun NetworkStatsScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, + onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkStatsViewer) -> Unit +) { val context = LocalContext.current val privilege by Privilege.status.collectAsStateWithLifecycle() val fm = LocalFocusManager.current @@ -1053,16 +1056,14 @@ fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkSta ) { var uidText by rememberSaveable { mutableStateOf(context.getString(NetworkStatsUID.All.strRes)) } var readOnly by rememberSaveable { mutableStateOf(true) } - if(!readOnly && uidText.toIntOrNull() != null) uid = uidText.toInt() - val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { - it ?: return@rememberLauncherForActivityResult - if(VERSION.SDK_INT >= 24 && readOnly) { - try { - uid = context.packageManager.getPackageUid(it, 0) - uidText = "$it ($uid)" - } catch(_: NameNotFoundException) { - context.showOperationResultToast(false) - } + if (!readOnly && uidText.toIntOrNull() != null) uid = uidText.toInt() + if (VERSION.SDK_INT >= 24) LaunchedEffect(Unit) { + val pkg = chosenPackage.receive() + try { + uid = context.packageManager.getPackageUid(pkg, 0) + uidText = "$uid ($pkg)" + } catch(_: NameNotFoundException) { + context.showOperationResultToast(false) } } OutlinedTextField( @@ -1093,7 +1094,7 @@ fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkSta onClick = { readOnly = true activeTextField = NetworkStatsActiveTextField.None - choosePackage.launch(null) + onChoosePackage() } ) DropdownMenuItem( @@ -1457,15 +1458,18 @@ fun PrivateDnsScreen(onNavigateUp: () -> Unit) { @RequiresApi(24) @Composable -fun AlwaysOnVpnPackageScreen(onNavigateUp: () -> Unit) { +fun AlwaysOnVpnPackageScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, onNavigateUp: () -> Unit +) { val context = LocalContext.current var lockdown by rememberSaveable { mutableStateOf(false) } var pkgName by rememberSaveable { mutableStateOf("") } - val focusMgr = LocalFocusManager.current - val refresh = { pkgName = Privilege.DPM.getAlwaysOnVpnPackage(Privilege.DAR) ?: "" } - LaunchedEffect(Unit) { refresh() } - val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result -> - result?.let { pkgName = it } + fun refresh() { + pkgName = Privilege.DPM.getAlwaysOnVpnPackage(Privilege.DAR) ?: "" + } + LaunchedEffect(Unit) { + refresh() + pkgName = chosenPackage.receive() } val setAlwaysOnVpn: (String?, Boolean)->Boolean = { vpnPkg: String?, lockdownEnabled: Boolean -> try { @@ -1483,21 +1487,8 @@ fun AlwaysOnVpnPackageScreen(onNavigateUp: () -> Unit) { } } MyScaffold(R.string.always_on_vpn, onNavigateUp) { - OutlinedTextField( - value = pkgName, - onValueChange = { pkgName = it }, - label = { Text(stringResource(R.string.package_name)) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), - trailingIcon = { - Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(50)) - .clickable { choosePackage.launch(null) } - .padding(3.dp)) - }, - modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) - ) + PackageNameTextField(pkgName, onChoosePackage, + Modifier.padding(vertical = 4.dp)) { pkgName = it } SwitchItem(R.string.enable_lockdown, state = lockdown, onCheckedChange = { lockdown = it }, padding = false) Spacer(Modifier.padding(vertical = 5.dp)) Button( @@ -2067,7 +2058,7 @@ fun AddApnSettingScreen(origin: ApnSetting?, onNavigateUp: () -> Unit) { keyboardActions = KeyboardActions { fm.clearFocus() } ) if(VERSION.SDK_INT >= 33) Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), Arrangement.SpaceBetween) { - val fr = FocusRequester() + val fr = remember { FocusRequester() } OutlinedTextField( mtuV4, { mtuV4 = it }, Modifier.fillMaxWidth(0.49F), label = { Text("MTU (IPv4)") }, @@ -2206,7 +2197,7 @@ fun AddApnSettingScreen(origin: ApnSetting?, onNavigateUp: () -> Unit) { if(dialog != 0) { var address by remember { mutableStateOf((if(dialog == 1) proxyAddress else mmsProxyAddress)) } var port by remember { mutableStateOf((if(dialog == 1) proxyPort else mmsProxyPort)) } - val fr = FocusRequester() + val fr = remember { FocusRequester() } AlertDialog( title = { Text(if(dialog == 1) "Proxy" else "MMS proxy") }, text = { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index 30c0f5f..eb53845 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -6,7 +6,6 @@ import android.content.Context import android.content.pm.PackageManager import android.os.Build.VERSION import android.os.PersistableBundle -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.annotation.Keep import androidx.annotation.RequiresApi import androidx.annotation.StringRes @@ -84,10 +83,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.DHIZUKU_CLIENTS_FILE import com.bintianqi.owndroid.DhizukuClientInfo import com.bintianqi.owndroid.DhizukuPermissions @@ -113,6 +110,7 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.rosan.dhizuku.api.Dhizuku import com.rosan.dhizuku.api.DhizukuRequestPermissionListener import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -731,30 +729,19 @@ fun DelegatedAdminsScreen(onNavigateUp: () -> Unit, onNavigate: (AddDelegatedAdm @RequiresApi(26) @Composable -fun AddDelegatedAdminScreen(data: AddDelegatedAdmin, onNavigateUp: () -> Unit) { +fun AddDelegatedAdminScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, + data: AddDelegatedAdmin, onNavigateUp: () -> Unit +) { val updateMode = data.pkg.isNotEmpty() - val fm = LocalFocusManager.current var input by remember { mutableStateOf(data.pkg) } val scopes = remember { mutableStateListOf(*data.scopes.toTypedArray()) } - val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result -> - result?.let { input = it } + LaunchedEffect(Unit) { + input = chosenPackage.receive() } MySmallTitleScaffold(if(updateMode) R.string.place_holder else R.string.add_delegated_admin, onNavigateUp, 0.dp) { - OutlinedTextField( - value = input, onValueChange = { input = it }, - label = { Text(stringResource(R.string.package_name)) }, - trailingIcon = { - if(!updateMode) IconButton({ choosePackage.launch(null) }) { - Icon(painterResource(R.drawable.list_fill0), null) - } - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions { fm.clearFocus() }, - readOnly = updateMode, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = HorizontalPadding) - ) + PackageNameTextField(input, onChoosePackage, + Modifier.padding(HorizontalPadding, 8.dp)) { input = it } DelegatedScope.entries.filter { VERSION.SDK_INT >= it.requiresApi }.forEach { scope -> val checked = scope in scopes Row( diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 635957b..86ad241 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -112,13 +112,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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.lifecycle.compose.collectAsStateWithLifecycle -import com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.NotificationUtils import com.bintianqi.owndroid.Privilege @@ -143,6 +141,7 @@ import com.bintianqi.owndroid.ui.Notes import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.uriToStream import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -1134,7 +1133,9 @@ fun NearbyStreamingPolicyScreen(onNavigateUp: () -> Unit) { @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(28) @Composable -fun LockTaskModeScreen(onNavigateUp: () -> Unit) { +fun LockTaskModeScreen( + chosenPackage: Channel, onChoosePackage: () -> Unit, onNavigateUp: () -> Unit +) { val coroutine = rememberCoroutineScope() val pagerState = rememberPagerState { 3 } var tabIndex by remember { mutableIntStateOf(0) } @@ -1177,8 +1178,8 @@ fun LockTaskModeScreen(onNavigateUp: () -> Unit) { .padding(horizontal = HorizontalPadding) .padding(bottom = 80.dp) ) { - if(page == 0) StartLockTaskMode() - else LockTaskPackages() + if(page == 0) StartLockTaskMode(chosenPackage, onChoosePackage) + else LockTaskPackages(chosenPackage, onChoosePackage) } } else { Column( @@ -1197,33 +1198,20 @@ fun LockTaskModeScreen(onNavigateUp: () -> Unit) { @RequiresApi(28) @Composable -private fun ColumnScope.StartLockTaskMode() { +private fun ColumnScope.StartLockTaskMode( + chosenPackage: Channel, onChoosePackage: () -> Unit +) { val context = LocalContext.current val focusMgr = LocalFocusManager.current var startLockTaskApp by rememberSaveable { mutableStateOf("") } var startLockTaskActivity by rememberSaveable { mutableStateOf("") } var specifyActivity by rememberSaveable { mutableStateOf(false) } - val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result -> - result?.let { startLockTaskApp = it } + LaunchedEffect(Unit) { + startLockTaskApp = chosenPackage.receive() } Spacer(Modifier.padding(vertical = 5.dp)) - OutlinedTextField( - value = startLockTaskApp, - onValueChange = { startLockTaskApp = it }, - label = { Text(stringResource(R.string.package_name)) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), - trailingIcon = { - Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(50)) - .clickable { choosePackage.launch(null) } - .padding(3.dp)) - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 3.dp) - ) + PackageNameTextField(startLockTaskApp, onChoosePackage, + Modifier.padding(vertical = 3.dp), { startLockTaskApp = it }) CheckBoxItem(R.string.specify_activity, specifyActivity) { specifyActivity = it } AnimatedVisibility(specifyActivity) { OutlinedTextField( @@ -1264,37 +1252,23 @@ private fun ColumnScope.StartLockTaskMode() { @RequiresApi(26) @Composable -private fun ColumnScope.LockTaskPackages() { +private fun ColumnScope.LockTaskPackages( + chosenPackage: Channel, onChoosePackage: () -> Unit +) { val context = LocalContext.current - val focusMgr = LocalFocusManager.current val lockTaskPackages = remember { mutableStateListOf() } var input by rememberSaveable { mutableStateOf("") } - val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result -> - result?.let { input = it } + LaunchedEffect(Unit) { + lockTaskPackages.addAll(Privilege.DPM.getLockTaskPackages(Privilege.DAR)) + input = chosenPackage.receive() } - LaunchedEffect(Unit) { lockTaskPackages.addAll(Privilege.DPM.getLockTaskPackages(Privilege.DAR)) } Spacer(Modifier.padding(vertical = 5.dp)) if(lockTaskPackages.isEmpty()) Text(text = stringResource(R.string.none)) for(i in lockTaskPackages) { ListItem(i) { lockTaskPackages -= i } } - OutlinedTextField( - value = input, - onValueChange = { input = it }, - label = { Text(stringResource(R.string.package_name)) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), - trailingIcon = { - Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(50)) - .clickable { choosePackage.launch(null) } - .padding(3.dp)) - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 3.dp) - ) + PackageNameTextField(input, onChoosePackage, + Modifier.padding(vertical = 3.dp), { input = it }) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Button( onClick = {