diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/routing/RoutingManager.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/routing/RoutingManager.kt index 29473212b..7b8945711 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/routing/RoutingManager.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/routing/RoutingManager.kt @@ -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 { + /* 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 = 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 } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/DirectAppsScreen.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/DirectAppsScreen.kt index 2df0719ca..04ca15c3c 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/DirectAppsScreen.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/DirectAppsScreen.kt @@ -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, diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/DirectAppsViewModel.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/DirectAppsViewModel.kt index 9e940509f..9f61b4d7c 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/DirectAppsViewModel.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/compose/routing/DirectAppsViewModel.kt @@ -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 { 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 }