Android: app crashes or restarts when reopened after running in the background #52

Closed
opened 2026-08-18 05:58:15 +00:00 by logan · 2 comments
Collaborator

Report

Reopening the app after it has been running for a while sometimes causes it to crash or restart. We should go through the start/stop and background/foreground lifecycles.

Findings / where to look

  • build/android/ is committed and hand-edited like source; MainActivity owns the safe-area insets and the back-button handling (webView.canGoBack(), finishing the activity otherwise).
  • The foreground service is started when playback starts, deliberately, because Android 12+ forbids starting one from the background (backend/mediacontrols/android.go).
  • The likely candidates: the activity being destroyed and recreated (configuration change or memory pressure) while the Go side keeps running; the WebView being reloaded and the frontend re-initialising against a backend that already has state; or the process being killed and restarted with a foreground service still live.
  • The Wails v3 service contexts are cancelled on shutdown — worth checking what "shutdown" means on Android when only the activity goes away.

Direction

  1. Reproduce deliberately: enable "Don't keep activities" in developer options, background/foreground repeatedly, rotate, and let it sit for hours.
  2. Capture logcat around the failure — this needs a real device; no test tier here can see it. make android-inspect/make android-eval reach the WebView, but the crash is likely below that.
  3. Then decide the model: does the activity restore the existing session, or is a restart always a cold start with the queue restored from the database (which is persisted already)?
**Report** Reopening the app after it has been running for a while sometimes causes it to crash or restart. We should go through the start/stop and background/foreground lifecycles. **Findings / where to look** - `build/android/` is committed and hand-edited like source; `MainActivity` owns the safe-area insets and the back-button handling (`webView.canGoBack()`, finishing the activity otherwise). - The foreground service is started when playback starts, deliberately, because Android 12+ forbids starting one from the background (`backend/mediacontrols/android.go`). - The likely candidates: the activity being destroyed and recreated (configuration change or memory pressure) while the Go side keeps running; the WebView being reloaded and the frontend re-initialising against a backend that already has state; or the process being killed and restarted with a foreground service still live. - The Wails v3 service contexts are cancelled on shutdown — worth checking what "shutdown" means on Android when only the activity goes away. **Direction** 1. Reproduce deliberately: enable "Don't keep activities" in developer options, background/foreground repeatedly, rotate, and let it sit for hours. 2. Capture `logcat` around the failure — this needs a real device; no test tier here can see it. `make android-inspect`/`make android-eval` reach the WebView, but the crash is likely below that. 3. Then decide the model: does the activity restore the existing session, or is a restart always a cold start with the queue restored from the database (which is persisted already)?
logan added the Area/PackagingKind/BugPlatform/Android
Priority
Critical
1
labels 2026-08-18 14:36:12 +00:00
logan self-assigned this 2026-08-20 16:07:14 +00:00
logan added the
Status
In Progress
label 2026-08-20 16:07:14 +00:00
Author
Collaborator

Claiming this. Branch: fix/52-android-activity-recreation-restarts-the-process.

Approach. The failure is reachable from committed source without a
device, and I have traced it end to end before reaching for logcat; the
device run is verification, not discovery. Diagnosis in a follow-up
comment.

Short form: nativeInit starts Go's main() on every activity
creation
, and main() ends in os.Exit(1) on a path that a second
run always takes — so an activity recreated while the process lives
kills the process. That is the "sometimes", and it is why the crash has
never left a tombstone.

Claiming this. Branch: `fix/52-android-activity-recreation-restarts-the-process`. **Approach.** The failure is reachable from committed source without a device, and I have traced it end to end before reaching for logcat; the device run is verification, not discovery. Diagnosis in a follow-up comment. Short form: `nativeInit` starts Go's `main()` on **every activity creation**, and `main()` ends in `os.Exit(1)` on a path that a second run always takes — so an activity recreated while the process lives kills the process. That is the "sometimes", and it is why the crash has never left a tombstone.
Author
Collaborator

Reproduced, diagnosed and fixed — PR #161. Runtime evidence, which
this issue has never had, below.

The fault is deterministic; only the trigger is occasional. That is
the whole of "sometimes".

Mechanism. nativeInit — which MainActivity.onCreate calls —
re-points the JNI reference at the new bridge and runs
go mainFunc(). WailsBridge.initialized is per-instance, so a
recreated activity's bridge does not know the process already did this.
Android recreates an activity without restarting the process, so
main() ran again on a live app: application.New returns the existing
app, app.Run() refuses (a.starting is still true behind Android's
select{}), and the os.Exit(1) under that error kills the first,
healthy app — its database, its queue, and the audio the foreground
service is holding the process alive to play.

Evidence. Light Phone III, Android 14, arm64-v8a. Debug build
installed beside the released v0.3.1; nothing uninstalled.

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.291 I/WindowManager: finishDrawing of relaunch: Window{...MainActivity} 603ms
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

has died: fg TOP is not a memory kill — the system does not
reclaim the foreground process — which is why "the OS killed it" was
the wrong hypothesis all along. And there is no crash record of any
kind: logcat -b crash empty, no AndroidRuntime, no
libc: Fatal signal, no tombstone.

