Skip to content

Add excluded apps config to Android#357

Open
johnmaguire wants to merge 6 commits intomainfrom
excluded-apps
Open

Add excluded apps config to Android#357
johnmaguire wants to merge 6 commits intomainfrom
excluded-apps

Conversation

@johnmaguire
Copy link
Member

Allows users to exclude apps from using the VPN - this is necessary for apps which need to speak to RFC 1918 space, such as IoT devices or cameras, despite the fact that the VPN does not advertise routes for this space.

Uses a 2-phase load for the app icons to avoid an observed slow load of the excluded apps page.

Uninstalled disabled apps are shown at the top, then installed & disabled, then forced disabled apps (the existing hardcoded apps), then all other apps.

image

Tested the behavior with an app that fails without disallowing it.

johnmaguire and others added 6 commits February 25, 2026 12:25
- Add ExcludedAppsScreen with searchable app list, icons, and checkboxes
- Add getInstalledApps method channel to retrieve installed Android apps
- Wire excludedApps through Site model (Dart + Kotlin) and config persistence
- Apply disallowedApplication in NebulaVpnService for selected packages
- Add Excluded Apps entry to Advanced Settings screen

Add QUERY_ALL_PACKAGES permission for excluded apps feature

Required on Android 11+ for getInstalledApplications() to return
all user-installed apps, not just system apps and those matching
the <queries> declaration.

Improve excluded apps screen performance

- Flutter: switch from Column+map to ListView.builder so only visible
  items are rendered (fixes scroll lag)
- Flutter: cache decoded icon bytes in _AppInfo to avoid re-decoding
  base64 on every rebuild
- Flutter: bypass FormPage/SingleChildScrollView to allow proper lazy
  list rendering; use SimplePage directly with scrollable=none
- Flutter: add cacheWidth/cacheHeight hints to Image.memory
- Kotlin: move icon encoding to background thread so it no longer
  blocks the main thread during initial load

Two-phase app loading: show list immediately, icons fill in after

- Split getInstalledApps into two methods:
  - android.getInstalledApps: returns names/packages only (fast)
  - android.getAppIcons: takes package names, returns icon map
- Filter to apps with INTERNET permission via getPackagesHoldingPermissions,
  matching WireGuard and Tailscale's approach — eliminates system services,
  libraries, and background-only processes that can't be meaningfully excluded
- Flutter phase 1: list appears as soon as names are loaded
- Flutter phase 2: icons fetched in background, fill in with single setState
- Icons stored in a separate iconCache map (no mutation of _AppInfo objects)

Load icons progressively in batches of 20

Icons now fill in as each batch of 20 completes rather than all
appearing at once after the full load, improving perceived performance.

Implement Claude Code improvements

Summary of changes

4 files modified across 5 improvements:

1. Icon resolution increased to 128x128 — MainActivity.kt now renders
   icons at 128x128 instead of 40x40, and excluded_apps_screen.dart uses
   cacheWidth: 128, cacheHeight: 128 to match.
2. Hard-coded exclusions surfaced in UI — NebulaVpnService.kt now has
   ALWAYS_EXCLUDED_APPS as a companion object constant (used by both
   startVpn() and the new channel handler). MainActivity.kt exposes
   android.getAlwaysExcludedApps via the method channel. The Flutter
   screen fetches this list and renders those apps as checked + disabled
   with 60% opacity and "(always excluded)" label.
3. Thread → coroutines migration — build.gradle.kts adds
   kotlinx-coroutines-android and lifecycle-runtime-ktx dependencies.
   Both getInstalledApps() and getAppIcons() in MainActivity.kt now use
   lifecycleScope.launch + withContext(Dispatchers.IO) instead of raw
   Thread + Handler.post.
4. Stale package filtering — _loadApps() in the Dart screen intersects
   selectedApps with the installed packages set, so uninstalled apps
   don't appear. The filtered list is what gets saved.
5. iconCache.addAll() instead of spread-copy — Avoids O(n) map copy per
   batch.
Add scrollable parameter to FormPage and use it in ExcludedAppsScreen
to replace manual PopScope/SimplePage/save/cancel wiring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of silently removing uninstalled packages from the selection,
show them at the top of the list with an italic 'Not installed' label,
subtle grey icon, and '(not installed)' subtitle. They remain
toggleable so users can deselect them to clean up.
@nbrownus
Copy link
Contributor

Would it make more sense to have the app exclusion at the app settings level instead of site specific to avoid having to block an app in multiple places?

@johnmaguire
Copy link
Member Author

johnmaguire commented Feb 27, 2026

@nbrownus Yeah, I considered that too. I don't have a strong preference, but it could cause some confusion in the future if we allow admins to restrict exclusions for managed sites, and there may be other site-specific use cases for disallowed apps beyond "this app doesn't work because it's WiFi IoT." Your call - either way solves my issue.

@JackDoan
Copy link
Contributor

Yeah my immediate reaction was "I bet someone is gonna want to only filter some apps sometimes"

@nbrownus
Copy link
Contributor

I think for most folks having it on the site is going to be just fine and likely 100% more obvious than if it was hidden in the app settings screen. It's mostly just a bit of a bummer if you have more than 1 site.

@nbrownus
Copy link
Contributor

That and it's a bit more hidden and inconvenient for the majority of folks that likely have a single site. Site config seems like a fine place to put it, would like to resolve #358 first as there could be some impact to how config works.

@nbrownus
Copy link
Contributor

nbrownus commented Mar 6, 2026

Ok, the config stuff has been reworked. We have two paths here.

  1. Write this into the raw config just like how dns_resolvers works. This method allows for yaml import and dn authoritative config without doing anything extra.
  2. Keep this as a top level, similar to how alwaysOn works. Requires additional work for dn authority and no yaml import support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants