Skip to content

chore: code tidiness and structure#1

Open
jessejamesjohnston wants to merge 10 commits into
kuscher:mainfrom
jessejamesjohnston:chore/cleanup
Open

chore: code tidiness and structure#1
jessejamesjohnston wants to merge 10 commits into
kuscher:mainfrom
jessejamesjohnston:chore/cleanup

Conversation

@jessejamesjohnston

Copy link
Copy Markdown

Stacked contribution series — PR 1 of 8. This is the first of a focused, sequential series elevating Book Recovery to production-grade Android craft. Each PR is small enough to read in one sitting and builds on the previous one. Recommended review/merge order: #1 cleanup → toolchain → adaptive → expressive → verification → real-erase → dino → playful-status. Because these are stacked, GitHub shows each PR's diff against main; a later PR's diff includes the earlier ones until they merge, at which point it reduces to just its own changes.


chore: code tidiness and structure

Summary

Ground-preparation PR: makes the codebase easier to review and extend without changing behavior. Eight commits, each independently revertible:

  1. Remove stray scratch files (test.kt, test_api36.kt) from the module root — one-off API 36 notification probes outside any source set.
  2. Promote ~38 fully-qualified inline references to imports (FlashScreen was the main offender; also RecoveryApp, SelectDriveScreen, SelectModelScreen, UsbFlasher, KeepAliveService).
  3. Type-safe navigation routes — a sealed Route hierarchy replaces the eight string literals that the nav graph, top-bar step logic, and navigate() calls previously agreed on by convention. SelectChannel.forModel() centralizes the URL-encoding of the model-name argument.
  4. Extract flash orchestration into FlashViewModel — the 353-line FlashScreen composable managed the foreground service, notification building, erase simulation, and flasher lifecycle inline. Now: FlashViewModel owns the state machine (erasing → flashing → success/error, plus cancel-and-reset) and exposes one FlashUiState; FlashNotificationController owns the channel, KeepAliveService start/stop, and the API 36 Live Update / NotificationCompat builders; FlashScreen (172 lines) renders state. Only the POST_NOTIFICATIONS prompt and lifecycle/window-focus tracking stay in the composable — they need the view layer.
  5. Move hardcoded UI strings to resources — all screen copy, notification text, and the rotating status-word lists (now index-aligned <string-array>s) join the flasher messages that were already in strings.xml. Copy is byte-for-byte identical.
  6. Remove leftover debug logging (the 500ms Log.d progress line in the write loop, a Log.d in the permission-denied path) and fix the compiler warnings (deprecated LinearProgressIndicator overload, redundant initializer, unnecessary !!).
  7. .editorconfig matching the Kotlin official style already declared in gradle.properties.
  8. GitHub Actions CIassembleDebug + lint on PRs and pushes to main; lint HTML report uploaded on failure. No secrets required.

Motivation

Subsequent PRs (adaptive layouts, M3 Expressive polish, post-flash verification, and a couple of surprises) all touch the flash flow and navigation. This PR gets the structure ready so those diffs stay small and focused: feature PRs shouldn't have to carve business logic out of composables to add a pane layout, and route changes shouldn't require hunting string literals.

A concrete payoff already visible: because the flash state now lives in a ViewModel scoped to the flash destination, a configuration change (rotation, fold, resize) no longer depends solely on the activity's configChanges opt-out to keep the flash alive. The adaptive-layout PR will build on exactly this.

Behavior

Intentionally identical, with one deliberate exception: a one-line fix: commit makes the channel screen read "Step 2 of 4" instead of "Step 1 of 4" (select_channel was never in the step mapping). Things deliberately not fixed here:

  • Leaving the flash screen via Home does not cancel the in-flight write (the write loop is not cooperatively cancelled). Pre-existing; the verification PR will revisit UsbFlasher's cancellation story.
  • Copy quirks (mixed ASCII ... / typographic , Locale.US percentage formatting in notifications) preserved byte-for-byte; a copy pass can follow separately.

Testing

  • Verified on emulator (Pixel, API 36): full wizard walk — Welcome → Identify → model list (live manifest fetch; manufacturer + product dropdowns) → channel list for "ACER Chromebook Spin 513 (CP513-2H), …" (a name with commas/parens, exercising the encoded route argument) → drive screen with graceful empty state and disabled Continue. No exceptions in logcat.
  • Logic unit-tested: none added in this PR — the ViewModel is now testable, and the verification PR adds the first real unit tests where they earn their keep (digest logic).
  • Not verified with USB OTG hardware: the flash screen itself is unreachable on an emulator (no USB host devices). The refactor preserves the flash path structurally (same calls, same ordering); an on-device OTG flash test is queued for the next hardware session.
  • ./gradlew assembleDebug lint passes clean; the only remaining compiler warning (unused device parameter in EraseScreen) is deliberate — the erase flow is currently simulated, and making it real is planned follow-up work.

Deferred follow-ups

  • Cooperative cancellation of the write loop when the screen is abandoned.
  • Real erase implementation (EraseScreen currently simulates).
  • Copy/ellipsis consistency pass.

Verification

  • ./gradlew assembleDebug lint clean.
  • Wizard smoke-tested on the Android emulator (API 36): Welcome → Identify → model list (live manifest) → channel → drive, no logcat exceptions.
  • No USB hardware needed for this PR (no flash-path behavior changed); the flash path itself is exercised on real hardware in later PRs of the series.

