Add alternative support for QUERY_ALL_PACKAGES in play flavor

This commit is contained in:
世界
2025-12-25 01:49:27 +08:00
parent 104da5d312
commit 08f51d5469
22 changed files with 855 additions and 169 deletions

View File

@@ -42,8 +42,6 @@ class Application : Application() {
UpdateProfileWork.reconfigureUpdater()
}
// Only register AppChangeReceiver if Per-app Proxy is available
// This receiver needs QUERY_ALL_PACKAGES permission to function
if (Vendor.isPerAppProxyAvailable()) {
registerReceiver(
AppChangeReceiver(),

View File

@@ -3,9 +3,18 @@ package io.nekohasekai.sfa.bg
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.widget.Toast
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
import io.nekohasekai.sfa.vendor.PackageQueryManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AppChangeReceiver : BroadcastReceiver() {
companion object {
@@ -17,10 +26,6 @@ class AppChangeReceiver : BroadcastReceiver() {
intent: Intent,
) {
Log.d(TAG, "onReceive: ${intent.action}")
checkUpdate(intent)
}
private fun checkUpdate(intent: Intent) {
if (!Settings.perAppProxyEnabled) {
Log.d(TAG, "per app proxy disabled")
return
@@ -29,19 +34,41 @@ class AppChangeReceiver : BroadcastReceiver() {
Log.d(TAG, "managed mode disabled")
return
}
val packageName = intent.dataString?.substringAfter("package:")
if (packageName.isNullOrBlank()) {
Log.d(TAG, "missing package name in intent")
return
}
val isChinaApp = PerAppProxyActivity.scanChinaPackage(packageName)
Log.d(TAG, "scan china app result for $packageName: $isChinaApp")
if (isChinaApp) {
Settings.perAppProxyManagedList += packageName
Log.d(TAG, "added to managed list")
} else {
Settings.perAppProxyManagedList -= packageName
Log.d(TAG, "removed from managed list")
val pendingResult = goAsync()
CoroutineScope(Dispatchers.IO).launch {
try {
rescanAllApps()
} catch (e: Exception) {
Log.e(TAG, "Failed to rescan apps", e)
withContext(Dispatchers.Main) {
Toast.makeText(context, R.string.error_title, Toast.LENGTH_SHORT).show()
}
} finally {
pendingResult.finish()
}
}
}
private suspend fun rescanAllApps() {
Log.d(TAG, "rescanning all apps")
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.MATCH_UNINSTALLED_PACKAGES or
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
} else {
@Suppress("DEPRECATION")
PackageManager.GET_UNINSTALLED_PACKAGES or
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
val installedPackages = PackageQueryManager.getInstalledPackages(packageManagerFlags)
val chinaApps = mutableSetOf<String>()
for (packageInfo in installedPackages) {
if (PerAppProxyActivity.scanChinaPackage(packageInfo)) {
chinaApps.add(packageInfo.packageName)
}
}
Settings.perAppProxyManagedList = chinaApps
Log.d(TAG, "rescan complete, found ${chinaApps.size} china apps")
}
}

View File

@@ -2,15 +2,14 @@ package io.nekohasekai.sfa.compose.screen.settings
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
@@ -22,6 +21,7 @@ import androidx.compose.material.icons.outlined.AppShortcut
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.Route
import androidx.compose.material.icons.outlined.SmartToy
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -30,10 +30,14 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -46,11 +50,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity
import io.nekohasekai.sfa.vendor.Vendor
import io.nekohasekai.sfa.vendor.PackageQueryManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@@ -66,7 +69,58 @@ fun ProfileOverrideScreen(navController: NavController) {
var perAppProxyEnabled by remember { mutableStateOf(Settings.perAppProxyEnabled) }
var managedModeEnabled by remember { mutableStateOf(Settings.perAppProxyManagedMode) }
var isScanning by remember { mutableStateOf(false) }
var showPerAppProxyDialog by remember { mutableStateOf(false) }
var showShizukuDialog by remember { mutableStateOf(false) }
var showRootDialog by remember { mutableStateOf(false) }
var showModeDialog by remember { mutableStateOf(false) }
val needsPrivilegedQuery = PackageQueryManager.needsPrivilegedQuery
var packageQueryMode by remember { mutableStateOf(Settings.perAppProxyPackageQueryMode) }
val useRootMode = packageQueryMode == Settings.PACKAGE_QUERY_MODE_ROOT
val isShizukuInstalled by PackageQueryManager.shizukuInstalled.collectAsState()
val isShizukuBinderReady by PackageQueryManager.shizukuBinderReady.collectAsState()
val isShizukuPermissionGranted by PackageQueryManager.shizukuPermissionGranted.collectAsState()
val isShizukuAvailable = isShizukuBinderReady && isShizukuPermissionGranted
DisposableEffect(needsPrivilegedQuery) {
if (needsPrivilegedQuery) {
PackageQueryManager.registerListeners()
}
onDispose {
if (needsPrivilegedQuery) {
PackageQueryManager.unregisterListeners()
}
}
}
// Auto-disable per-app proxy if Shizuku authorization is revoked (only when using Shizuku mode)
LaunchedEffect(isShizukuAvailable, useRootMode) {
if (needsPrivilegedQuery && !useRootMode && !isShizukuAvailable && perAppProxyEnabled) {
perAppProxyEnabled = false
withContext(Dispatchers.IO) {
Settings.perAppProxyEnabled = false
}
}
}
// Auto-close dialog and enable feature when Shizuku becomes available
LaunchedEffect(isShizukuAvailable) {
if (needsPrivilegedQuery && isShizukuAvailable && showShizukuDialog) {
showShizukuDialog = false
perAppProxyEnabled = true
withContext(Dispatchers.IO) {
Settings.perAppProxyEnabled = true
}
if (managedModeEnabled) {
isScanning = true
val chinaApps = scanAllChinaApps()
withContext(Dispatchers.IO) {
Settings.perAppProxyManagedList = chinaApps
}
isScanning = false
}
}
}
Column(
modifier =
@@ -158,7 +212,11 @@ fun ProfileOverrideScreen(navController: NavController) {
}
// Section: Per-App Proxy
val isPerAppProxyAvailable = Vendor.isPerAppProxyAvailable()
val canUsePerAppProxy = if (needsPrivilegedQuery) {
if (useRootMode) true else isShizukuAvailable
} else {
true
}
Text(
text = stringResource(R.string.per_app_proxy),
@@ -178,6 +236,52 @@ fun ProfileOverrideScreen(navController: NavController) {
),
) {
Column {
// Mode selector (only when privileged query is needed)
if (needsPrivilegedQuery) {
val modeEnabled = !perAppProxyEnabled
val disabledAlpha = 0.38f
ListItem(
headlineContent = {
Text(
stringResource(R.string.per_app_proxy_package_query_mode),
style = MaterialTheme.typography.bodyLarge,
color = if (modeEnabled) Color.Unspecified
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
)
},
supportingContent = {
Text(
if (useRootMode) "ROOT" else "Shizuku",
style = MaterialTheme.typography.bodyMedium,
color = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = disabledAlpha),
)
},
leadingContent = {
Icon(
imageVector = Icons.Outlined.Tune,
contentDescription = null,
tint = if (modeEnabled) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
)
},
trailingContent = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight,
contentDescription = null,
tint = if (modeEnabled) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha),
)
},
modifier = Modifier
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
.clickable(enabled = modeEnabled) { showModeDialog = true },
colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
),
)
}
// Enabled toggle
ListItem(
headlineContent = {
@@ -186,19 +290,6 @@ fun ProfileOverrideScreen(navController: NavController) {
style = MaterialTheme.typography.bodyLarge,
)
},
supportingContent =
if (!isPerAppProxyAvailable) {
{
Text(
text = context.getString(R.string.per_app_proxy_disabled_play_store),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(top = 4.dp),
)
}
} else {
null
},
leadingContent = {
Icon(
imageVector = Icons.Outlined.FilterList,
@@ -207,38 +298,40 @@ fun ProfileOverrideScreen(navController: NavController) {
)
},
trailingContent = {
if (isPerAppProxyAvailable) {
if (isScanning) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp,
)
} else {
Switch(
checked = perAppProxyEnabled,
onCheckedChange = { checked ->
perAppProxyEnabled = checked
scope.launch(Dispatchers.IO) {
Settings.perAppProxyEnabled = checked
}
if (checked && managedModeEnabled) {
isScanning = true
scope.launch {
val chinaApps = scanAllChinaApps()
withContext(Dispatchers.IO) {
Settings.perAppProxyManagedList = chinaApps
}
isScanning = false
Switch(
checked = perAppProxyEnabled,
onCheckedChange = { checked ->
if (checked && needsPrivilegedQuery) {
if (useRootMode) {
showRootDialog = true
} else {
showShizukuDialog = true
}
} else {
perAppProxyEnabled = checked
scope.launch(Dispatchers.IO) {
Settings.perAppProxyEnabled = checked
}
if (checked && managedModeEnabled) {
isScanning = true
scope.launch {
val chinaApps = scanAllChinaApps()
withContext(Dispatchers.IO) {
Settings.perAppProxyManagedList = chinaApps
}
isScanning = false
}
},
)
}
}
}
}
},
enabled = !isScanning,
)
},
modifier =
Modifier.clip(
if (perAppProxyEnabled && isPerAppProxyAvailable) {
if (needsPrivilegedQuery) {
RoundedCornerShape(0.dp)
} else if (perAppProxyEnabled && canUsePerAppProxy) {
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
} else {
RoundedCornerShape(12.dp)
@@ -250,7 +343,7 @@ fun ProfileOverrideScreen(navController: NavController) {
),
)
if (perAppProxyEnabled && isPerAppProxyAvailable) {
if (perAppProxyEnabled && canUsePerAppProxy) {
// Manage entry
val manageEnabled = !managedModeEnabled
val disabledAlpha = 0.38f
@@ -366,23 +459,199 @@ fun ProfileOverrideScreen(navController: NavController) {
}
}
// Dialog for Per-app Proxy disabled message
if (showPerAppProxyDialog) {
// Shizuku dialog
if (showShizukuDialog) {
AlertDialog(
onDismissRequest = { showPerAppProxyDialog = false },
onDismissRequest = { showShizukuDialog = false },
title = {
Text(stringResource(R.string.unavailable))
Text(stringResource(R.string.per_app_proxy))
},
text = {
Text(context.getString(R.string.per_app_proxy_disabled_message))
Text(stringResource(R.string.per_app_proxy_shizuku_required))
},
confirmButton = {
when {
isShizukuAvailable -> {
TextButton(
onClick = {
showShizukuDialog = false
perAppProxyEnabled = true
scope.launch(Dispatchers.IO) {
Settings.perAppProxyEnabled = true
}
if (managedModeEnabled) {
isScanning = true
scope.launch {
val chinaApps = scanAllChinaApps()
withContext(Dispatchers.IO) {
Settings.perAppProxyManagedList = chinaApps
}
isScanning = false
}
}
},
) {
Text(stringResource(R.string.ok))
}
}
isShizukuBinderReady -> {
TextButton(
onClick = {
PackageQueryManager.requestShizukuPermission()
},
) {
Text(stringResource(R.string.request_shizuku))
}
}
isShizukuInstalled -> {
TextButton(
onClick = {
showShizukuDialog = false
val intent = context.packageManager.getLaunchIntentForPackage("moe.shizuku.privileged.api")
if (intent != null) {
context.startActivity(intent)
}
},
) {
Text(stringResource(R.string.start_shizuku))
}
}
else -> {
TextButton(
onClick = {
showShizukuDialog = false
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://shizuku.rikka.app/"))
context.startActivity(intent)
},
) {
Text(stringResource(R.string.get_shizuku))
}
}
}
},
dismissButton = {
if (!isShizukuAvailable) {
TextButton(
onClick = { showShizukuDialog = false },
) {
Text(stringResource(R.string.cancel))
}
}
},
)
}
// ROOT dialog
if (showRootDialog) {
AlertDialog(
onDismissRequest = { showRootDialog = false },
title = {
Text(stringResource(R.string.per_app_proxy))
},
text = {
Text(stringResource(R.string.per_app_proxy_root_required))
},
confirmButton = {
TextButton(
onClick = { showPerAppProxyDialog = false },
onClick = {
scope.launch {
val hasRoot = PackageQueryManager.checkRootAvailable()
if (hasRoot) {
showRootDialog = false
perAppProxyEnabled = true
withContext(Dispatchers.IO) {
Settings.perAppProxyEnabled = true
}
if (managedModeEnabled) {
isScanning = true
val chinaApps = scanAllChinaApps()
withContext(Dispatchers.IO) {
Settings.perAppProxyManagedList = chinaApps
}
isScanning = false
}
} else {
showRootDialog = false
Toast.makeText(
context,
R.string.root_access_denied,
Toast.LENGTH_LONG
).show()
}
}
},
) {
Text(context.getString(R.string.ok))
Text(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(
onClick = { showRootDialog = false },
) {
Text(stringResource(R.string.cancel))
}
},
)
}
// Mode selection dialog
if (showModeDialog) {
AlertDialog(
onDismissRequest = { showModeDialog = false },
title = {
Text(stringResource(R.string.per_app_proxy_package_query_mode))
},
text = {
Column {
ListItem(
headlineContent = { Text("Shizuku") },
leadingContent = {
RadioButton(
selected = packageQueryMode == Settings.PACKAGE_QUERY_MODE_SHIZUKU,
onClick = null,
)
},
modifier = Modifier.clickable {
packageQueryMode = Settings.PACKAGE_QUERY_MODE_SHIZUKU
PackageQueryManager.setQueryMode(Settings.PACKAGE_QUERY_MODE_SHIZUKU)
scope.launch(Dispatchers.IO) {
Settings.perAppProxyPackageQueryMode = Settings.PACKAGE_QUERY_MODE_SHIZUKU
}
if (perAppProxyEnabled && !isShizukuAvailable) {
perAppProxyEnabled = false
scope.launch(Dispatchers.IO) {
Settings.perAppProxyEnabled = false
}
}
showModeDialog = false
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
),
)
ListItem(
headlineContent = { Text("ROOT") },
leadingContent = {
RadioButton(
selected = packageQueryMode == Settings.PACKAGE_QUERY_MODE_ROOT,
onClick = null,
)
},
modifier = Modifier.clickable {
packageQueryMode = Settings.PACKAGE_QUERY_MODE_ROOT
PackageQueryManager.setQueryMode(Settings.PACKAGE_QUERY_MODE_ROOT)
scope.launch(Dispatchers.IO) {
Settings.perAppProxyPackageQueryMode = Settings.PACKAGE_QUERY_MODE_ROOT
}
showModeDialog = false
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
),
)
}
},
confirmButton = {},
)
}
}
@@ -390,25 +659,22 @@ fun ProfileOverrideScreen(navController: NavController) {
private suspend fun scanAllChinaApps(): Set<String> = withContext(Dispatchers.Default) {
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.MATCH_UNINSTALLED_PACKAGES
PackageManager.MATCH_UNINSTALLED_PACKAGES or
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
} else {
@Suppress("DEPRECATION")
PackageManager.GET_UNINSTALLED_PACKAGES
PackageManager.GET_UNINSTALLED_PACKAGES or
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
val installedPackages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Application.packageManager.getInstalledPackages(
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
)
} else {
@Suppress("DEPRECATION")
Application.packageManager.getInstalledPackages(packageManagerFlags)
}
val installedPackages = PackageQueryManager.getInstalledPackages(packageManagerFlags)
val chinaApps = mutableSetOf<String>()
installedPackages.map { packageInfo ->
async {
if (PerAppProxyActivity.scanChinaPackage(packageInfo.packageName)) {
if (PerAppProxyActivity.scanChinaPackage(packageInfo)) {
synchronized(chinaApps) {
chinaApps.add(packageInfo.packageName)
}

View File

@@ -20,6 +20,7 @@ object SettingsKey {
const val PER_APP_PROXY_LIST = "per_app_proxy_list"
const val PER_APP_PROXY_MANAGED_MODE = "per_app_proxy_managed_mode"
const val PER_APP_PROXY_MANAGED_LIST = "per_app_proxy_managed_list"
const val PER_APP_PROXY_PACKAGE_QUERY_MODE = "per_app_proxy_package_query_mode"
const val SYSTEM_PROXY_ENABLED = "system_proxy_enabled"

View File

@@ -79,6 +79,10 @@ object Settings {
var perAppProxyManagedMode by dataStore.boolean(SettingsKey.PER_APP_PROXY_MANAGED_MODE) { false }
var perAppProxyManagedList by dataStore.stringSet(SettingsKey.PER_APP_PROXY_MANAGED_LIST) { emptySet() }
const val PACKAGE_QUERY_MODE_SHIZUKU = "SHIZUKU"
const val PACKAGE_QUERY_MODE_ROOT = "ROOT"
var perAppProxyPackageQueryMode by dataStore.string(SettingsKey.PER_APP_PROXY_PACKAGE_QUERY_MODE) { PACKAGE_QUERY_MODE_SHIZUKU }
fun getEffectivePerAppProxyList(): Set<String> {
return if (perAppProxyManagedMode) {
perAppProxyList union perAppProxyManagedList

View File

@@ -22,6 +22,7 @@ import io.nekohasekai.sfa.databinding.ViewVpnAppItemBinding
import io.nekohasekai.sfa.ktx.dp2px
import io.nekohasekai.sfa.ktx.toStringIterator
import io.nekohasekai.sfa.ui.shared.AbstractActivity
import io.nekohasekai.sfa.vendor.PackageQueryManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -139,13 +140,7 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
@Suppress("DEPRECATION")
PackageManager.GET_UNINSTALLED_PACKAGES
}
val installedPackages =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(flag.toLong()))
} else {
@Suppress("DEPRECATION")
packageManager.getInstalledPackages(flag)
}
val installedPackages = PackageQueryManager.getInstalledPackages(flag)
val vpnAppList =
installedPackages.filter {
it.services?.any { it.permission == Manifest.permission.BIND_VPN_SERVICE && it.applicationInfo != null }

View File

@@ -32,7 +32,8 @@ import io.nekohasekai.sfa.databinding.DialogProgressbarBinding
import io.nekohasekai.sfa.databinding.ViewAppListItemBinding
import io.nekohasekai.sfa.ktx.clipboardText
import io.nekohasekai.sfa.ui.shared.AbstractActivity
import io.nekohasekai.sfa.vendor.Vendor
import io.nekohasekai.sfa.vendor.PackageQueryManager
import io.nekohasekai.sfa.vendor.PrivilegedAccessRequiredException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@@ -80,6 +81,8 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
val applicationLabel by lazy {
appInfo.loadLabel(packageManager).toString()
}
val info: PackageInfo get() = packageInfo
}
private lateinit var adapter: ApplicationAdapter
@@ -91,19 +94,6 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Check if Per-app Proxy is available
if (!Vendor.isPerAppProxyAvailable()) {
MaterialAlertDialogBuilder(this)
.setTitle("Unavailable")
.setMessage(getString(R.string.per_app_proxy_disabled_message))
.setPositiveButton(R.string.ok) { _, _ ->
finish()
}
.setCancelable(false)
.show()
return
}
setTitle(R.string.per_app_proxy)
ViewCompat.setOnApplyWindowInsetsListener(binding.appList) { view, windowInsets ->
@@ -127,7 +117,9 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
binding.perAppProxyMode.setText(R.string.per_app_proxy_mode_exclude_description)
}
}
reloadApplicationList()
if (!reloadApplicationList()) {
return@withContext
}
filterApplicationList()
withContext(Dispatchers.Main) {
adapter = ApplicationAdapter(displayPackages)
@@ -139,25 +131,31 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
}
}
private fun reloadApplicationList() {
private suspend fun reloadApplicationList(): Boolean {
val packageManagerFlags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES or
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
} else {
@Suppress("DEPRECATION")
PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES
PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES or
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or
PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
val installedPackages =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getInstalledPackages(
PackageManager.PackageInfoFlags.of(
packageManagerFlags.toLong(),
),
)
} else {
@Suppress("DEPRECATION")
packageManager.getInstalledPackages(packageManagerFlags)
val installedPackages = try {
PackageQueryManager.getInstalledPackages(packageManagerFlags)
} catch (e: PrivilegedAccessRequiredException) {
withContext(Dispatchers.Main) {
Toast.makeText(
this@PerAppProxyActivity,
R.string.privileged_access_required,
Toast.LENGTH_LONG
).show()
finish()
}
return false
}
val packages = mutableListOf<PackageCache>()
for (packageInfo in installedPackages) {
if (packageInfo.packageName == packageName) continue
@@ -173,6 +171,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
}
this.packages = packages
this.selectedUIDs = selectedUIDs
return true
}
private fun filterApplicationList(selectedUIDs: Set<Int> = this.selectedUIDs) {
@@ -592,7 +591,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
val progressInt = AtomicInteger()
currentPackages.map { it ->
async {
if (scanChinaPackage(it.packageName)) {
if (scanChinaPackage(it.info)) {
foundApps[it.packageName] = it
}
runOnUiThread {
@@ -729,36 +728,17 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
}
fun scanChinaPackage(packageName: String): Boolean {
fun scanChinaPackage(packageInfo: PackageInfo): Boolean {
val packageName = packageInfo.packageName
skipPrefixList.forEach {
if (packageName == it || packageName.startsWith("$it.")) return false
}
val packageManagerFlags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
} else {
@Suppress("DEPRECATION")
PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
if (packageName.matches(chinaAppRegex)) {
Log.d("PerAppProxyActivity", "Match package name: $packageName")
return true
}
try {
val packageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Application.packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong()),
)
} else {
@Suppress("DEPRECATION")
Application.packageManager.getPackageInfo(
packageName,
packageManagerFlags,
)
}
val appInfo = packageInfo.applicationInfo ?: return false
packageInfo.services?.forEach {
if (it.name.matches(chinaAppRegex)) {