On the Direction's step 1. "Don't keep activities" does not work
on this device
settings put global always_finish_activities 1
reads back 1, am set-always-finish-activities does not exist on this
build, and the activity is never finished on backgrounding. Do not
spend time on it. A configuration change the manifest does not declare
does the job in one line, deterministically:

adb shell settings put system font_scale 1.15

(AndroidManifest.xml declares orientation|screenSize|keyboardHidden| uiMode, so none of those are triggers.)

Result: 8 of 8 recreations killed the process before, 0 of 5 after,
plus 6 background/foreground cycles and 3 interleaved recreations on one
pid. Runs where no recreation happened are inconclusive rather than
passes — a harness that does not check for the second bridge init counts
them green, which is what makes this read as flaky.

On the Direction's step 3, the model question. Restore the existing
session, and playback settles it rather than preference: the audio lives
in the Go process, so a cold start on every recreation stops the music
mid-song — the thing the mediaPlayback service exists to prevent. The
activity is a view; the app is the process. Written into CLAUDE.md.

Worth knowing for anything else that touches this: the tempting Java
fix (making initialized static) keeps the process alive and silently
breaks the app, because it also skips the JNI re-point — Go then
executes JavaScript against the destroyed WebView. Verified the real fix
does not, by hooking dispatchWailsEvent on the recreated page:
["IndexStatusChanged","JobsChanged","JobsChanged","android:storageAccess"].

Also found: no ServiceShutdown has ever run on Android
App.Quit() reaches an empty destroy(), and Run()'s deferred
shutdownServices() cannot fire behind select{}.

Filed while here: #159 (android:run:device uninstalls the released app
and the user's library — Priority/Critical) and #160 (route slog to
logcat; this bug's entire diagnosis was a line the app already writes,
sent to /dev/null).

**Reproduced, diagnosed and fixed — PR #161.** Runtime evidence, which this issue has never had, below. **The fault is deterministic; only the trigger is occasional.** That is the whole of "sometimes". **Mechanism.** `nativeInit` — which `MainActivity.onCreate` calls — re-points the JNI reference at the new bridge *and* runs `go mainFunc()`. `WailsBridge.initialized` is per-instance, so a recreated activity's bridge does not know the process already did this. Android recreates an activity without restarting the process, so `main()` ran again on a live app: `application.New` returns the existing app, `app.Run()` refuses (`a.starting` is still true behind Android's `select{}`), and the `os.Exit(1)` under that error kills the **first**, healthy app — its database, its queue, and the audio the foreground service is holding the process alive to play. **Evidence.** Light Phone III, Android 14, arm64-v8a. Debug build installed *beside* the released v0.3.1; nothing uninstalled. ``` 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.291 I/WindowManager: finishDrawing of relaunch: Window{...MainActivity} 603ms 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 ``` `has died: fg TOP` is **not** a memory kill — the system does not reclaim the foreground process — which is why "the OS killed it" was the wrong hypothesis all along. And there is no crash record of any kind: `logcat -b crash` empty, no `AndroidRuntime`, no `libc: Fatal signal`, no tombstone. **On the Direction's step 1.** "Don't keep activities" **does not work on this device** — `settings put global always_finish_activities 1` reads back `1`, `am set-always-finish-activities` does not exist on this build, and the activity is never finished on backgrounding. Do not spend time on it. A configuration change the manifest does not declare does the job in one line, deterministically: ```bash adb shell settings put system font_scale 1.15 ``` (`AndroidManifest.xml` declares `orientation|screenSize|keyboardHidden| uiMode`, so none of those are triggers.) **Result: 8 of 8 recreations killed the process before, 0 of 5 after**, plus 6 background/foreground cycles and 3 interleaved recreations on one pid. Runs where no recreation happened are inconclusive rather than passes — a harness that does not check for the second bridge init counts them green, which is what makes this read as flaky. **On the Direction's step 3, the model question.** Restore the existing session, and playback settles it rather than preference: the audio lives in the Go process, so a cold start on every recreation stops the music mid-song — the thing the `mediaPlayback` service exists to prevent. The activity is a view; the app is the process. Written into `CLAUDE.md`. Worth knowing for anything else that touches this: the tempting Java fix (making `initialized` static) keeps the process alive and silently breaks the app, because it also skips the JNI re-point — Go then executes JavaScript against the destroyed WebView. Verified the real fix does not, by hooking `dispatchWailsEvent` on the recreated page: `["IndexStatusChanged","JobsChanged","JobsChanged","android:storageAccess"]`. Also found: **no `ServiceShutdown` has ever run on Android** — `App.Quit()` reaches an empty `destroy()`, and `Run()`'s deferred `shutdownServices()` cannot fire behind `select{}`. Filed while here: #159 (`android:run:device` uninstalls the released app and the user's library — `Priority/Critical`) and #160 (route `slog` to logcat; this bug's entire diagnosis was a line the app already writes, sent to `/dev/null`).
logan closed this issue 2026-08-20 17:18:19 +00:00
gitea-actions bot removed the
Status
In Progress
label 2026-08-20 17:18:31 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#52