Skip to content

feat(updates): Play in-app updates with FLEXIBLE flow + staleness gate#73

Open
stozo04 wants to merge 2 commits into
mainfrom
feature/in-app-updates
Open

feat(updates): Play in-app updates with FLEXIBLE flow + staleness gate#73
stozo04 wants to merge 2 commits into
mainfrom
feature/in-app-updates

Conversation

@stozo04

@stozo04 stozo04 commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a gentle "update ready — restart to install" nudge on app cold start using Google's official Play In-App Updates 2.1.0 library (FLEXIBLE flow). Reuses the existing styled SnackbarHost in MainActivity — no new UI surface, no new OpenLoopUiState.

Full design + verification plan: docs/active/in-app-updates/IMPLEMENTATION.md.

Behavior

  • On cold start: controller calls appUpdateInfo, applies the gate, and if Play reports a newer build that's ≥ 3 days stale, starts a FLEXIBLE update flow.
  • On onResume: re-surfaces the snackbar if a prior download completed while the app was backgrounded.
  • On download complete: snackbar fires → user taps Restart → Play installs and relaunches.
  • Off-Play (sideloaded debug installs): silent no-op. Confirmed locally — no UI, no exceptions.

The staleness threshold is 3 (tunable in AppUpdateController.UPDATE_STALENESS_DAYS_THRESHOLD). A debug-only BuildConfig.UPDATE_BYPASS_STALENESS flag exists so the snackbar is verifiable via Internal App Sharing (where clientVersionStalenessDays() returns null). Release builds always honor the real threshold — the build config wires the bypass to false in the release type explicitly.

DoD status

Gate Status
./gradlew :app:testDebugUnitTest 206/206 ✅
./gradlew :app:lintDebug 0 errors, 9 baseline-allowed warnings (none in new code) ✅
./gradlew :app:assembleDebug BUILD SUCCESSFUL
./gradlew :app:bundleRelease BUILD SUCCESSFUL (R8 + Crashlytics mapping upload clean) ✅
Emulator launch + screenshot ⏸️ deferred — local emulator was offline; happy to attach once one is up

Drive-by test fixes (commit 2)

main had four test failures blocking the DoD test gate, all from earlier feature commits that didn't update their tests. Folded inline so this PR can ship clean:

  1. DeviceMediaHintsTest + SamsungReversePreviewRegressionTest referenced *_CODEC_SETTLE_MS Long constants that commit 4eed9b6 renamed to Duration-typed values. Without this fix the whole test source set fails to compile, blocking every other test.
  2. OpenLoopViewModelTest reverse-failure test polled with virtual-time delay(10) inside withTimeout(5_000), but ensureReversedSegment runs on real Dispatchers.IO. Virtual time burned through the 5s budget in microseconds. Switched to the existing awaitReversePreviewFailedFallback() helper used by 13 other tests in the same file.
  3. OpenLoopViewModelTest saveBoomerang test asserted both RAW and BOOMERANG entries remained in storage after save; commit 7d202bf ("Delete source video") deletes the raw after rendering. Updated assertions accordingly.

These are scope creep from "in-app updates" — flagged here so they're visible during review.

Test plan

  • Unit (T1): AppUpdateGateTest — 8 cases over the gate's input matrix
  • Wiring (T2): AppUpdateControllerTest — mockk-based, drives listener through every InstallStatus, asserts only DOWNLOADED fires the host callback
  • Off-Play sideload check: adb install the debug APK → cold start → no snackbar, UPDATE_NOT_AVAILABLE in logcat
  • T3 — Internal App Sharing (manual, before next release):
    • Upload current versionCode (22) AAB to IAS; install via link; cold-start → expect UPDATE_NOT_AVAILABLE
    • Bump to versionCode 23, upload to IAS, click the new link on the phone (do NOT install from the Play Store page that appears), open installed v22 from launcher
    • Expect: Play's confirmation dialog → background download → snackbar "Update ready — restart to install" with Restart action → tap Restart → app relaunches as v23
    • Repeat with a release-signed AAB (bypass off) to confirm the staleness threshold genuinely gates the prompt
  • T4 — Internal testing track (post-merge): ship to internal track, wait 3+ days, install v_current on a fresh device, cold-start, confirm clientVersionStalenessDays() populates and the snackbar fires once the threshold is met. This is a known limitation: IAS never populates that field, so it cannot be verified pre-merge.
  • Emulator screenshot: attach once an emulator is up (deferred from local DoD).

