android: routing - package scans off the main thread, no needless reconnects
- computeExcludedPackages() (full PackageManager scan) runs on Default, not Main.immediate, on config changes and package add/remove - updateConfig() ignores no-op transforms and reconnects when routing is switched off too (direct apps kept bypassing the tunnel until then) - config is volatile and read after its version, so an old config can no longer be cached under a new version - Direct apps loads the app list once per entry, without GET_META_DATA Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
07fb02790c
commit
ce80c36be1
3 files changed
+27
-14
No files matched your search
+24
-8
@@ -116,6 +116,8 @@ class RoutingManager private constructor(private val context: Context)
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
/* read from the charon and IO threads in computeExcludedPackages() */
|
||||
@Volatile
|
||||
private var config = RoutingConfig()
|
||||
private var service: VpnStateService? = null
|
||||
private var migratedOnlyProfiles = 0
|
||||
@@ -158,7 +160,8 @@ class RoutingManager private constructor(private val context: Context)
|
||||
override fun onReceive(context: Context?, intent: Intent?)
|
||||
{
|
||||
invalidateCache()
|
||||
scope.launch { computeExcludedPackages() }
|
||||
/* a full PackageManager scan: never on the main thread */
|
||||
scope.launch(Dispatchers.Default) { computeExcludedPackages() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,13 +186,23 @@ class RoutingManager private constructor(private val context: Context)
|
||||
|
||||
fun updateConfig(transform: (RoutingConfig) -> RoutingConfig)
|
||||
{
|
||||
config = transform(config)
|
||||
saveConfig(config)
|
||||
val previous = config
|
||||
val updated = transform(previous)
|
||||
if (updated == previous)
|
||||
{ /* e.g. a chip tapped that is already in that state: no reconnect */
|
||||
return
|
||||
}
|
||||
config = updated
|
||||
saveConfig(updated)
|
||||
invalidateCache()
|
||||
publish()
|
||||
if (config.enabled)
|
||||
{ /* warm the package cache so the connect path stays fast */
|
||||
scope.launch { computeExcludedPackages() }
|
||||
if (updated.enabled)
|
||||
{ /* warm the package cache (off the main thread) so the connect path stays fast */
|
||||
scope.launch(Dispatchers.Default) { computeExcludedPackages() }
|
||||
}
|
||||
if (updated.enabled || previous.enabled)
|
||||
{ /* also when routing was just switched off: the excluded apps keep
|
||||
* bypassing the tunnel until it is re-established */
|
||||
scheduleReconnect()
|
||||
}
|
||||
}
|
||||
@@ -384,12 +397,15 @@ class RoutingManager private constructor(private val context: Context)
|
||||
*/
|
||||
fun computeExcludedPackages(): Set<String>
|
||||
{
|
||||
/* version before config: updateConfig() writes config first, so this
|
||||
* order can at worst cache a newer config under an older version
|
||||
* (recomputed next time), never an old config under the new one */
|
||||
val version = configVersion
|
||||
val current = config
|
||||
if (!current.enabled)
|
||||
{
|
||||
return emptySet()
|
||||
}
|
||||
val version = configVersion
|
||||
packageCache?.let { (cachedVersion, packages) ->
|
||||
if (cachedVersion == version)
|
||||
{
|
||||
@@ -624,7 +640,7 @@ class RoutingManager private constructor(private val context: Context)
|
||||
/** All installed apps that hold the INTERNET permission. */
|
||||
fun loadInternetPackages(pm: PackageManager): Set<String> = try
|
||||
{
|
||||
pm.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||
pm.getInstalledApplications(0)
|
||||
.asSequence()
|
||||
.filter { pm.checkPermission(android.Manifest.permission.INTERNET, it.packageName) == PackageManager.PERMISSION_GRANTED }
|
||||
.map { it.packageName }
|
||||
|
||||
+2
@@ -74,6 +74,8 @@ import org.strongswan.android.ui.compose.theme.ShxSpacing
|
||||
fun DirectAppsScreen(onBack: () -> Unit, viewModel: DirectAppsViewModel = viewModel())
|
||||
{
|
||||
val state by viewModel.ui.collectAsStateWithLifecycle()
|
||||
/* the only load trigger (also re-entering the screen with a retained
|
||||
* view model picks up newly installed apps) */
|
||||
androidx.compose.runtime.LaunchedEffect(Unit) { viewModel.refresh() }
|
||||
DirectAppsContent(
|
||||
state = state,
|
||||
|
||||
+1
-6
@@ -114,11 +114,6 @@ class DirectAppsViewModel(app: Application) : AndroidViewModel(app)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DirectAppsUiState())
|
||||
|
||||
init
|
||||
{
|
||||
refresh()
|
||||
}
|
||||
|
||||
/** (Re)loads the installed apps; the previous list stays visible meanwhile. */
|
||||
fun refresh()
|
||||
{
|
||||
@@ -156,7 +151,7 @@ object DirectApps
|
||||
fun loadInstalledApps(pm: PackageManager, ownPackage: String): List<InstalledApp>
|
||||
{
|
||||
val collator = Collator.getInstance()
|
||||
return pm.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||
return pm.getInstalledApplications(0)
|
||||
.asSequence()
|
||||
.filter { it.packageName != ownPackage }
|
||||
.filter { pm.checkPermission(android.Manifest.permission.INTERNET, it.packageName) == PackageManager.PERMISSION_GRANTED }
|
||||
|
||||
Reference in new issue
Block a user