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 <noreply@anthropic.com>
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 handler = Handler(Looper.getMainLooper())
|
||||||
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||||
|
/* read from the charon and IO threads in computeExcludedPackages() */
|
||||||
|
@Volatile
|
||||||
private var config = RoutingConfig()
|
private var config = RoutingConfig()
|
||||||
private var service: VpnStateService? = null
|
private var service: VpnStateService? = null
|
||||||
private var migratedOnlyProfiles = 0
|
private var migratedOnlyProfiles = 0
|
||||||
@@ -158,7 +160,8 @@ class RoutingManager private constructor(private val context: Context)
|
|||||||
override fun onReceive(context: Context?, intent: Intent?)
|
override fun onReceive(context: Context?, intent: Intent?)
|
||||||
{
|
{
|
||||||
invalidateCache()
|
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)
|
fun updateConfig(transform: (RoutingConfig) -> RoutingConfig)
|
||||||
{
|
{
|
||||||
config = transform(config)
|
val previous = config
|
||||||
saveConfig(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()
|
invalidateCache()
|
||||||
publish()
|
publish()
|
||||||
if (config.enabled)
|
if (updated.enabled)
|
||||||
{ /* warm the package cache so the connect path stays fast */
|
{ /* warm the package cache (off the main thread) so the connect path stays fast */
|
||||||
scope.launch { computeExcludedPackages() }
|
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()
|
scheduleReconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,12 +397,15 @@ class RoutingManager private constructor(private val context: Context)
|
|||||||
*/
|
*/
|
||||||
fun computeExcludedPackages(): Set<String>
|
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
|
val current = config
|
||||||
if (!current.enabled)
|
if (!current.enabled)
|
||||||
{
|
{
|
||||||
return emptySet()
|
return emptySet()
|
||||||
}
|
}
|
||||||
val version = configVersion
|
|
||||||
packageCache?.let { (cachedVersion, packages) ->
|
packageCache?.let { (cachedVersion, packages) ->
|
||||||
if (cachedVersion == version)
|
if (cachedVersion == version)
|
||||||
{
|
{
|
||||||
@@ -624,7 +640,7 @@ class RoutingManager private constructor(private val context: Context)
|
|||||||
/** All installed apps that hold the INTERNET permission. */
|
/** All installed apps that hold the INTERNET permission. */
|
||||||
fun loadInternetPackages(pm: PackageManager): Set<String> = try
|
fun loadInternetPackages(pm: PackageManager): Set<String> = try
|
||||||
{
|
{
|
||||||
pm.getInstalledApplications(PackageManager.GET_META_DATA)
|
pm.getInstalledApplications(0)
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { pm.checkPermission(android.Manifest.permission.INTERNET, it.packageName) == PackageManager.PERMISSION_GRANTED }
|
.filter { pm.checkPermission(android.Manifest.permission.INTERNET, it.packageName) == PackageManager.PERMISSION_GRANTED }
|
||||||
.map { it.packageName }
|
.map { it.packageName }
|
||||||
|
|||||||
+2
@@ -74,6 +74,8 @@ import org.strongswan.android.ui.compose.theme.ShxSpacing
|
|||||||
fun DirectAppsScreen(onBack: () -> Unit, viewModel: DirectAppsViewModel = viewModel())
|
fun DirectAppsScreen(onBack: () -> Unit, viewModel: DirectAppsViewModel = viewModel())
|
||||||
{
|
{
|
||||||
val state by viewModel.ui.collectAsStateWithLifecycle()
|
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() }
|
androidx.compose.runtime.LaunchedEffect(Unit) { viewModel.refresh() }
|
||||||
DirectAppsContent(
|
DirectAppsContent(
|
||||||
state = state,
|
state = state,
|
||||||
|
|||||||
+1
-6
@@ -114,11 +114,6 @@ class DirectAppsViewModel(app: Application) : AndroidViewModel(app)
|
|||||||
.flowOn(Dispatchers.Default)
|
.flowOn(Dispatchers.Default)
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DirectAppsUiState())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DirectAppsUiState())
|
||||||
|
|
||||||
init
|
|
||||||
{
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** (Re)loads the installed apps; the previous list stays visible meanwhile. */
|
/** (Re)loads the installed apps; the previous list stays visible meanwhile. */
|
||||||
fun refresh()
|
fun refresh()
|
||||||
{
|
{
|
||||||
@@ -156,7 +151,7 @@ object DirectApps
|
|||||||
fun loadInstalledApps(pm: PackageManager, ownPackage: String): List<InstalledApp>
|
fun loadInstalledApps(pm: PackageManager, ownPackage: String): List<InstalledApp>
|
||||||
{
|
{
|
||||||
val collator = Collator.getInstance()
|
val collator = Collator.getInstance()
|
||||||
return pm.getInstalledApplications(PackageManager.GET_META_DATA)
|
return pm.getInstalledApplications(0)
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { it.packageName != ownPackage }
|
.filter { it.packageName != ownPackage }
|
||||||
.filter { pm.checkPermission(android.Manifest.permission.INTERNET, it.packageName) == PackageManager.PERMISSION_GRANTED }
|
.filter { pm.checkPermission(android.Manifest.permission.INTERNET, it.packageName) == PackageManager.PERMISSION_GRANTED }
|
||||||
|
|||||||
Reference in new issue
Block a user