Android: sleep stage-timeline rows (iOS #988 port) + HC sleep import fixes - #240
Merged
Conversation
TheBoroer
marked this pull request as ready for review
July 10, 2026 20:49
12 tasks
ryanbr
pushed a commit
that referenced
this pull request
Jul 11, 2026
…rt order (iOS twin of #240) (#241) * fix: prefer richer stagesJSON over recency in sleep-session merge A re-imported session whose newer twin lacked stage data used to clobber the older stage-bearing row. Merge now keeps the candidate with non-empty stagesJSON regardless of recency; ties still resolve by recency. Covered by richness/tie/regression cases in SleepMergeTests. * fix: canonical import outranks computed siblings in sourceCandidates After a device re-add (active strap id != canonical "my-whoop"), the resolver tried the active strap's computed sibling before the canonical import, so fresh computed estimates shadowed richer imported history. Candidate order is now: active strap > canonical import > computed siblings > Apple fallback. Swift twin of the #240 precedence fix; covered by SourceCandidatesOrderTests (single-device path stays byte-identical). * fix: point StrandTests TEST_HOST at renamed "NOOP Staging" product The staging rename set the app's PRODUCT_NAME to "NOOP Staging" but TEST_HOST still referenced NOOP.app, so local `xcodebuild test` could not find the test host. Track the renamed product path (regenerate with xcodegen after pulling).
ryanbr
added a commit
that referenced
this pull request
Jul 11, 2026
…computed stages (parity with #241) (#247) * Android: mergeSleep richness exception — stage-less import yields to computed stages (parity with #241) Re-review of the #241/#246 merge surfaced a missed twin. Android's WhoopRepository.mergeSleep is the direct twin of Swift WhoopStore.SleepMerge.merge (its own comment says "Mirrors WhoopStore.SleepMerge"), but it still had the plain imported-wins-per-day logic — #241 added the richness exception to the Swift side only, so the shared-logic twins diverged. Port the exception: a sparse import (no stage data on ANY of its sessions that day) no longer clobbers a computed day that HAS stage data; days where the import carries stages, or where neither side does, keep the imported-wins rule unchanged. Adds hasStages() (null/""/"[]"/blank = no stages), byte-parity with Swift's hasStages. Not covered by #240: that's a Health-Connect-specific write-gate + heal-purge; it never touches mergeSleep and doesn't protect non-HC sparse imports (Apple/CSV). This is the mergeSleep-level twin. Mirrors Swift SleepMergeTests' 5 richness cases in MergeSleepLocalDayTest; compileFullDebugKotlin + MergeSleepLocalDayTest pass (8/0). * Android: route the Sleep screen through the richness merge too (re-review) Re-review of the first commit found it incomplete: mergeSleep feeds only CSV export + HealthConnectWriter. The Sleep SCREEN builds its night list with its OWN inline imported-wins merge (SleepScreen.kt, two sites: the main loader + the post-nap-edit reload), which had no richness exception — so a stage-less import would still blank stages on the Android sleep screen, while iOS gets richness via repo.sleeps (WhoopStore.SleepMerge). That's the exact user-visible surface the fix is meant to protect. Extract mergeSleepRichness (the imported-wins + #241 richness rule, returned UNSORTED) and route both SleepScreen sites through it, keeping their effectiveStartTs sort (#395) and the #814 union inputs unchanged. mergeSleep is now that helper keyed by wake-day + sorted by startTs, so all three consumers (browse, CSV, screen) share ONE richness implementation. compileFullDebugKotlin + MergeSleepLocalDayTest pass (9/0, +1 mergeSleepRichness case).
github-actions Bot
pushed a commit
to vinayakgupta62/noop-new
that referenced
this pull request
Jul 11, 2026
…rt order (iOS twin of #240) (#241) * fix: prefer richer stagesJSON over recency in sleep-session merge A re-imported session whose newer twin lacked stage data used to clobber the older stage-bearing row. Merge now keeps the candidate with non-empty stagesJSON regardless of recency; ties still resolve by recency. Covered by richness/tie/regression cases in SleepMergeTests. * fix: canonical import outranks computed siblings in sourceCandidates After a device re-add (active strap id != canonical "my-whoop"), the resolver tried the active strap's computed sibling before the canonical import, so fresh computed estimates shadowed richer imported history. Candidate order is now: active strap > canonical import > computed siblings > Apple fallback. Swift twin of the ryanbr/noop#240 precedence fix; covered by SourceCandidatesOrderTests (single-device path stays byte-identical). * fix: point StrandTests TEST_HOST at renamed "NOOP Staging" product The staging rename set the app's PRODUCT_NAME to "NOOP Staging" but TEST_HOST still referenced NOOP.app, so local `xcodebuild test` could not find the test host. Track the renamed product path (regenerate with xcodegen after pulling).
ryanbr
added a commit
that referenced
this pull request
Jul 12, 2026
… not return raw fragments (#240 follow-up) (#316) The Android port of Swift Hypnogram.displaySmoothed added an early return `if (intervals.size <= 2 || minDurationSec <= 0.0) return intervals`. The `|| minDurationSec <= 0.0` short-circuit isn't in Swift, which guards ONLY on count and lets the "raw" (minDuration 0) case fall through to coalesce() — so Swift returns the coalesced timeline, while Android returned the 60-100 un-merged epoch fragments (the "comb" the smoothing exists to remove). Everything else in the port is faithful. Production is unaffected (the only caller passes STAGE_ROW_SMOOTH_SEC = 90.0), so this is a latent port-fidelity fix, not a shipping bug — but it makes the "straight port" claim true and protects a future 0-caller. Also strengthens `zeroFloorReturnsCoalescedInputUnsmoothed`: its old [light, deep, rem] fixture had no adjacent same-stage runs, so coalesce was a no-op and it passed even with the short-circuit. New fixture has a light/light seam and asserts the coalesced result — verified it FAILS against the old short-circuit and passes with the fix. Android-only (Health Connect / SleepScreen); testFullDebugUnitTest green.
This was referenced Jul 12, 2026
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…rt order (iOS twin of ryanbr#240) (ryanbr#241) * fix: prefer richer stagesJSON over recency in sleep-session merge A re-imported session whose newer twin lacked stage data used to clobber the older stage-bearing row. Merge now keeps the candidate with non-empty stagesJSON regardless of recency; ties still resolve by recency. Covered by richness/tie/regression cases in SleepMergeTests. * fix: canonical import outranks computed siblings in sourceCandidates After a device re-add (active strap id != canonical "my-whoop"), the resolver tried the active strap's computed sibling before the canonical import, so fresh computed estimates shadowed richer imported history. Candidate order is now: active strap > canonical import > computed siblings > Apple fallback. Swift twin of the ryanbr#240 precedence fix; covered by SourceCandidatesOrderTests (single-device path stays byte-identical). * fix: point StrandTests TEST_HOST at renamed "NOOP Staging" product The staging rename set the app's PRODUCT_NAME to "NOOP Staging" but TEST_HOST still referenced NOOP.app, so local `xcodebuild test` could not find the test host. Track the renamed product path (regenerate with xcodegen after pulling).
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…computed stages (parity with ryanbr#241) (ryanbr#247) * Android: mergeSleep richness exception — stage-less import yields to computed stages (parity with ryanbr#241) Re-review of the ryanbr#241/ryanbr#246 merge surfaced a missed twin. Android's WhoopRepository.mergeSleep is the direct twin of Swift WhoopStore.SleepMerge.merge (its own comment says "Mirrors WhoopStore.SleepMerge"), but it still had the plain imported-wins-per-day logic — ryanbr#241 added the richness exception to the Swift side only, so the shared-logic twins diverged. Port the exception: a sparse import (no stage data on ANY of its sessions that day) no longer clobbers a computed day that HAS stage data; days where the import carries stages, or where neither side does, keep the imported-wins rule unchanged. Adds hasStages() (null/""/"[]"/blank = no stages), byte-parity with Swift's hasStages. Not covered by ryanbr#240: that's a Health-Connect-specific write-gate + heal-purge; it never touches mergeSleep and doesn't protect non-HC sparse imports (Apple/CSV). This is the mergeSleep-level twin. Mirrors Swift SleepMergeTests' 5 richness cases in MergeSleepLocalDayTest; compileFullDebugKotlin + MergeSleepLocalDayTest pass (8/0). * Android: route the Sleep screen through the richness merge too (re-review) Re-review of the first commit found it incomplete: mergeSleep feeds only CSV export + HealthConnectWriter. The Sleep SCREEN builds its night list with its OWN inline imported-wins merge (SleepScreen.kt, two sites: the main loader + the post-nap-edit reload), which had no richness exception — so a stage-less import would still blank stages on the Android sleep screen, while iOS gets richness via repo.sleeps (WhoopStore.SleepMerge). That's the exact user-visible surface the fix is meant to protect. Extract mergeSleepRichness (the imported-wins + ryanbr#241 richness rule, returned UNSORTED) and route both SleepScreen sites through it, keeping their effectiveStartTs sort (ryanbr#395) and the ryanbr#814 union inputs unchanged. mergeSleep is now that helper keyed by wake-day + sorted by startTs, so all three consumers (browse, CSV, screen) share ONE richness implementation. compileFullDebugKotlin + MergeSleepLocalDayTest pass (9/0, +1 mergeSleepRichness case).
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…mport fixes (ryanbr#240) * docs: design + implementation plan for Android sleep stage-timeline rows (ryanbr#988 port) * feat: sleep stage-interval reconstruction from hero weight pairs (ryanbr#988 port) * docs: plan uses full-flavor gradle task names (app has product flavors) * feat: port Hypnogram.displaySmoothed to Kotlin for the sleep timeline rows * feat: per-stage row span extraction for the sleep timeline rows * feat: Metrics constants for the sleep stage-timeline rows * feat: StageTimeline row composables for the sleep hero (iOS ryanbr#988 port) * feat: wire StageTimeline into the sleep hero card, drop StageLegend * fix: gate HC sleep import on days covered by the active strap * fix: in-bed figure spans the full night group, not the main fragment * fix: purge HC my-whoop sleep rows shadowing strap-computed nights
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
… not return raw fragments (ryanbr#240 follow-up) (ryanbr#316) The Android port of Swift Hypnogram.displaySmoothed added an early return `if (intervals.size <= 2 || minDurationSec <= 0.0) return intervals`. The `|| minDurationSec <= 0.0` short-circuit isn't in Swift, which guards ONLY on count and lets the "raw" (minDuration 0) case fall through to coalesce() — so Swift returns the coalesced timeline, while Android returned the 60-100 un-merged epoch fragments (the "comb" the smoothing exists to remove). Everything else in the port is faithful. Production is unaffected (the only caller passes STAGE_ROW_SMOOTH_SEC = 90.0), so this is a latent port-fidelity fix, not a shipping bug — but it makes the "straight port" claim true and protects a future 0-caller. Also strengthens `zeroFloorReturnsCoalescedInputUnsmoothed`: its old [light, deep, rem] fixture had no adjacent same-stage runs, so coalesce was a no-op and it passed even with the short-circuit. New fixture has a light/light seam and asserts the coalesced result — verified it FAILS against the old short-circuit and passes with the fix. Android-only (Health Connect / SleepScreen); testFullDebugUnitTest green.
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.
What this PR does
Ports the iOS #988 sleep stage-timeline rows to Android: replaces the hero card's
StageLegendwith per-stageStageTimelinerows (span extraction,displaySmoothedport,Metricsconstants, interval reconstruction from hero weight pairs).Also fixes three Health Connect / sleep-hero bugs found while validating against strap data:
SleepView.mergeDay.my-whoopsleep rows that shadow strap-computed nights.Type of change
How it was tested
./gradlew testFullDebugUnitTest— fullcom.noop.ingest.*andcom.noop.ui.*packages pass, including the newHealthConnectCoveredDaysTest(gate + purge behavior).WhoopDao.Checklist
android/(./gradlew testFullDebugUnitTest)StrandDesigntokens — no hardcoded colors, fonts, or spacingdocs/CONTRIBUTING.mdStrand.xcodeproj/) or any secrets/keystoresRelated issues
Android port of iOS #988 (stage timeline rows).