Reopening the app after it had been backgrounded killed the process.
The report said "sometimes"; measured on a device, the fault is
deterministic and only its trigger is occasional.
Commits
commit
what
issue
d64b069
fix(android): run main() once per process, not once per activity
Wails' Android entry point is nativeInit, which MainActivity.onCreate
calls. It does two things: re-points the native library's global JNI
reference at the calling WailsBridge, and runs go mainFunc(). WailsBridge.initialized is a per-instance field, so a recreated
activity's fresh bridge does not know the process already did this.
Android destroys and recreates an activity without restarting the
process. So main() ran a second time on a live app, and every path
out is fatal:
application.New returns the existingglobalApplication, silently
discarding the second Services set;
app.Run() refuses by design — a.starting is still true, because
Android's platformRun is select{} and never returns — with "application is running or a previous run has failed";
which reaches os.Exit(1), killing the first, healthy app: its
database, its queue, and the audio the mediaPlayback foreground
service is holding the process alive to play.
ActivityManager restarts it. That is the report verbatim.
It left no evidence because os.Exit is not a crash — and the slog
line naming the error went to /dev/null with the rest of fd 1, which
is #160.
Runtime evidence
This issue has never had any, so it is the deliverable as much as the
fix. Light Phone III (TLP301), Android 14 / SDK 34, arm64-v8a, WebView
Chrome 113 at 424x439. Debug build (app.yellowjacket.dev) installed beside the released v0.3.1 with install -r; nothing uninstalled.
The whole bug, three lines:
12:47:56.159 I/WailsBridge(22956): Wails bridge initialized
12:48:38.898 I/WailsBridge(22956): Wails bridge initialized <- same pid
12:48:39.357 I/ActivityManager: Process app.yellowjacket.dev (pid 22956) has died: fg TOP
12:48:39.386 W/ActivityTaskManager: Force removing ActivityRecord{...}: app died, no saved state
with, immediately above it,
12:48:39.291 I/WindowManager: finishDrawing of relaunch: Window{...MainActivity} 603ms
Two nativeInits in one pid, dead 459 ms later.
Two things about reading that. has died: fg TOP is not a memory
kill — the system does not reclaim the foreground process — which is
why "the OS killed it" is the wrong first hypothesis and why this sat
unverified. And there is no crash record at all: logcat -b crash
empty, no AndroidRuntime, no libc: Fatal signal, no tombstone.
Reproduction
"Don't keep activities" — the issue's own suggested lever — does not
work on this device.settings put global always_finish_activities 1
reads back as 1, am set-always-finish-activities does not exist on
this build, and the activity was never finished on backgrounding.
What works, deterministically and in one line, is a configuration change
the manifest does not declare (AndroidManifest.xml declares orientation|screenSize|keyboardHidden|uiMode):
adb shell settings put system font_scale 1.15
That is the same in-process destroy/recreate that a memory trim and a
locale change produce.
before
after
activity recreations observed
8
5
of which the process died
8
0
Plus, on the fixed build: 6 background/foreground cycles and 3
interleaved recreations, all on one pid, and make android-smoke SECONDS=60 green.
Runs where no recreation happened are inconclusive, not passes. A
harness that does not check for the second Wails bridge initialized
counts them green — which is exactly what makes this read as flakiness.
Does the activity restore the existing session, or is a restart a cold
start with the queue restored from the database?
Restore, and playback settles it rather than preference. The audio
lives in the Go process, so a cold start on every activity recreation
stops the music mid-song — the thing the mediaPlayback foreground
service exists to prevent. The activity is a view; the app is the
process. The frontend already cooperates: a recreated WebView loads the
page fresh and fetches state from a backend that never went away.
Written into CLAUDE.md.
Why the fix is in Go and not in Java
The obvious Java fix — making WailsBridge.initializedstatic, so the
second nativeInit is skipped — keeps the process alive and silently
breaks the app, because nativeInit is also what re-points the JNI
reference. Go would keep executing JavaScript against the destroyed
activity's WebView: the app opens, renders, and never receives another
backend event.
So the latch lets nativeInit do its first job and declines only its
second. Verified on the recreated page by hooking window._wails.dispatchWailsEvent and then backgrounding/foregrounding:
Events from the live services — so the recreated WebView is wired to the
app that never restarted. [] would have meant a stale reference.
The one Java change is the other half of the same question: onDestroy no longer calls bridge.shutdown(). It was harmless only by
accident — App.Quit() reaches an androidApp.destroy() that is an
empty method, and Run()'s deferred shutdownServices() cannot fire
behind select{}, so no ServiceShutdown has ever run on Android.
Removing it changes nothing today and stops the day someone implements destroy() from killing playback on a rotation.
What guards it
No tier here runs main() on Android, so "add a spec" is not available
and the guard is split, deliberately and incompletely:
TestMainClaimsBeforeItDoesAnything — a source sweep, in the
spirit of TestNoDirectRuntimeEmits, asserting the latch is the first statement of main(). The failure it exists for is not
deletion (loud) but a line creeping in above it: a second NewYellowJacketApp opens the SQLite database again, on every
recreation, silently. Confirmed non-vacuous — replacing the guard with _ = claimMainOnce() fails it.
TestMainRunsOncePerProcess — pins the latch itself.
A documented device check in .pi/skills/yellowjacket-dev/references/android-tier.md: the trigger,
the logcat signature, and the event probe.
What is not guarded, stated plainly: nothing in CI can see the
Android build's runtime at all, so a future change to Wails' nativeInit, to MainActivity, or to how Run() reports a second call
would not be caught by any of the above. This is a device-tier
regression and needs a device-tier re-check.
Filed while here
#159 (Priority/Critical) — wails3 task android:run:device
runs adb uninstall app.yellowjacket (the release id) while
installing the debug variant (app.yellowjacket.dev): it deletes the
user's library, then fails to launch the package it removed. android-tier.md had been recommending it. Found by reading the task
before running it, on a device carrying a real library.
#160 — route slog to logcat via __android_log_write. This
bug's entire diagnosis was one line the app already writes.
Reopening the app after it had been backgrounded killed the process.
The report said "sometimes"; measured on a device, the fault is
deterministic and only its *trigger* is occasional.
## Commits
| commit | what | issue |
|---|---|---|
| `d64b069` | `fix(android): run main() once per process, not once per activity` | #52 |
| `d714bd7` | `fix(android): keep the Go app alive when the activity is destroyed` | #52 |
| `8a757c9` | `docs(android): record the lifecycle model and the device check` | #52, #159, #160 |
## The mechanism
Wails' Android entry point is `nativeInit`, which `MainActivity.onCreate`
calls. It does two things: re-points the native library's global JNI
reference at the calling `WailsBridge`, and runs `go mainFunc()`.
`WailsBridge.initialized` is a **per-instance** field, so a recreated
activity's fresh bridge does not know the process already did this.
Android destroys and recreates an activity **without restarting the
process**. So `main()` ran a second time on a live app, and every path
out is fatal:
- `application.New` returns the *existing* `globalApplication`, silently
discarding the second `Services` set;
- `app.Run()` refuses by design — `a.starting` is still true, because
Android's `platformRun` is `select{}` and never returns — with
`"application is running or a previous run has failed"`;
- which reaches `os.Exit(1)`, killing the **first**, healthy app: its
database, its queue, and the audio the `mediaPlayback` foreground
service is holding the process alive to play.
ActivityManager restarts it. That is the report verbatim.
It left no evidence because `os.Exit` is not a crash — and the `slog`
line naming the error went to `/dev/null` with the rest of fd 1, which
is #160.
## Runtime evidence
This issue has never had any, so it is the deliverable as much as the
fix. Light Phone III (TLP301), Android 14 / SDK 34, arm64-v8a, WebView
Chrome 113 at 424x439. Debug build (`app.yellowjacket.dev`) installed
**beside** the released `v0.3.1` with `install -r`; nothing uninstalled.
The whole bug, three lines:
```
12:47:56.159 I/WailsBridge(22956): Wails bridge initialized
12:48:38.898 I/WailsBridge(22956): Wails bridge initialized <- same pid
12:48:39.357 I/ActivityManager: Process app.yellowjacket.dev (pid 22956) has died: fg TOP
12:48:39.386 W/ActivityTaskManager: Force removing ActivityRecord{...}: app died, no saved state
```
with, immediately above it,
```
12:48:39.291 I/WindowManager: finishDrawing of relaunch: Window{...MainActivity} 603ms
```
Two `nativeInit`s in one pid, dead 459 ms later.
Two things about reading that. **`has died: fg TOP` is not a memory
kill** — the system does not reclaim the foreground process — which is
why "the OS killed it" is the wrong first hypothesis and why this sat
unverified. And there is **no crash record at all**: `logcat -b crash`
empty, no `AndroidRuntime`, no `libc: Fatal signal`, no tombstone.
## Reproduction
**"Don't keep activities" — the issue's own suggested lever — does not
work on this device.** `settings put global always_finish_activities 1`
reads back as `1`, `am set-always-finish-activities` does not exist on
this build, and the activity was never finished on backgrounding.
What works, deterministically and in one line, is a configuration change
the manifest does not declare (`AndroidManifest.xml` declares
`orientation|screenSize|keyboardHidden|uiMode`):
```bash
adb shell settings put system font_scale 1.15
```
That is the same in-process destroy/recreate that a memory trim and a
locale change produce.
| | before | after |
|---|---|---|
| activity recreations observed | 8 | 5 |
| of which the process died | **8** | **0** |
Plus, on the fixed build: 6 background/foreground cycles and 3
interleaved recreations, all on one pid, and
`make android-smoke SECONDS=60` green.
**Runs where no recreation happened are inconclusive, not passes.** A
harness that does not check for the second `Wails bridge initialized`
counts them green — which is exactly what makes this read as flakiness.
## The model decision (#52's actual question)
Does the activity restore the existing session, or is a restart a cold
start with the queue restored from the database?
**Restore, and playback settles it rather than preference.** The audio
lives in the Go process, so a cold start on every activity recreation
stops the music mid-song — the thing the `mediaPlayback` foreground
service exists to prevent. The activity is a view; the app is the
process. The frontend already cooperates: a recreated WebView loads the
page fresh and fetches state from a backend that never went away.
Written into `CLAUDE.md`.
## Why the fix is in Go and not in Java
The obvious Java fix — making `WailsBridge.initialized` `static`, so the
second `nativeInit` is skipped — keeps the process alive and **silently
breaks the app**, because `nativeInit` is also what re-points the JNI
reference. Go would keep executing JavaScript against the destroyed
activity's WebView: the app opens, renders, and never receives another
backend event.
So the latch lets `nativeInit` do its first job and declines only its
second. Verified on the recreated page by hooking
`window._wails.dispatchWailsEvent` and then backgrounding/foregrounding:
```
["IndexStatusChanged","JobsChanged","JobsChanged","android:storageAccess"]
```
Events from the live services — so the recreated WebView is wired to the
app that never restarted. `[]` would have meant a stale reference.
The one Java change is the other half of the same question:
`onDestroy` no longer calls `bridge.shutdown()`. It was harmless only by
accident — `App.Quit()` reaches an `androidApp.destroy()` that is an
empty method, and `Run()`'s deferred `shutdownServices()` cannot fire
behind `select{}`, so **no `ServiceShutdown` has ever run on Android**.
Removing it changes nothing today and stops the day someone implements
`destroy()` from killing playback on a rotation.
## What guards it
No tier here runs `main()` on Android, so "add a spec" is not available
and the guard is split, deliberately and incompletely:
- **`TestMainClaimsBeforeItDoesAnything`** — a source sweep, in the
spirit of `TestNoDirectRuntimeEmits`, asserting the latch is the
*first* statement of `main()`. The failure it exists for is not
deletion (loud) but a line creeping in above it: a second
`NewYellowJacketApp` opens the SQLite database again, on every
recreation, silently. Confirmed non-vacuous — replacing the guard with
`_ = claimMainOnce()` fails it.
- **`TestMainRunsOncePerProcess`** — pins the latch itself.
- **A documented device check** in
`.pi/skills/yellowjacket-dev/references/android-tier.md`: the trigger,
the logcat signature, and the event probe.
**What is not guarded, stated plainly:** nothing in CI can see the
Android build's runtime at all, so a future change to Wails'
`nativeInit`, to `MainActivity`, or to how `Run()` reports a second call
would not be caught by any of the above. This is a device-tier
regression and needs a device-tier re-check.
## Filed while here
- **#159** (`Priority/Critical`) — `wails3 task android:run:device`
runs `adb uninstall app.yellowjacket` (the *release* id) while
installing the debug variant (`app.yellowjacket.dev`): it deletes the
user's library, then fails to launch the package it removed.
`android-tier.md` had been recommending it. Found by reading the task
before running it, on a device carrying a real library.
- **#160** — route `slog` to logcat via `__android_log_write`. This
bug's entire diagnosis was one line the app already writes.
## Verification
| | |
|---|---|
| `make lint` (3 tag sets) | 0 issues |
| `make test` (3 tag sets, `-race`) | pass |
| `make ui-test` | 949 tests, 85 files |
| `make e2e` | 187 passed, run **twice** against one app |
| `make css-check` / `bindings-check` / `skill-check` | clean |
| `tsc --noEmit`, `frontend/` and `e2e/` | clean |
| `make android` | release APK builds |
| android-tagged Go cross-compile | clean |
| device: recreation survival | 8/8 fatal → 5/5 survive |
| device: `make android-smoke SECONDS=60` | PASS |
| device: screenshot after resume | no change, insets correct |
Closes #52
Wails' Android entry point is `nativeInit`, which `MainActivity.onCreate`
calls, and it runs `go mainFunc()` every time. Android destroys and
recreates an activity **without restarting the process** -- a
configuration change the manifest does not declare, memory pressure, or
every background under "Don't keep activities" -- so main() ran again on
a live app.
Every path out of that is fatal. `application.New` returns the existing
app rather than building a second one, `app.Run()` then refuses because
`a.starting` is still true behind Android's `select{}`, and the
`os.Exit(1)` under that error takes the **first**, healthy app down with
it: its database, its queue, and the audio a mediaPlayback foreground
service is holding the process alive to play. ActivityManager restarts
the app, which is the report.
Measured on a Light Phone III (Android 14, arm64): conditional on the
activity actually being recreated, the process died 8 times out of 8.
The runs that "passed" were runs where no recreation happened, which is
the whole of the report's "sometimes". After this, 5/5 recreations
survive on one pid, plus six background/foreground cycles.
It never left evidence because os.Exit is not a crash: no tombstone, no
AndroidRuntime stack, nothing in `logcat -b crash`, and the slog line
naming the error went to /dev/null with the rest of fd 1.
The latch is first in main() because everything below it -- above all
NewYellowJacketApp, which opens the SQLite database -- is work that must
not happen twice in one process. It is inert off Android.
Returning early is not a degraded mode: nativeInit has already
re-pointed the JNI reference at the new bridge, so the recreated
WebView talks to the app that is still running, with its queue and
playback position intact. Verified by hooking dispatchWailsEvent on the
recreated page: IndexStatusChanged, JobsChanged, android:storageAccess.
No tier here runs main() on Android, so the guard is a source sweep, in
the spirit of TestNoDirectRuntimeEmits. The failure it exists for is not
the latch being deleted -- that is loud -- but a line creeping in above
it.
Closes#52
onDestroy called bridge.shutdown(), which is the natural reading of the
callback and is wrong for this app twice over. Android destroys and
recreates an activity without restarting the process, and when the user
really does leave, this app's reason for existing in the background is
that a song is playing -- which is what the mediaPlayback foreground
service holds the process alive for. Either way, tearing the Go side
down here stops the music.
It was harmless only by accident, and that is worth writing down:
nativeShutdown calls App.Quit(), whose Android destroy() is an empty
method, and Run()'s deferred shutdownServices() cannot fire because
platformRun is `select{}` and never returns. So **no ServiceShutdown has
ever run on Android**. Removing the call changes nothing today; it stops
the day someone implements destroy() from silently killing playback on a
rotation. There is no callback for the process going away -- Android
just kills it -- so durability here is the persist writers, which submit
on every mutation rather than at exit.
WailsBridge.initialize gains the comment for the trap next to it.
Making `initialized` static is the obvious reading of "initialise once
per process" and is wrong: nativeInit also stores the global JNI
reference to *this* bridge, so skipping it leaves Go executing
JavaScript against the destroyed activity's WebView, and the app opens,
renders, and never receives another backend event. The half that must
not repeat is latched in Go instead -- which is also where the damage
was, and the only place that can see it.
Refs #52
The lifecycle answer is load-bearing, so CLAUDE.md states it: an
activity is a view onto the process, and main() runs once per process.
The "restore the session or cold-start" question the issue asks for a
decision on is settled by playback rather than by preference -- the
audio lives in the Go process, so a cold start on every recreation
stops the music mid-song, which is the thing the foreground service
exists to prevent.
android-tier.md's build table said "arm64, real device -- unverified,
still" for five phases. It is verified now, on a Light Phone III
(Android 14, arm64-v8a, WebView Chrome 113 at 424x439), and what the
run found is a lifecycle section: how to force an activity recreation
on demand, the three-line logcat signature, why `has died: fg TOP` is
not a memory kill, and the second assertion that surviving does not
imply working.
It also carries the correction that "Don't keep activities" -- the
report's own suggested lever -- does not work on this device at all,
so nobody spends an afternoon on it. A configuration change the
manifest does not declare does, in one line.
And it stops recommending `wails3 task android:run:device`, which
uninstalls the released app and the user's library to install a build
with a different id (#159), in favour of the manual sequence.
NOTES.md carries the measurements, dated: 8 of 8 recreations fatal
before, 5 of 5 survived after, and the note that runs where no
recreation happened are inconclusive rather than passes -- a harness
that does not check for the second bridge init reports those as green
and reads as flakiness.
Refs #52, #159, #160
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Reopening the app after it had been backgrounded killed the process.
The report said "sometimes"; measured on a device, the fault is
deterministic and only its trigger is occasional.
Commits
d64b069fix(android): run main() once per process, not once per activityd714bd7fix(android): keep the Go app alive when the activity is destroyed8a757c9docs(android): record the lifecycle model and the device checkThe mechanism
Wails' Android entry point is
nativeInit, whichMainActivity.onCreatecalls. It does two things: re-points the native library's global JNI
reference at the calling
WailsBridge, and runsgo mainFunc().WailsBridge.initializedis a per-instance field, so a recreatedactivity's fresh bridge does not know the process already did this.
Android destroys and recreates an activity without restarting the
process. So
main()ran a second time on a live app, and every pathout is fatal:
application.Newreturns the existingglobalApplication, silentlydiscarding the second
Servicesset;app.Run()refuses by design —a.startingis still true, becauseAndroid's
platformRunisselect{}and never returns — with"application is running or a previous run has failed";os.Exit(1), killing the first, healthy app: itsdatabase, its queue, and the audio the
mediaPlaybackforegroundservice is holding the process alive to play.
ActivityManager restarts it. That is the report verbatim.
It left no evidence because
os.Exitis not a crash — and theslogline naming the error went to
/dev/nullwith the rest of fd 1, whichis #160.
Runtime evidence
This issue has never had any, so it is the deliverable as much as the
fix. Light Phone III (TLP301), Android 14 / SDK 34, arm64-v8a, WebView
Chrome 113 at 424x439. Debug build (
app.yellowjacket.dev) installedbeside the released
v0.3.1withinstall -r; nothing uninstalled.The whole bug, three lines:
with, immediately above it,
Two
nativeInits in one pid, dead 459 ms later.Two things about reading that.
has died: fg TOPis not a memorykill — the system does not reclaim the foreground process — which is
why "the OS killed it" is the wrong first hypothesis and why this sat
unverified. And there is no crash record at all:
logcat -b crashempty, no
AndroidRuntime, nolibc: Fatal signal, no tombstone.Reproduction
"Don't keep activities" — the issue's own suggested lever — does not
work on this device.
settings put global always_finish_activities 1reads back as
1,am set-always-finish-activitiesdoes not exist onthis build, and the activity was never finished on backgrounding.
What works, deterministically and in one line, is a configuration change
the manifest does not declare (
AndroidManifest.xmldeclaresorientation|screenSize|keyboardHidden|uiMode):That is the same in-process destroy/recreate that a memory trim and a
locale change produce.
Plus, on the fixed build: 6 background/foreground cycles and 3
interleaved recreations, all on one pid, and
make android-smoke SECONDS=60green.Runs where no recreation happened are inconclusive, not passes. A
harness that does not check for the second
Wails bridge initializedcounts them green — which is exactly what makes this read as flakiness.
The model decision (#52's actual question)
Does the activity restore the existing session, or is a restart a cold
start with the queue restored from the database?
Restore, and playback settles it rather than preference. The audio
lives in the Go process, so a cold start on every activity recreation
stops the music mid-song — the thing the
mediaPlaybackforegroundservice exists to prevent. The activity is a view; the app is the
process. The frontend already cooperates: a recreated WebView loads the
page fresh and fetches state from a backend that never went away.
Written into
CLAUDE.md.Why the fix is in Go and not in Java
The obvious Java fix — making
WailsBridge.initializedstatic, so thesecond
nativeInitis skipped — keeps the process alive and silentlybreaks the app, because
nativeInitis also what re-points the JNIreference. Go would keep executing JavaScript against the destroyed
activity's WebView: the app opens, renders, and never receives another
backend event.
So the latch lets
nativeInitdo its first job and declines only itssecond. Verified on the recreated page by hooking
window._wails.dispatchWailsEventand then backgrounding/foregrounding:Events from the live services — so the recreated WebView is wired to the
app that never restarted.
[]would have meant a stale reference.The one Java change is the other half of the same question:
onDestroyno longer callsbridge.shutdown(). It was harmless only byaccident —
App.Quit()reaches anandroidApp.destroy()that is anempty method, and
Run()'s deferredshutdownServices()cannot firebehind
select{}, so noServiceShutdownhas ever run on Android.Removing it changes nothing today and stops the day someone implements
destroy()from killing playback on a rotation.What guards it
No tier here runs
main()on Android, so "add a spec" is not availableand the guard is split, deliberately and incompletely:
TestMainClaimsBeforeItDoesAnything— a source sweep, in thespirit of
TestNoDirectRuntimeEmits, asserting the latch is thefirst statement of
main(). The failure it exists for is notdeletion (loud) but a line creeping in above it: a second
NewYellowJacketAppopens the SQLite database again, on everyrecreation, silently. Confirmed non-vacuous — replacing the guard with
_ = claimMainOnce()fails it.TestMainRunsOncePerProcess— pins the latch itself..pi/skills/yellowjacket-dev/references/android-tier.md: the trigger,the logcat signature, and the event probe.
What is not guarded, stated plainly: nothing in CI can see the
Android build's runtime at all, so a future change to Wails'
nativeInit, toMainActivity, or to howRun()reports a second callwould not be caught by any of the above. This is a device-tier
regression and needs a device-tier re-check.
Filed while here
Priority/Critical) —wails3 task android:run:deviceruns
adb uninstall app.yellowjacket(the release id) whileinstalling the debug variant (
app.yellowjacket.dev): it deletes theuser's library, then fails to launch the package it removed.
android-tier.mdhad been recommending it. Found by reading the taskbefore running it, on a device carrying a real library.
slogto logcat via__android_log_write. Thisbug's entire diagnosis was one line the app already writes.
Verification
make lint(3 tag sets)make test(3 tag sets,-race)make ui-testmake e2emake css-check/bindings-check/skill-checktsc --noEmit,frontend/ande2e/make androidmake android-smoke SECONDS=60Closes #52
Wails' Android entry point is `nativeInit`, which `MainActivity.onCreate` calls, and it runs `go mainFunc()` every time. Android destroys and recreates an activity **without restarting the process** -- a configuration change the manifest does not declare, memory pressure, or every background under "Don't keep activities" -- so main() ran again on a live app. Every path out of that is fatal. `application.New` returns the existing app rather than building a second one, `app.Run()` then refuses because `a.starting` is still true behind Android's `select{}`, and the `os.Exit(1)` under that error takes the **first**, healthy app down with it: its database, its queue, and the audio a mediaPlayback foreground service is holding the process alive to play. ActivityManager restarts the app, which is the report. Measured on a Light Phone III (Android 14, arm64): conditional on the activity actually being recreated, the process died 8 times out of 8. The runs that "passed" were runs where no recreation happened, which is the whole of the report's "sometimes". After this, 5/5 recreations survive on one pid, plus six background/foreground cycles. It never left evidence because os.Exit is not a crash: no tombstone, no AndroidRuntime stack, nothing in `logcat -b crash`, and the slog line naming the error went to /dev/null with the rest of fd 1. The latch is first in main() because everything below it -- above all NewYellowJacketApp, which opens the SQLite database -- is work that must not happen twice in one process. It is inert off Android. Returning early is not a degraded mode: nativeInit has already re-pointed the JNI reference at the new bridge, so the recreated WebView talks to the app that is still running, with its queue and playback position intact. Verified by hooking dispatchWailsEvent on the recreated page: IndexStatusChanged, JobsChanged, android:storageAccess. No tier here runs main() on Android, so the guard is a source sweep, in the spirit of TestNoDirectRuntimeEmits. The failure it exists for is not the latch being deleted -- that is loud -- but a line creeping in above it. Closes #52onDestroy called bridge.shutdown(), which is the natural reading of the callback and is wrong for this app twice over. Android destroys and recreates an activity without restarting the process, and when the user really does leave, this app's reason for existing in the background is that a song is playing -- which is what the mediaPlayback foreground service holds the process alive for. Either way, tearing the Go side down here stops the music. It was harmless only by accident, and that is worth writing down: nativeShutdown calls App.Quit(), whose Android destroy() is an empty method, and Run()'s deferred shutdownServices() cannot fire because platformRun is `select{}` and never returns. So **no ServiceShutdown has ever run on Android**. Removing the call changes nothing today; it stops the day someone implements destroy() from silently killing playback on a rotation. There is no callback for the process going away -- Android just kills it -- so durability here is the persist writers, which submit on every mutation rather than at exit. WailsBridge.initialize gains the comment for the trap next to it. Making `initialized` static is the obvious reading of "initialise once per process" and is wrong: nativeInit also stores the global JNI reference to *this* bridge, so skipping it leaves Go executing JavaScript against the destroyed activity's WebView, and the app opens, renders, and never receives another backend event. The half that must not repeat is latched in Go instead -- which is also where the damage was, and the only place that can see it. Refs #52