Skip to content

chore(android): enable StrictMode penaltyDeath in the demo app#489

Draft
jkmassel wants to merge 4 commits intotrunkfrom
jkmassel/issue-488
Draft

chore(android): enable StrictMode penaltyDeath in the demo app#489
jkmassel wants to merge 4 commits intotrunkfrom
jkmassel/issue-488

Conversation

@jkmassel
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel commented May 4, 2026

Closes #488. Configures StrictMode in the demo app's Application.onCreate() with detectAll() + penaltyDeath() on ThreadPolicy and penaltyLog() on VmPolicy, gated on BuildConfig.DEBUG. Release builds are unaffected.

Diverges from the phased Phase 1 / Phase 2 plan in #488 — the original plan was penaltyLog only, with penaltyDeath deferred to a follow-up after the catalogued violations were addressed. We've collapsed both phases into one PR: the library-side violations are wrapped at the demo call sites (per the issue's "no library permits" rule), so penaltyDeath ships green on day one and any future regression crashes the demo immediately.

Demo-side initialization shape

Because penaltyDeath crashes on any violation, the demo had to be restructured so nothing on the main thread touches disk:

  • AccountRepository is initialized lazily on Dispatchers.IO behind a Deferred. Its constructor reads filesDir and triggers com.sun.jna.Native.<clinit>, which runs ~8 disk reads while loading the dispatch library.
  • A new GutenbergKitApplication.withAccountRepository { ... } helper awaits the deferred and dispatches the call to Dispatchers.IO. All .all() / .store() / .remove() callsites across MainActivity, AuthenticationManager, EditorActivity, PostsListActivity, and SitePreparationViewModel use it.
  • SitePreparationViewModel.createEditorService wraps EditorService.create in withContext(IO). Same for EditorService.purge and EditorService.fetchAssetBundleCount.
  • SitePreparationActivity.launchEditor moves EditorDependenciesSerializer.writeToDisk into a coroutine before startActivity.
  • EditorActivity.onCreate reads dependencies on IO into Compose state and defers setContent until the read completes.

NetworkAvailabilityProvider stays initialized eagerly on the main thread — its constructor only stores a SAM lambda.

Library-side debt the demo wraps around

docs/code/strictmode.md lists the library-internal disk I/O the demo wraps in withContext(IO) rather than fixing upstream:

  • EditorService.create (sync EditorHTTPClient SSL setup + Paths.cacheRoot / storageRoot / defaultTempStorageRoot + EditorURLCache.<init> + EditorAssetsLibrary.<init>).
  • EditorService.fetchAssetBundleCount / EditorAssetsLibrary.readAssetBundles.
  • EditorDependenciesSerializer.writeToDisk / readFromDisk.

Each is a candidate for a follow-up that moves the I/O off the caller's thread or exposes a suspend API. Library fixes are out of scope here per #488.

Test plan

Verified on a Pixel 9 (Android 16) and an Android 14 emulator. Zero ThreadPolicy violations, no crashes:

  • App cold launch — emulator (no saved accounts) and Pixel 9 (saved account, Keystore decrypt path)
  • Tap Standalone editorSitePreparationActivity loads
  • Tap Prepare Editor → editor dependencies load
  • Tap StartEditorActivity loads, editor renders
  • Type into the editor
  • ./gradlew :app:compileDebugKotlin — clean
  • The full demo flow listed in Enable strict StrictMode in the Android demo app #488 (connected sites, native inserter sheet, photo permission paths, search, overflow menu, backgrounding) — left for incremental verification as those flows are walked through. Any new flag = wrap at the demo level (docs/code/strictmode.md documents the convention).

