feat(updates): Play in-app updates with FLEXIBLE flow + staleness gate#73
Open
stozo04 wants to merge 2 commits into
Open
feat(updates): Play in-app updates with FLEXIBLE flow + staleness gate#73stozo04 wants to merge 2 commits into
stozo04 wants to merge 2 commits into
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
SnackbarHostinMainActivity— no new UI surface, no newOpenLoopUiState.Full design + verification plan:
docs/active/in-app-updates/IMPLEMENTATION.md.Behavior
appUpdateInfo, applies the gate, and if Play reports a newer build that's ≥ 3 days stale, starts a FLEXIBLE update flow.onResume: re-surfaces the snackbar if a prior download completed while the app was backgrounded.The staleness threshold is
3(tunable inAppUpdateController.UPDATE_STALENESS_DAYS_THRESHOLD). A debug-onlyBuildConfig.UPDATE_BYPASS_STALENESSflag exists so the snackbar is verifiable via Internal App Sharing (whereclientVersionStalenessDays()returns null). Release builds always honor the real threshold — the build config wires the bypass tofalsein the release type explicitly.DoD status
./gradlew :app:testDebugUnitTest./gradlew :app:lintDebug./gradlew :app:assembleDebugBUILD SUCCESSFUL✅./gradlew :app:bundleReleaseBUILD SUCCESSFUL(R8 + Crashlytics mapping upload clean) ✅Drive-by test fixes (commit 2)
mainhad 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:DeviceMediaHintsTest+SamsungReversePreviewRegressionTestreferenced*_CODEC_SETTLE_MSLongconstants that commit4eed9b6renamed toDuration-typed values. Without this fix the whole test source set fails to compile, blocking every other test.OpenLoopViewModelTestreverse-failure test polled with virtual-timedelay(10)insidewithTimeout(5_000), butensureReversedSegmentruns on realDispatchers.IO. Virtual time burned through the 5s budget in microseconds. Switched to the existingawaitReversePreviewFailedFallback()helper used by 13 other tests in the same file.OpenLoopViewModelTestsaveBoomerangtest asserted bothRAWandBOOMERANGentries remained in storage after save; commit7d202bf("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
AppUpdateGateTest— 8 cases over the gate's input matrixAppUpdateControllerTest— mockk-based, drives listener through everyInstallStatus, asserts onlyDOWNLOADEDfires the host callbackadb installthe debug APK → cold start → no snackbar,UPDATE_NOT_AVAILABLEin logcatUPDATE_NOT_AVAILABLEclientVersionStalenessDays()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.🤖 Generated with Claude Code