Skip to content

fix: allow devnet url switch from app start intent for qa - #2148

Open
Bilb wants to merge 1 commit into
devfrom
fix-allow-devnet-switch-no-rebuild
Open

fix: allow devnet url switch from app start intent for qa#2148
Bilb wants to merge 1 commit into
devfrom
fix-allow-devnet-switch-no-rebuild

Conversation

@Bilb

@Bilb Bilb commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Lets QA automation point a build at a different devnet seed / service network via launch
intent extras, instead of requiring an app rebuild per target network.

Android apps can't read the launcher's environment, so this is the Android counterpart of
iOS's processUnitTestEnvVariablesIfNeeded: the equivalent channel is intent extras on the
launch activity, which Appium sets with appium:optionalIntentArguments.

'appium:optionalIntentArguments': '--es sessionDevnetSeedUrl http://10.0.0.1:1280'

Supported extras:

Extra Values
sessionDevnetSeedUrl http(s) URL; empty clears the override
sessionServiceNetwork mainnet | testnet | devnet

How it works

  • QaLaunchConfig.apply() runs from HomeActivity.onCreate, the launcher target via the
    RoutingActivity alias. It's in the plain onCreate overload rather than
    onCreate(savedInstanceState, ready) because the latter only runs when the base class
    didn't route away — so on a fresh install, exactly the automation case, it never fires.
  • Values are persisted to preferences, not held in memory, because the components that read
    them (SnodeDirectory.seedNodePool) are app-scoped singletons that may resolve before or
    after the first activity. They take effect on the next launch; the harness force-restarts
    after the first launch.
  • SnodeDirectory records a marker identifying the seed configuration each cached snode pool
    was fetched from, and discards the pool when that configuration changes. Without this, a
    cached pool from the previous network is happily reused and the switch appears to do nothing
    until app data is wiped. The marker is also re-checked before persisting a fetched pool, so a
    config change racing an in-flight fetch can't leave an old-network pool sitting behind an
    already-updated marker.

Security

The launcher alias is android:exported="true", so any app on the device can start it with
extras. Acting on them is gated on BuildConfig.ALLOW_QA_LAUNCH_CONFIG — a compile-time
constant, so R8 strips the reader entirely from builds that don't opt in. It defaults to
false in defaultConfig, is false for release/releaseWithDebugMenu, and true only
for debug/qa/automaticQa. Gating on anything weaker (a runtime pref, a build-type string
compare) would turn an exported launcher into a way for a third-party app to repoint a release
build's network.

Extras parsing is also hardened against a hostile Bundle: a caller can reference a
Parcelable class we don't have, and the unparcel then throws BadParcelableException, which
uncaught is a remote crash-on-launch for every QA build. A malformed Bundle is logged and
treated as no configuration.

Testing

  • Verified ALLOW_QA_LAUNCH_CONFIG in the generated BuildConfig for all five build types.
  • Malformed / unparseable values are logged and ignored rather than throwing, since this runs
    on the startup path.

@Bilb
Bilb force-pushed the fix-allow-devnet-switch-no-rebuild branch from 7c4d7dc to 539f3ed Compare August 3, 2026 16:59
* them (e.g. `SnodeDirectory.seedNodePool`) are app-scoped singletons that may resolve before or
* after the first activity is created. Persisting means the value is guaranteed to be in effect on
* the NEXT launch; callers that need it applied deterministically should force-restart the app after
* the first launch (which is what the appium harness does).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this comment relates to some local changes you have but at the moment I don't think Appium does a force-restart - openAndroidApp in open_app.ts it calls await wrappedDevice.createSession(capabilities)

prefs.setLastSnodePoolRefresh(0L)
}

prefs.setSnodePoolSeedMarker(marker)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like PathManager persists the onion request paths from previous launches so swapping environments could result in paths from previous launches sticking around (not a huge issue for our use case but might be good to clear it here as well):

private val warmUpJob: Deferred<Unit> = scope.async(start = CoroutineStart.LAZY) {
        val persisted = sanitizePaths(storage.getOnionRequestPaths())
        _paths.update { current -> if (current.isEmpty()) persisted else current }
    }

    init {
        // persist to DB whenever paths change
        scope.launch {
            _paths.drop(1).collectLatest { paths ->
                try {
                    if (paths.isEmpty()) storage.clearOnionRequestPaths()
                    else storage.setOnionRequestPaths(paths)
                } catch (e: Exception) {
                    Log.e("Onion Request", "Failed to persist paths to storage, keeping in-memory only", e)
                }
            }
        }
    }

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.

2 participants