Files changed

  • android/app/src/main/java/com/example/gutenbergkit/GutenbergKitApplication.kt — StrictMode setup, Deferred<AccountRepository>, withAccountRepository helper.
  • android/app/src/main/java/com/example/gutenbergkit/MainActivity.kt — load saved accounts in lifecycleScope, dispatch delete to IO.
  • android/app/src/main/java/com/example/gutenbergkit/AuthenticationManager.kt — take GutenbergKitApplication instead of AccountRepository; dispatch store/all paths to IO.
  • android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.ktcreateEditorService(...) wrapper, IO dispatch on purge / fetchAssetBundleCount / account lookup.
  • android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt — async launchEditor.
  • android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt — async readFromDisk into Compose state; deferred setContent.
  • android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt, PostsListActivity.kt — IO-dispatched account lookups.
  • docs/code/strictmode.md (new) — configuration, demo-side init shape, library debt, verification, conventions.
  • docs/code/README.md — link to the new doc.

Configure StrictMode in the demo's Application.onCreate() with detectAll() +
penaltyLog() on both ThreadPolicy and VmPolicy, gated on BuildConfig.DEBUG.

Phase 1 of #488 — log-only so existing library-side violations surface during
local dev without crashing manual testing. Phase 2 (penaltyDeath) is a
separate PR and depends on the library-side violations catalogued in
docs/code/strictmode.md being addressed.
@github-actions github-actions Bot added the [Type] Task Issues or PRs that have been broken down into an individual action to take label May 4, 2026
@jkmassel jkmassel marked this pull request as draft May 4, 2026 19:39
Switch the demo's StrictMode ThreadPolicy from penaltyLog to penaltyDeath, so
any new main-thread disk/network I/O introduced in the library crashes the
demo immediately rather than logging quietly. VmPolicy stays on penaltyLog —
VM violations fire during GC and would crash at unpredictable times unrelated
to the offending code path.

To make penaltyDeath viable, the demo's startup and editor flows are
restructured to keep the main thread off disk:

- AccountRepository init moves to Dispatchers.IO behind a Deferred. Callers
  use a new withAccountRepository helper that awaits the deferred and then
  hops to IO for the actual repo call.
- All AccountRepository reads/writes (.all(), .store(), .remove()) across
  MainActivity, AuthenticationManager, EditorActivity, PostsListActivity,
  and SitePreparationViewModel are wrapped via withAccountRepository.
- SitePreparationViewModel.createEditorService wraps EditorService.create
  in withContext(IO). EditorService.purge / fetchAssetBundleCount also.
- SitePreparationActivity.launchEditor moves
  EditorDependenciesSerializer.writeToDisk into a coroutine.
- EditorActivity.onCreate reads dependencies into Compose state on IO and
  defers setContent until the read completes.

NetworkAvailabilityProvider stays eager on the main thread (its constructor
only stores a SAM lambda).

Verified on a Pixel 9 (Android 16) and an emulator: cold launch + Standalone
Editor + Prepare Editor + Start + typing produces zero ThreadPolicy
violations.

Closes #488.
@jkmassel jkmassel changed the title chore(android): enable StrictMode penaltyLog in the demo app chore(android): enable StrictMode penaltyDeath in the demo app May 4, 2026
jkmassel added 2 commits May 4, 2026 14:05
The async deserialize of EditorDependencies left the activity rendering
nothing during the read and would have either hung blank or crashed the
activity coroutine on a deserialization failure.

Replace the boolean ready flag with a sealed Loading/Failed/Ready state.
While loading, show a CircularProgressIndicator. On failure, catch the
exception, surface its message, and offer a Close button. The Ready path is
unchanged.

This also makes the activity safe across process-death restore — a stale
EXTRA_DEPENDENCIES_PATH whose temp file has since been cleaned up now lands
on the error screen instead of crashing.
CI Detekt flagged three issues from the prior commits:

- AuthenticationManager.handleOAuthCallback was 61 lines (max 60). Extract a
  persistOAuthAccount helper that takes the unpacked blogId / accessToken and
  handles the repo write + OAuth scratch-state cleanup.
- EditorActivity.onCreate was 63 lines (max 60). Extract the dependencies
  loader to loadDependenciesFromIntent, which returns the state holder.
- The catch (Exception) around EditorDependenciesSerializer.readFromDisk was
  redundant — the serializer already swallows exceptions internally and
  returns null on failure. Drop the try/catch and treat a null return from a
  non-null path as the Failed signal.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Task Issues or PRs that have been broken down into an individual action to take

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enable strict StrictMode in the Android demo app

1 participant