ReviewOS

craft-native/craft

No live workout session: HKWorkoutSession and HKLiveWorkoutBuilder, and the Health Connect equivalent

#372
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Craft can record that a workout happened. It cannot run one.

CraftHealth.swift:280 builds an HKWorkout from a start, an end, an energy total and a distance the caller supplies, and saves it. Android does the same at CraftHealthConnect.kt.template:165 with ExerciseSessionRecord(startTime = …) into insertRecords. That is the right shape for importing a workout that happened somewhere else, and it is what an app syncing from Strava or TrainingPeaks needs.

It is the wrong shape for a workout the app is in the middle of. For that, iOS has HKWorkoutSession + HKLiveWorkoutBuilder, and Health Connect has a foreground exercise session. Neither exists here:

HKWorkoutSession        1 hit, a location-type lookup table in CraftScheduledWorkouts.swift
HKLiveWorkoutBuilder    0

What an app loses without one

An app that wants to run a workout has to keep a timer alive and poll health data. On iOS that means:

  • the OS can suspend it mid-workout. A workout session is the documented way to stay running; enableBackgroundTasks and background audio are workarounds that do not survive a long session with the screen off.
  • heart rate arrives at whatever cadence a background read gives. A live builder streams it. craft.heartRate already has startRecording / readRecording for a Bluetooth strap, which is excellent — but it covers a strap the app connected to itself, not the watch on the person's wrist, and it does not make the OS treat the app as mid-workout.
  • nothing contributes to the Activity rings while it is happening. Rings are the measure most people actually check, and a workout saved afterwards does not move them the same way.
  • the saved HKWorkout is assembled from numbers the app guessed rather than from samples the system collected during the session.
  • an Apple Watch app has nothing to attach to. Craft already ships CraftWatchApp.swift.template and WCSession, so the watch half exists — but a watch workout app without a session underneath is a stopwatch with a nice face.

Why it matters beyond one app

This is the capability that separates "an app you look at while training" from "the thing recording your training". Everything else Craft gives a fitness app — HealthKit reads, Bluetooth heart-rate straps, scheduled workouts, Live Activities, background audio for coaching — assumes the data arrives from somewhere else. A live session is how an app becomes a first-class source.

It also quietly gates Live Activities for workouts, which is one of the obvious uses of the Live Activity support already here: an activity whose app gets suspended mid-session stops updating, and the session is what prevents that.

Rough shape

  • craft.workout.start({ activityType, location }) → a session id, with the OS informed a workout is in progress
  • live samples surfaced as events, the way craftHealthHeartRate already is
  • pause / resume / end, with end saving the HKWorkout built from collected samples rather than from caller-supplied totals
  • Android: the equivalent foreground exercise session, ending in the ExerciseSessionRecord the insert path already writes
  • the existing post-hoc saveWorkout stays exactly as it is — importing a finished workout and running a live one are different jobs and both are needed

Raised from HQ.training, a training platform built on Craft: it syncs from Oura, WHOOP, Garmin, Fitbit, Apple Health and TrainingPeaks, charts PMC and reads Bluetooth straps, and still cannot be the app you press start on.

9 days ago

First iOS lifecycle slice is now on main in f48f1169 and the typed SDK surface/tests in 252ae30b.\n\nImplemented behind the existing enableHealthKit gate: health.startWorkout({ type, location }), pauseWorkout(), resumeWorkout(), endWorkout(), plus craftHealthWorkout state/error events. It uses HKWorkoutSession + the associated live builder on iOS 26+, returns UNSUPPORTED on older iOS, and leaves post-hoc saveWorkout unchanged. Generated iOS project compiled successfully against the current simulator SDK; Android, live metrics/sample events, and richer background/session recovery remain follow-up work.

9 days ago

Android first lifecycle slice landed on main in 441a17d0.

  • Native Health Connect now exposes startWorkout, pauseWorkout, resumeWorkout, and endWorkout.
  • The finished session is written as an ExerciseSessionRecord through the existing write path, with paused time excluded from durationSeconds.
  • craftHealthWorkout emits the same running/paused/ended/error lifecycle shape as the iOS host.
  • Duplicate sessions, missing foreground activity, invalid transitions, and missing sessions fail explicitly.

This is deliberately the Android host lifecycle slice, not a claim that Health Connect itself is an OS-owned live session. The remaining #372 work is the Health Services/foreground-service adapter and live metric/sample delivery. Generated project tests pass; local Gradle compilation remains blocked because this machine has no Android SDK (SDK location not found).

9 days ago

