Optimize Applications and PackageChooser

This commit is contained in:
BinTianqi
2025-09-17 18:29:52 +08:00
parent e7c7a3b3c6
commit 80c1ddb36c
13 changed files with 787 additions and 802 deletions

View File

@@ -34,7 +34,6 @@ import com.rosan.dhizuku.server_api.DhizukuService
import com.rosan.dhizuku.shared.DhizukuVariables
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val TAG = "DhizukuServer"

View File

@@ -79,7 +79,6 @@ import com.bintianqi.owndroid.dpm.AutoTimePolicyScreen
import com.bintianqi.owndroid.dpm.AutoTimeZonePolicy
import com.bintianqi.owndroid.dpm.AutoTimeZonePolicyScreen
import com.bintianqi.owndroid.dpm.BlockUninstall
import com.bintianqi.owndroid.dpm.BlockUninstallScreen
import com.bintianqi.owndroid.dpm.CaCert
import com.bintianqi.owndroid.dpm.CaCertScreen
import com.bintianqi.owndroid.dpm.ChangeTime
@@ -101,9 +100,7 @@ import com.bintianqi.owndroid.dpm.CredentialManagerPolicyScreen
import com.bintianqi.owndroid.dpm.CrossProfileIntentFilter
import com.bintianqi.owndroid.dpm.CrossProfileIntentFilterScreen
import com.bintianqi.owndroid.dpm.CrossProfilePackages
import com.bintianqi.owndroid.dpm.CrossProfilePackagesScreen
import com.bintianqi.owndroid.dpm.CrossProfileWidgetProviders
import com.bintianqi.owndroid.dpm.CrossProfileWidgetProvidersScreen
import com.bintianqi.owndroid.dpm.DelegatedAdmins
import com.bintianqi.owndroid.dpm.DelegatedAdminsScreen
import com.bintianqi.owndroid.dpm.DeleteWorkProfile
@@ -115,9 +112,7 @@ import com.bintianqi.owndroid.dpm.DhizukuServerSettingsScreen
import com.bintianqi.owndroid.dpm.DisableAccountManagement
import com.bintianqi.owndroid.dpm.DisableAccountManagementScreen
import com.bintianqi.owndroid.dpm.DisableMeteredData
import com.bintianqi.owndroid.dpm.DisableMeteredDataScreen
import com.bintianqi.owndroid.dpm.DisableUserControl
import com.bintianqi.owndroid.dpm.DisableUserControlScreen
import com.bintianqi.owndroid.dpm.EnableSystemApp
import com.bintianqi.owndroid.dpm.EnableSystemAppScreen
import com.bintianqi.owndroid.dpm.FrpPolicy
@@ -125,13 +120,11 @@ import com.bintianqi.owndroid.dpm.FrpPolicyScreen
import com.bintianqi.owndroid.dpm.HardwareMonitor
import com.bintianqi.owndroid.dpm.HardwareMonitorScreen
import com.bintianqi.owndroid.dpm.Hide
import com.bintianqi.owndroid.dpm.HideScreen
import com.bintianqi.owndroid.dpm.InstallExistingApp
import com.bintianqi.owndroid.dpm.InstallExistingAppScreen
import com.bintianqi.owndroid.dpm.InstallSystemUpdate
import com.bintianqi.owndroid.dpm.InstallSystemUpdateScreen
import com.bintianqi.owndroid.dpm.KeepUninstalledPackages
import com.bintianqi.owndroid.dpm.KeepUninstalledPackagesScreen
import com.bintianqi.owndroid.dpm.Keyguard
import com.bintianqi.owndroid.dpm.KeyguardDisabledFeatures
import com.bintianqi.owndroid.dpm.KeyguardDisabledFeaturesScreen
@@ -157,6 +150,8 @@ import com.bintianqi.owndroid.dpm.OrganizationOwnedProfile
import com.bintianqi.owndroid.dpm.OrganizationOwnedProfileScreen
import com.bintianqi.owndroid.dpm.OverrideApn
import com.bintianqi.owndroid.dpm.OverrideApnScreen
import com.bintianqi.owndroid.dpm.PackageFunctionScreen
import com.bintianqi.owndroid.dpm.PackageFunctionScreenWithoutResult
import com.bintianqi.owndroid.dpm.Password
import com.bintianqi.owndroid.dpm.PasswordInfo
import com.bintianqi.owndroid.dpm.PasswordInfoScreen
@@ -166,9 +161,8 @@ import com.bintianqi.owndroid.dpm.PermissionPolicyScreen
import com.bintianqi.owndroid.dpm.PermissionsManager
import com.bintianqi.owndroid.dpm.PermissionsManagerScreen
import com.bintianqi.owndroid.dpm.PermittedAccessibilityServices
import com.bintianqi.owndroid.dpm.PermittedAccessibilityServicesScreen
import com.bintianqi.owndroid.dpm.PermittedAsAndImPackages
import com.bintianqi.owndroid.dpm.PermittedInputMethods
import com.bintianqi.owndroid.dpm.PermittedInputMethodsScreen
import com.bintianqi.owndroid.dpm.PreferentialNetworkService
import com.bintianqi.owndroid.dpm.PreferentialNetworkServiceScreen
import com.bintianqi.owndroid.dpm.PrivateDns
@@ -195,7 +189,6 @@ import com.bintianqi.owndroid.dpm.SupportMessageScreen
import com.bintianqi.owndroid.dpm.Suspend
import com.bintianqi.owndroid.dpm.SuspendPersonalApp
import com.bintianqi.owndroid.dpm.SuspendPersonalAppScreen
import com.bintianqi.owndroid.dpm.SuspendScreen
import com.bintianqi.owndroid.dpm.SystemManager
import com.bintianqi.owndroid.dpm.SystemManagerScreen
import com.bintianqi.owndroid.dpm.SystemOptions
@@ -276,6 +269,9 @@ fun Home(vm: MyViewModel, onLock: () -> 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<DhizukuServerSettings> { DhizukuServerSettingsScreen(::navigateUp) }
composable<DelegatedAdmins> { DelegatedAdminsScreen(::navigateUp, ::navigate) }
composable<AddDelegatedAdmin>{ AddDelegatedAdminScreen(it.toRoute(), ::navigateUp) }
composable<AddDelegatedAdmin>{
AddDelegatedAdminScreen(vm.chosenPackage, ::choosePackage, it.toRoute(), ::navigateUp)
}
composable<DeviceInfo> { DeviceInfoScreen(::navigateUp) }
composable<LockScreenInfo> { LockScreenInfoScreen(::navigateUp) }
composable<SupportMessage> { SupportMessageScreen(::navigateUp) }
@@ -331,7 +329,9 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<PermissionPolicy> { PermissionPolicyScreen(::navigateUp) }
composable<MtePolicy> { MtePolicyScreen(::navigateUp) }
composable<NearbyStreamingPolicy> { NearbyStreamingPolicyScreen(::navigateUp) }
composable<LockTaskMode> { LockTaskModeScreen(::navigateUp) }
composable<LockTaskMode> {
LockTaskModeScreen(vm.chosenPackage, ::choosePackage, ::navigateUp)
}
composable<CaCert> { CaCertScreen(::navigateUp) }
composable<SecurityLogging> { SecurityLoggingScreen(::navigateUp) }
composable<DisableAccountManagement> { DisableAccountManagementScreen(::navigateUp) }
@@ -346,12 +346,16 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<AddNetwork> { AddNetworkScreen(it.arguments!!, ::navigateUp) }
composable<WifiSecurityLevel> { WifiSecurityLevelScreen(::navigateUp) }
composable<WifiSsidPolicyScreen> { WifiSsidPolicyScreen(::navigateUp) }
composable<QueryNetworkStats> { NetworkStatsScreen(::navigateUp, ::navigate) }
composable<QueryNetworkStats> {
NetworkStatsScreen(vm.chosenPackage, ::choosePackage, ::navigateUp, ::navigate)
}
composable<NetworkStatsViewer>(mapOf(serializableNavTypePair<List<NetworkStatsViewer.Data>>())) {
NetworkStatsViewerScreen(it.toRoute(), ::navigateUp)
}
composable<PrivateDns> { PrivateDnsScreen(::navigateUp) }
composable<AlwaysOnVpnPackage> { AlwaysOnVpnPackageScreen(::navigateUp) }
composable<AlwaysOnVpnPackage> {
AlwaysOnVpnPackageScreen(vm.chosenPackage, ::choosePackage, ::navigateUp)
}
composable<RecommendedGlobalProxy> { RecommendedGlobalProxyScreen(::navigateUp) }
composable<NetworkLogging> { NetworkLoggingScreen(::navigateUp) }
composable<WifiAuthKeypair> { WifiAuthKeypairScreen(::navigateUp) }
@@ -368,14 +372,25 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<DeleteWorkProfile> { DeleteWorkProfileScreen(::navigateUp) }
composable<ApplicationsList> {
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<ApplicationsFeatures> {
ApplicationsFeaturesScreen(::navigateUp, ::navigate) {
@@ -385,24 +400,78 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
}
}
composable<ApplicationDetails> { ApplicationDetailsScreen(it.toRoute(), ::navigateUp, ::navigate) }
composable<Suspend> { SuspendScreen(::navigateUp) }
composable<Hide> { HideScreen(::navigateUp) }
composable<BlockUninstall> { BlockUninstallScreen(::navigateUp) }
composable<DisableUserControl> { DisableUserControlScreen(::navigateUp) }
composable<PermissionsManager> { PermissionsManagerScreen(::navigateUp, it.toRoute()) }
composable<DisableMeteredData> { DisableMeteredDataScreen(::navigateUp) }
composable<ClearAppStorage> { ClearAppStorageScreen(::navigateUp) }
composable<UninstallApp> { UninstallAppScreen(::navigateUp) }
composable<KeepUninstalledPackages> { KeepUninstalledPackagesScreen(::navigateUp) }
composable<InstallExistingApp> { InstallExistingAppScreen(::navigateUp) }
composable<CrossProfilePackages> { CrossProfilePackagesScreen(::navigateUp) }
composable<CrossProfileWidgetProviders> { CrossProfileWidgetProvidersScreen(::navigateUp) }
composable<CredentialManagerPolicy> { CredentialManagerPolicyScreen(::navigateUp) }
composable<PermittedAccessibilityServices> { PermittedAccessibilityServicesScreen(::navigateUp) }
composable<PermittedInputMethods> { PermittedInputMethodsScreen(::navigateUp) }
composable<EnableSystemApp> { EnableSystemAppScreen(::navigateUp) }
composable<SetDefaultDialer> { SetDefaultDialerScreen(::navigateUp) }
composable<ApplicationDetails> {
ApplicationDetailsScreen(it.toRoute(), vm, ::navigateUp, ::navigate)
}
composable<Suspend> {
PackageFunctionScreen(R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged,
vm::setPackageSuspended, ::navigateUp, vm.chosenPackage, ::choosePackage,
R.string.info_suspend_app)
}
composable<Hide> {
PackageFunctionScreen(R.string.hide, vm.hiddenPackages, vm::getHiddenPackages,
vm::setPackageHidden, ::navigateUp, vm.chosenPackage, ::choosePackage)
}
composable<BlockUninstall> {
PackageFunctionScreenWithoutResult(R.string.block_uninstall, vm.ubPackages,
vm::getUbPackages, vm::setPackageUb, ::navigateUp, vm.chosenPackage, ::choosePackage)
}
composable<DisableUserControl> {
PackageFunctionScreenWithoutResult(R.string.disable_user_control, vm.ucdPackages,
vm::getUcdPackages, vm::setPackageUcd, ::navigateUp, vm.chosenPackage,
::choosePackage, R.string.info_disable_user_control)
}
composable<PermissionsManager> {
PermissionsManagerScreen(vm.packagePermissions, vm::getPackagePermissions,
vm::setPackagePermission, ::navigateUp, it.toRoute(), vm.chosenPackage, ::choosePackage)
}
composable<DisableMeteredData> {
PackageFunctionScreen(R.string.disable_metered_data, vm.mddPackages,
vm::getMddPackages, vm::setPackageMdd, ::navigateUp, vm.chosenPackage, ::choosePackage)
}
composable<ClearAppStorage> {
ClearAppStorageScreen(vm.chosenPackage, ::choosePackage, vm::clearAppData, ::navigateUp)
}
composable<UninstallApp> {
UninstallAppScreen(vm.chosenPackage, ::choosePackage, vm::uninstallPackage, ::navigateUp)
}
composable<KeepUninstalledPackages> {
PackageFunctionScreenWithoutResult(R.string.keep_uninstalled_packages, vm.kuPackages,
vm::getKuPackages, vm::setPackageKu, ::navigateUp, vm.chosenPackage,
::choosePackage, R.string.info_keep_uninstalled_apps)
}
composable<InstallExistingApp> {
InstallExistingAppScreen(vm.chosenPackage, ::choosePackage,
vm::installExistingApp, ::navigateUp)
}
composable<CrossProfilePackages> {
PackageFunctionScreenWithoutResult(R.string.cross_profile_apps, vm.cpPackages,
vm::getCpPackages, vm::setPackageCp, ::navigateUp, vm.chosenPackage, ::choosePackage)
}
composable<CrossProfileWidgetProviders> {
PackageFunctionScreen(R.string.cross_profile_widget, vm.cpwProviders,
vm::getCpwProviders, vm::setCpwProvider, ::navigateUp, vm.chosenPackage, ::choosePackage)
}
composable<CredentialManagerPolicy> {
CredentialManagerPolicyScreen(vm.chosenPackage, ::choosePackage,
vm.cmPackages, vm::getCmPolicy, vm::setCmPackage, vm::setCmPolicy, ::navigateUp)
}
composable<PermittedAccessibilityServices> {
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<PermittedInputMethods> {
PermittedAsAndImPackages(R.string.permitted_ime, R.string.system_ime_always_allowed,
vm.chosenPackage, ::choosePackage, vm.pimPackages, vm::getPimPackages,
vm::setPimPackage, vm::setPimPolicy, ::navigateUp)
}
composable<EnableSystemApp> {
EnableSystemAppScreen(vm.chosenPackage, ::choosePackage, vm::enableSystemApp, ::navigateUp)
}
composable<SetDefaultDialer> {
SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp)
}
composable<UserRestriction> {
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) }

View File

@@ -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<String>(1, BufferOverflow.DROP_LATEST)
val installedPackages = MutableStateFlow(emptyList<AppInfo>())
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<AppInfo>())
@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<AppInfo>())
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<AppInfo>())
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<AppInfo>())
@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<String, Int>())
@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<AppInfo>())
@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<AppInfo>())
@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<AppInfo>())
@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<AppInfo>())
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<AppInfo>())
@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<AppInfo>())
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<AppInfo>())
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(

View File

@@ -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<MyViewModel>()
enableEdgeToEdge()
setContent {
val theme by vm.theme.collectAsStateWithLifecycle()
OwnDroidTheme(theme) {
AppChooserScreen(ApplicationsList(false), {
setResult(0, Intent().putExtra("package", it))
finish()
}, {})
}
}
}
}
val installedApps = MutableStateFlow(emptyList<AppInfo>())
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<List<AppInfo>>,
refreshProgress: MutableStateFlow<Float>, 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

View File

@@ -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)

View File

@@ -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 <reified T> serializableNavTypePair() =
Json.encodeToString(value)
}
class ChoosePackageContract: ActivityResultContract<Nothing?, String?>() {
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")

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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<NetworkStats.Bucket> {
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(23)
@Composable
fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkStatsViewer) -> Unit) {
fun NetworkStatsScreen(
chosenPackage: Channel<String>, 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<String>, 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 = {

View File

@@ -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<String>, 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(

View File

@@ -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<String>, 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<String>, 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<String>, onChoosePackage: () -> Unit
) {
val context = LocalContext.current
val focusMgr = LocalFocusManager.current
val lockTaskPackages = remember { mutableStateListOf<String>() }
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 = {