Add alternative support for QUERY_ALL_PACKAGES in play flavor
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user