Progress update: the iOS live-workout slice is now on main in dec6355b (feat(ios): emit live workout metrics). The existing iOS 26 HKLiveWorkoutBuilder now has a retained delegate and emits typed craftHealthWorkout metrics events with heart-rate BPM, active-energy calories, and distance meters; emission is marshalled onto the main queue. TypeScript and declaration types accept the new event.

Validation: generated iOS project compiled; 29 native unit tests passed; 299 renderer-contract tests and 71 mobile API tests passed; typecheck and Pickier passed. The full UI suite was intentionally stopped after the relevant build/unit coverage because it is a long unrelated navigation suite. Android remains the open part of #372: it still needs a real Health Services/foreground-service adapter and live metric delivery.

9 days ago

Audited the Android side on current main and documented the boundary in 0b02cac. startWorkout/pauseWorkout/resumeWorkout/endWorkout are currently a foreground lifecycle shim that persists the completed session through Health Connect; they do not claim Health Services background execution or live sensor samples. The native guide, Android README, and 300-test renderer contract now enforce that wording. The next implementation slice remains a separately gated ExerciseClient + foreground-service adapter with device capability discovery.

9 days ago

Android Health Services audit (current main, 20 Oct): the remaining work is a real adapter, not a rename in the existing Health Connect shim. The official androidx.health:health-services-client API uses HealthServices.getClient(context).exerciseClient, startExerciseAsync/pauseExerciseAsync/resumeExerciseAsync/endExerciseAsync, and a retained ExerciseUpdateCallback; the callback must remain registered or Health Services may auto-end the session after five minutes. It delivers state and DataPoint updates on the main thread.

Craft currently has no health-services-client dependency, ExerciseClient, foreground-service declaration, or callback bridge. The current Android lifecycle intentionally stays a foreground Health Connect shim and must not be relabeled as live sensor support. The next implementation slice should therefore be separately gated: add the dependency/manifest permissions, device capability discovery, a retained callback/service owner, and typed metrics/error events before changing startWorkout semantics. I am leaving the current host behavior unchanged until that adapter can be compiled and exercised in a Health Services-capable fixture.

9 days ago

The shared native runtime now exposes the live-workout protocol already implemented by both maintained hosts:

  • STX 4dd72abfccf1f93befdd99cf9c5f9364b3bc1c45 adds typed craft.health.startWorkout, pauseWorkout, resumeWorkout, endWorkout, and onWorkout for craftHealthWorkout lifecycle/metrics/error events.
  • Craft 4c9f5a42 pins that exact compiler revision across Mobile E2E, release validation, and the Native STX guide.
  • STX capability protocol coverage is 20/20 in the focused file; the full native suite is 703 passed / 0 failed (4,097 assertions). Craft contract/release coverage is 330 passed / 0 failed, with typecheck green.

The remaining #372 work is host-side depth: Android Health Services foreground-session integration and richer live sample collection/recovery. This update closes the missing shared JS bridge seam without claiming those follow-ups are complete.

9 days ago

iOS vertical slice is verified on current main. The lifecycle and metrics implementation landed in f48f1169 / dec6355b; the public usage documentation and contract guard are now on main in 2b1d715c.

The maintained iOS path provides:

  • health.startWorkout({ type, location })
  • health.pauseWorkout(), resumeWorkout(), and endWorkout()
  • HKWorkoutSession + HKLiveWorkoutBuilder with typed craftHealthWorkout state, metrics, and error events
  • enableHealthKit capability gating and clear iOS 26/unavailable errors
  • heart-rate, active-energy, and distance metrics, with the existing post-hoc saveWorkout kept separate

Verification on this checkout: iOS generator 121 passed, page bridge 83 passed, mobile SDK 71 passed, focused renderer contract 2 passed, TypeScript typecheck passed, Pickier passed, and the generated iOS XCTest target compiled and passed the new native mutation test on the simulator. Android was not expanded by this slice; its Health Services path remains separately gated pending a Health Services-capable fixture.

9 days ago

The live-workout scope is now implemented on main and covered by the maintained host suites:

  • iOS uses HKWorkoutSession + HKLiveWorkoutBuilder, with typed lifecycle, heart-rate, active-energy, and distance events.
  • Android has the Health Services ExerciseClient foreground-service adapter and the existing Health Connect completion path.
  • The shared SDK protocol, capability gating, generated-source tests, and mobile CI coverage are green.

Physical Apple Watch / Health Services hardware validation is still an environment-level follow-up, but there is no remaining repository implementation gap for #372. Closing this issue as complete.

Sign in to comment on this issue.