🤖 Generated with Claude Code

stozo04 and others added 2 commits June 15, 2026 15:06
… gate

Adds a gentle "update ready — restart to install" nudge on cold start using
Google's Play In-App Updates 2.1.0 library. Reuses the existing SnackbarHost
in MainActivity for the prompt — no new UI surface, no new OpenLoopUiState.

Design (full spec in docs/active/in-app-updates/IMPLEMENTATION.md):

- AppUpdateController wraps the Play AppUpdateManager. Activity owns the
  ActivityResultLauncher (only it can register one before STARTED) and
  hands it in via attach(); controller owns the listener lifecycle.
- Pure shouldPromptForFlexibleUpdate() function encodes the gate:
  UPDATE_AVAILABLE AND FLEXIBLE allowed AND (staleness >= 3 days OR bypass on).
- Debug-only BuildConfig.UPDATE_BYPASS_STALENESS flag (true/debug, false/release)
  exists so the snackbar can be visually verified via Internal App Sharing,
  where clientVersionStalenessDays() returns null. Release AABs cannot ship
  with the bypass on.
- Off-Play behavior (sideloaded debug builds) is a silent no-op — Play returns
  UPDATE_NOT_AVAILABLE, controller logs at debug, no UI is shown.

Verification (DoD per docs/DEFINITION_OF_DONE.md):

- T1: JVM unit test on the pure gate (8 cases over the availability x staleness
  x flexible-allowed x bypass matrix).
- T2: mockk-based wiring test on AppUpdateController (listener registration,
  DOWNLOADED routing, completeUpdate delegation, attach idempotency, detach
  cleanup). Mockk chosen over FakeAppUpdateManager because the latter
  requires a Context — see PRD §Verification for the trade-off.
- T3 (manual, Internal App Sharing) and T4 (internal testing track) documented
  in the PRD; cannot be run pre-merge.

Unit tests 206/206 green. lintDebug 0 errors. assembleDebug + bundleRelease
both BUILD SUCCESSFUL. Emulator screenshot deferred — see PR description.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four test failures on main were blocking unit test compilation/runs. None
were caused by this branch — all were left behind when prior feature commits
changed semantics. Folded inline so this PR can clear the DoD test gate.

1. DeviceMediaHintsTest + SamsungReversePreviewRegressionTest referenced
   SAMSUNG_POST_TRANSFORM_CODEC_SETTLE_MS / PRE_REVERSE_CODEC_SETTLE_MS,
   which commit 4eed9b6 renamed to Duration-typed constants without the
   _MS suffix. Mechanical update: import kotlin.time.Duration.Companion.milliseconds,
   compare Duration values directly. Without this the whole test source set
   failed to compile, blocking every other test.

2. OpenLoopViewModelTest `reverse generation failure flags reverseFailed`
   polled with virtual-time delay(10) inside withTimeout(5_000), but
   ensureReversedSegment runs on real Dispatchers.IO. Virtual time burned
   through the 5s budget in microseconds before the real IO thread could
   dispatch its continuation. Switched to awaitReversePreviewFailedFallback() —
   the codebase's existing real-time spin helper, used by 13 other tests
   in the same file.

3. OpenLoopViewModelTest `saveBoomerang ... emitting Share` asserted both
   RAW and BOOMERANG entries remained in storage after save. Commit 7d202bf
   ("Delete source video") changed the render flow to delete the source raw
   after rendering the boomerang. Updated assertions to match: exactly one
   BOOMERANG, zero RAW, boomerang's sourceRawId still references its source.

Full suite: 206/206 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
stozo04 added a commit that referenced this pull request Jun 17, 2026
New production release off main. Includes the merged privacy-policy and
data-safety corrections (#74, #75). Does NOT include the in-app updates
feature (PR #73) — that flow is still under test and remains on its branch.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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