test.kt and test_api36.kt were one-off probes for the API 36
Notification.ProgressStyle / setShortCriticalText APIs. They live outside
any source set, so they never compiled into the app; delete them to keep
the module root clean.
FlashScreen.kt in particular had ~38 fully-qualified references
(androidx.compose.ui.platform.LocalLifecycleOwner, android.app.PendingIntent,
and friends) inline in the code. Promote them all to proper imports in
FlashScreen, RecoveryApp, SelectDriveScreen, SelectModelScreen, UsbFlasher,
and KeepAliveService so the code reads like idiomatic Kotlin.

No behavior change; the compiled output is identical.
The navigation graph, the top-bar step indicator, and every navigate() call
previously agreed on eight string literals by convention alone. A typo in
any one of them would compile fine and fail at runtime.

Add a sealed Route hierarchy in ui/Route.kt as the single source of truth:
each destination exposes its route pattern, and SelectChannel (the only
parameterized destination) exposes forModel() so the URL-encoding of the
model-name argument lives in exactly one place.

No behavior change; the route strings are byte-for-byte the same.
FlashScreen was a 353-line composable that managed the Foreground Service,
built API-36 Live Update notifications, ran the erase simulation, drove
UsbFlasher, and rendered the UI, all inline. Business logic in the view
layer meant no state survival story of its own and no way to unit test the
flow.

Split it three ways:
- FlashViewModel owns the state machine (erasing -> flashing ->
  success/error, plus the cancel-and-reset path) and exposes a single
  FlashUiState via StateFlow. Scoped to the flash NavBackStackEntry, so
  the running flash now survives configuration changes by design rather
  than relying on the activity's configChanges opt-out.
- FlashNotificationController owns the notification channel, the
  KeepAliveService start/stop, and the Live Update / NotificationCompat
  builders. The ViewModel decides when to notify, never how.
- FlashScreen renders FlashUiState. The only view concerns left are the
  POST_NOTIFICATIONS permission prompt (needs an Activity) and
  lifecycle/window-focus tracking (needs a View), both reported to the
  ViewModel.

Also names the flasher's 'Cancelled' sentinel (UsbFlasher.RESULT_CANCELLED)
instead of comparing a bare string literal across files.

Behavior is unchanged: same state transitions, same notification timing and
content, same service lifetime.
Roughly half the user-facing copy lived in strings.xml (the flasher's step
and error messages); the other half was hardcoded in composables, the
notification builders, and the flash state machine. Move all of it to
resources so the copy is localizable and reviewable in one place:

- Screen copy for Welcome, Identify, SelectModel, SelectChannel,
  SelectDrive, Erase, and Flash.
- Notification titles, channel name, and the rotating fun-words/status-chip
  pairs (now index-aligned <string-array>s).
- Shared actions (Continue, OK, Back to Home) deduplicated.

Two deliberate non-changes, noted for review: the copy is byte-for-byte
identical (including the existing mix of ASCII '...' and typographic
ellipsis, worth a separate copy pass), and the notification percentage
keeps its explicit Locale.US formatting rather than picking up the
resource locale, to avoid a behavior change in this PR.
- Drop the Log.d progress line that fired every 500ms during the entire
  write, and the Log.d in SelectDriveScreen's permission-denied path (the
  denial already surfaces to the user via the error dialog). Log.i/Log.e
  diagnostics stay.
- Fix the remaining Kotlin compiler warnings: deprecated
  LinearProgressIndicator overload in EraseScreen, a redundant
  displayName initializer, and an unnecessary non-null assertion.

The build is now warning-free except for EraseScreen's unused 'device'
parameter, which is kept deliberately: the erase flow is currently
simulated and a follow-up will make it operate on the real device.
gradle.properties already declares kotlin.code.style=official; this makes
editors and IDEs enforce the same conventions (4-space indent, UTF-8, LF,
final newline) without depending on per-machine IDE settings.
Runs assembleDebug and Android lint on every PR and on pushes to main,
uploading the lint HTML report as an artifact when the build fails. Both
tasks pass clean on the current tree. No secrets required.
select_channel was never included in the top bar's step mapping, so the
channel screen fell through to the else branch and displayed 'Step 1 of 4'.
Model identification and channel choice are both part of picking the image,
so it joins identify/select_model as step 2.
@jessejamesjohnston

Copy link
Copy Markdown
Author

📋 Contribution series overview

Eight small, sequential PRs that take Book Recovery from AI-generated draft to production-grade Android craft, plus one signature playful feature. Each is independently reviewable; they're stacked, so please take them in order — each PR's diff against main reduces to just its own changes as the earlier ones merge.

Recommended review / merge order:

Testing: the full flow is hardware-validated end to end over USB-OTG on a Pixel 10 Pro XL and a Pixel Tablet (real Cr-48 flash with checksum verification, real erase, adaptive two-pane layout, the game, and the status copy). Pure-Kotlin logic (digests, game engine) is covered by 19 JVM unit tests. assembleDebug + lint are clean on every PR; the CI workflow added in #1 will start running the build+lint on subsequent PRs once #1 merges.

A note on approach: #2 (toolchain) stops at the current stable Material 3; #4 pins the Expressive 1.5.0-alpha only where those APIs are actually used, and flags every experimental usage. Every change that touches the SCSI write path was reviewed with extra care — a bug there can brick a drive mid-write.

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.

1 participant