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