First-class health: HealthKit, Health Connect, BLE sensors, workouts and simulator - #5475
First-class health: HealthKit, Health Connect, BLE sensors, workouts and simulator#5475shai-almog wants to merge 96 commits into
Conversation
First slice of the cross-platform health effort: the portable core that every port will implement against, plus the Bluetooth sensor layer, which needs no port code at all. com.codename1.health follows the Bluetooth API's shape -- a never-null facade with a no-op base class, reached via Display.getHealth(), with CodenameOneImplementation.getHealth() defaulting to null so no existing port breaks. What is here: - Vocabulary: HealthDataType (an interned value object rather than an enum, so constants can carry units without an initialisation-order hazard and forId() can stay total across versions), HealthUnit with affine conversion, and the HealthSample hierarchy covering quantity, category, series and session data. - HealthStore: public methods are final and do validation, unit normalisation, paging, bucket boundaries and the subscription registry; ports implement only the protected do* SPI. Anchors are persisted by the framework and stored only after a listener returns, so a crash costs one redelivered batch rather than the data. - com.codename1.health.sensors: built entirely on com.codename1.bluetooth.le, so it works on every port with BLE -- including desktop and JavaScript, where no health store exists. Byte-exact parsers for the standard SIG profiles. - com.codename1.health.workout: state machine, elapsed clock and statistics rollup in shared code, with RecordedWorkoutSession as the path for platforms with no live session -- which is what Google documents for Health Connect on phones, not a fallback. Two platform truths the API refuses to paper over: HealthKit cannot report read authorisation, so there is no hasReadPermission and the docs say to show "no data available" rather than "you denied access"; and Health Connect never wakes an app, so isPushDelivery() is a queryable fact. Aggregate results return null rather than a synthetic zero, since a day with no data and a day with no steps are different facts. Prerequisite refactors: SimulatorHookLoader gains a backward-compatible groups= form (one resource per classpath entry cannot otherwise host a second namespace), and the sim schedulers move to impl.javase.simulator so the health simulator can share them. Tests: 40 new, covering the fallback contract, unit conversion including the glucose factor, and the 0x2A37 traps -- unsigned reads, the contact-supported/detected pair, kilojoule energy, and variable-count RR intervals. Full suites green: 4213 core, 176 javase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Desktop, JavaScript and the simulator have no HealthKit or Health Connect, but that does not have to mean a no-op. LocalHealthStore is a real store -- reads, writes, deletes and aggregates all work -- so aggregation logic, chart code and unit handling can be developed and unit-tested on a laptop rather than only on a device. It reports HealthAvailability.LOCAL_ONLY rather than pretending to be a platform store, because the difference matters: nothing else writes into it, so an app whose purpose is reading what a watch or another app recorded should say the feature needs a phone instead of showing an empty chart. Wired into the JavaSE, Windows, Linux and JavaScript ports. Workouts and the Bluetooth sensor layer already worked on all four and are unchanged. Being the only store with no platform behind it, this is also where the shared aggregation rules get exercised. The tests pin the two that are easiest to get wrong and hardest to notice: an empty bucket reports null rather than zero, and calendar-day buckets follow the calendar across the 2026-03-08 spring-forward transition, where a Los Angeles day is 23 hours and a fixed 86_400_000 bucket would quietly drift. 15 new tests; full core suite green at 4228. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit resolved a persisted background-listener class name with Class.forName, following GeofenceManager. That was the wrong precedent to copy: LocationManager needs it because it works around obfuscation with keep rules, and reusing the shape here inherited the fragility without the reason. Reflection is specifically bad on the platform this feature exists for. A class referenced only by a string is invisible to the iOS and JavaScript translators' dead-code elimination, so it can be stripped from the very build that needs it, and obfuscation renames it so a name persisted by an earlier version stops resolving. The old docs had to tell developers to "keep the class reachable" -- a warning that was really an admission the mechanism was unsound. The build server already scans bytecode for interface implementations and already injects registration code at startup, which is what it is for. So HealthBackgroundListenerFactory is a build-generated binding: the builder finds HealthBackgroundListener implementations, emits a factory that constructs each with a direct `new`, and registers it via HealthStore.setBackgroundListenerFactory. A real reference, which both dead-code elimination and obfuscation follow correctly. There is deliberately no reflective fallback. Where no bindings exist -- the simulator, unit tests -- delivery is skipped and the anchor is not advanced, so the changes are redelivered later rather than lost. Generating the factory is now part of the build-tooling work. 5 new tests. The two positive-delivery cases initially passed alone and failed in the full suite, which was a real ordering flake rather than a harness quirk; they now wait on a CountDownLatch through the established UITestBase.waitFor helper. Full suite run three times: 4233, no failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nutrition closes a gap: HealthDataType.NUTRITION existed and its docs referenced com.codename1.health.nutrition, which had not been written. A NutritionSample carries a sparse set of Nutrient amounts rather than the forty nullable fields both platforms model, and a nutrient that was never measured reads back as null rather than zero -- the same distinction aggregate buckets make. HealthManifestFragments emits the Health Connect manifest pieces: per-type permissions, the provider <queries> entry, and the permissions-rationale activity, which is not optional -- Health Connect silently declines to show its consent dialog to an app that lacks one. Permissions come from build hints because the type an app uses is a field reference and the class scanner's visitFieldInsn is an empty override, so the set genuinely cannot be inferred; that also matches Play policy on declaring exactly what you use. Duplicate suppression is quote-delimited, since READ_HEART_RATE is a prefix of READ_HEART_RATE_VARIABILITY, READ_EXERCISE of READ_EXERCISE_ROUTE, and READ_HEALTH_DATA of both its longer forms. HealthListenerBindings generates the background-listener factory promised when the reflection was removed: a direct `new` per listener, plus -keepnames so the persisted name key survives obfuscation. One hazard found while wiring AiDependencyTable and now pinned by tests: entries match with startsWith, so com/codename1/health/ also matches the sensors subpackage. Putting the androidx.health dependency there would give an app that only reads a heart-rate strap a Health Connect dependency and a Play health-permissions review it has no business needing. The dependency moves to AndroidGradleBuilder, gated on health usage outside sensors; the table keeps only what is safe for both cases. 44 builder tests, including a golden token list so a HealthDataType addition that is not mirrored into the builder table fails CI. Full plugin suite green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Detection is deliberately two-level. usesHealth means the app touched com.codename1.health at all; usesHealthStore means it touched something outside the sensors subpackage. Only the latter gates HealthKit, its entitlement, the privacy-string requirement, the Health Connect dependency and the per-type permissions -- so an app that only streams a heart-rate strap does ordinary BLE and acquires no health-data review on either store. iOS fails the build, loudly, when a health app declares no NSHealthShareUsageDescription or NSHealthUpdateUsageDescription, and again when it writes health data without the update string. That is the opposite of the camera and bluetooth entries, which default their copy, and it is deliberate: Apple reviews health purpose strings against what the app actually does, so a placeholder is what gets an app rejected rather than what keeps the build working. It also emits the HealthKit entitlement, which has no Bluetooth precedent since CoreBluetooth is not entitlement-gated. Android validates what it cannot infer: the data types, since a type is a field reference the scanner cannot see, and the privacy-policy URL, since Play requires one and the rationale screen must link to it. It raises minSdk to 26 and refuses targetSdk below 30, where <queries> is not emitted and the provider is invisible. Extending Executor.ClassScanner with implementsInterface is what makes the generated listener bindings possible: the visitor already had the implementing class name and was discarding it, reporting only the interface. The other three builders get a no-op, since they generate no callbacks. The manifest fragments are staged in fields rather than written where they are computed, because xQueries is reset and the <application> body is assembled well after the permission block runs. Full plugin suite green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SimulatedHealthStore layers scripted permissions and one-shot fault injection over the real local store, and its default is deliberately the awkward one. HealthKit does not disclose read authorization: a denied read returns an empty result, indistinguishable from having no data, so an app cannot infer what a user is hiding. A developer testing against a permissive store meets this for the first time in review or in production. So IOS_OPAQUE is the default policy, DENIED_SILENT reproduces the trap exactly -- authorization resolves true, the status reads UNKNOWN, queries come back empty with no error -- and GRANTED_BUT_NO_DATA produces observationally identical results, which is precisely why the API offers no hasReadPermission. Switching to ANDROID_EXPLICIT makes the same script fail loudly, as Health Connect does. Synthetic data rather than recorded fixtures. The Bluetooth simulator replays scrubbed traces, and that does not transfer: for BLE the sensitive parts are identifiers, so scrubbing leaves a trace tests can still assert on, whereas for health the sensitive part IS the asserted value. Scrubbing it either destroys the signal or commits quasi-identifying biometric data to a public repository's history forever. There is also no desktop HealthKit to record from. Everything is seeded, so exact-total assertions stay stable. The health menu is why SimulatorHookLoader gained the groups= form: a classpath entry carries one copy of simulator-hooks.properties, so a second namespace could not be a second file. A test parses the real shipped file rather than a fixture, which is what proves the extension actually works. 20 new tests; full javase suite green at 196. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Health chapter in the developer guide, included after Bluetooth, with eight snippets that live in docs/demos and compile against the real API rather than being inline prose. The ordering is deliberate: permissions come before any read example, because the read-authorization asymmetry is the thing that will otherwise be discovered in App Review. The chapter says plainly that requestAuthorization resolving true means the user was asked rather than that anything was granted, that getReadAuthorizationStatus is UNKNOWN on iOS by design, and that a UI must therefore say "no data available" rather than accusing the user of denying access. The privacy section covers what the technical permissions do not: Apple's prohibition on advertising and iCloud storage and its specificity requirement for purpose strings, Google's health apps declaration form and the mandatory privacy-policy activity, and what Codename One itself does -- never uploads health data, never logs sample values, and fails the build rather than inventing a purpose string. Troubleshooting leads with "my query returns empty even though I granted permission", which is the support question this API will generate most. Verified locally rather than assumed: the snippets compile under JDK 17 via docs/demos process-classes, validate-guide-snippets passes across 636 include-backed blocks, and LanguageTool over the rendered guide reports zero matches. It caught two British spellings of mine, which are fixed in the prose rather than added to the accept list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Layered exactly like com.codename1.car, and for the same reason. androidx.health.connect is an AndroidX library whose API surface is Kotlin suspend functions, while the Codename One Android port compiles against a fixed old android.jar with no AndroidX, no Kotlin and no coroutines. The port cannot reference it -- and does not need to, because the port ships as source that the app's own Gradle compiles at a modern compileSdk, Kotlin sources included. So HealthConnectDelegate is a pure-Java seam speaking only Strings and primitives, CN1HealthConnectBridge.kt implements it and is copied into the generated project only for apps that use a health store, and AndroidHealthSupport publishes it at startup. Verified: the port still compiles under -Pcompile-android against the old android.jar, so nothing leaked across the boundary. Two deliberate choices worth recording. Aggregation is not delegated to Health Connect. The shared code already computes daylight-saving-correct bucket boundaries and a duration-weighted average, and having a second implementation on one platform is how the two come to disagree about a user's weekly total. The permissions-rationale activity lives in com.codename1.health rather than the impl package, because it is named from the manifest and needs a stable declared name -- the same reason the background-location activity sits outside impl. It is not optional: Health Connect silently declines to show its consent dialog to an app without one, so the failure mode is a permission request that never appears rather than one that is refused. HealthWire carries the line-delimited sample format shared with the iOS bridge. A year of heart rate is hundreds of thousands of samples and parsing that as JSON exhausts the ParparVM heap; unknown type ids and unit symbols skip the line rather than failing the page, so an older app reading a newer store loses the unfamiliar rows and keeps the rest. Core suite green at 4233. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CN1Health.m is gated on CN1_INCLUDE_HEALTH, which IPhoneBuilder uncomments only when the scanner saw health classes outside the sensors subpackage -- so a heart-rate-strap app ships without HealthKit symbols, without the entitlement, and without Apple's health-data review. The #else branch was written first and provides a no-op trampoline for every declared native, so a health-free app still links; the same branch is what compiles the feature out on tvOS and Mac Catalyst, where HealthKit does not exist. Two behaviours the native layer reports honestly rather than smoothing over. hkShareAuthorizationStatus covers write types only, and there is deliberately no read equivalent: HealthKit does not expose read authorization, so a read query is the only evidence there is. HKErrorDatabaseInaccessible maps to its own retryable error rather than being collapsed into "no data" -- the store is encrypted at rest and unreadable before first unlock, which is exactly when a background observer fires. IOSImplementation.getHealth() throws before constructing anything when no HealthKit privacy string is declared, following getLocationManager(). A missing usage description is a developer bug that gets an app rejected, so it must not be swallowable into an AsyncResource error nobody reads. Two gaps in the surrounding slices, now closed: HealthKit is weak-linked for tvOS, which would otherwise fail to link against the iOS slice's reference, and WatchNativeBuilder emits the health privacy strings and the workout-processing background mode. The watch Info.plist previously carried no privacy strings at all, which would have failed at runtime on the richest HealthKit target there is. Aggregation is deliberately left to shared code on both ports, so the bucket arithmetic has one implementation rather than two that can drift. Full plugin suite green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tier 1 gains the wire-format, sensor-parser, workout and nutrition suites. The parser cases each target a trap that produces plausible wrong numbers rather than an obvious failure: cycling power read unsigned turns a back-pedal into 65531 W, the weight scale uses a different resolution per unit system rather than a conversion applied afterwards, temperature uses the 32-bit IEEE-11073 FLOAT while blood pressure and glucose use the 16-bit SFLOAT, and a cuff or thermometer that fails sends a reserved value that surfaces as 2047 mmHg if it is let through. The wire-format cases pin the behaviour that matters for longevity: an unknown record type or unit skips its line rather than failing the page, so an older app reading a store a newer OS has extended loses the unfamiliar rows and keeps the rest. HealthConformanceTest runs on device on every port. It asserts only what must hold everywhere -- the facade and sub-facades are non-null, capability queries answer rather than throw, operations return a resource rather than hanging -- plus exact results for the pure functions, since unit conversion, the flags-byte parsing and the bucket boundaries have to agree across ParparVM, ART, the browser and the desktop JVM. Unsigned reads are called out specifically as the likeliest place for a translator to diverge. One defect found while reviewing: readSamples paged by mutating the caller's SampleQuery, leaving the last page token stuck on it, so a query object reused for a second read would silently resume mid-way through the first. It now pages over a copy. Suites: 4291 core, 196 javase, 322 plugin. Repo gates green -- since-tags, package-info, markdown javadoc, guide snippets, paragraph capitalization. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An actual iOS build found three ways the native bridge was wrong. It had never been compiled -- the ParparVM headers only exist after translation -- so this is exactly the class of defect the missing CI leg was hiding. Native definitions omitted the instance parameter. ParparVM emits `(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1ThisObject, ...)` for an instance native, and every hk* function declared only the thread state, so each argument was read one slot early. Arrays used an XMLVM type that does not exist in this VM. The String[] parameters of hkRequestAuthorization are JAVA_ARRAY, walked via a->length and (JAVA_ARRAY_OBJECT *)a->data, as cn1btUuidArray does. Callbacks used a macro that does not exist. Calling into Java from a GCD block attaches the thread with getThreadLocalData() rather than CN1_THREAD_STATE_MULTI_THREADED. Verified end to end rather than by inspection: hellocodenameone now references com.codename1.health from main sources, so the scanner fires and the builder does real work. The generated project confirms CN1_INCLUDE_HEALTH, INCLUDE_HEALTHSHARE_USAGE and INCLUDE_HEALTHUPDATE_USAGE all flipped and HealthKit.framework linked, and xcodebuild compiles CN1Health.m for arm64 against the iOS 26.2 SDK -- BUILD SUCCEEDED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…device builds A real Gradle build caught the generator emitting `new Outer$Inner()`. The dollar form is the binary name Class.getName() returns, which is correct as the lookup key, but it is not valid Java source for a constructor call -- and a listener nested inside the class that subscribes is the common case, so this would have failed for most apps that use background delivery. The key stays the binary name; the constructor call now uses the dotted source name. Regression test added. Both native layers are now verified by real builds rather than by inspection, which is what the earlier commits could not claim: iOS -- ParparVM translation plus xcodebuild for arm64 against the iOS 26.2 SDK. The generated project confirms CN1_INCLUDE_HEALTH and both INCLUDE_HEALTH*_USAGE defines flipped and HealthKit.framework linked. Android -- Gradle assembleDebug at compileSdk 36. The shipped dex contains CN1HealthConnectBridge, the generated CN1HealthListenerBindings, AndroidHealthSupport and androidx.health.connect.HealthConnectClient, so the Kotlin bridge genuinely compiled against Health Connect rather than being skipped. The merged manifest carries exactly the declared permission set -- READ_STEPS, READ_HEART_RATE, WRITE_STEPS -- plus the rationale activity, the API-34 ViewPermissionUsageActivity alias and the provider <queries> entry. hellocodenameone now references com.codename1.health from main sources and declares the required build hints, so these paths stay exercised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six real findings across two modules, all in code this branch added. AndroidHealthSupport tripped EI_EXPOSE_STATIC_REP2 for storing a mutable object in a static field. That is the whole point of the registry, and AndroidCarSupport carries an exclusion for exactly the same pattern, so this mirrors it rather than contorting the design. Verified by removing the exclusion and watching the finding come back. The other five are genuine and are fixed rather than excluded: - Two anonymous callbacks in HealthStore and one in HealthSensors held a synthetic reference to their enclosing object. They are now built in static factory methods, matching how Bluetooth dispatches its EDT runnables and how the rest of this branch already did it. - validateForWrite iterated keySet and then looked each value up again, a second hash probe per entry. Now entrySet. - readPageInto evaluated the same limit comparison twice; hoisted, which also makes the trimming comment easier to follow. Worth recording for the next person: `spotbugs:check` reads a cached report, and core-unittests compiles the core sources itself, so a fix appears to have no effect until the module is recompiled. Re-running with test-compile is what actually re-analyses. Also caught while writing the exclusion: an em-dash written as `--` inside an XML comment is illegal and made the whole filter file unreadable, which silently dropped every existing exclusion rather than failing loudly. Suites still green: 4304 core, 196 javase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
CI's developer-guide job runs Vale in addition to LanguageTool, which I had not run locally -- LanguageTool was clean, so I wrongly concluded the prose gates were satisfied. Vale reported 55 issues, all of them in the new chapter; every other chapter was already clean, so this was entirely mine. Most were the Microsoft style rules the guide follows: contractions, a few adverbs carrying no weight, punctuation inside quotes, and two phrases in first person. Fixed in the prose rather than allowlisted, since the rest of the guide observes the same rules. One case was not a prose problem. "Grant Write, Silently Deny Read" is the simulator's menu label, so the docs have to match the UI, and a vale-skip comment does not work inside a table row. The label is ours to choose, so it becomes "Grant Write, Deny Read Without Error" -- which names the behaviour that matters (the read fails with no error) rather than the manner, and is clearer for it. Renamed in the hooks properties, the test that pins it and the chapter together. Vale now reports zero across all 77 guide files, LanguageTool zero over the rendered guide, snippets and paragraph capitalization pass, javase suite green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cloudflare Preview
|
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
|
Companion BuildDaemon PR: https://github.com/codenameone/BuildDaemon/pull/162 The daemon carries its own copies of |
The Ant build compiles core with -bootclasspath Ports/CLDC11/dist/CLDC11.jar, so only the CLDC profile's java.* is visible. Maven compiles against the JDK's rt.jar with -source 1.5 and never saw this, which is why every local check passed while the javase-simulator-tests job failed: the code would not have compiled for a device at all. Three APIs I used are not in that profile: - Calendar.getTimeInMillis() and setTimeInMillis(long) are protected there, so the instant has to move through getTime() / setTime(new Date(millis)). - Calendar.clear() and the six-argument set(y, m, d, h, mi, s) do not exist, so GattDateTime sets each field individually. - Calendar.setFirstDayOfWeek does not exist. The week bucketing already walked back to the configured first day by hand, so the call was redundant as well as unavailable. - java.lang.Math has no pow(). MathUtil.pow is the framework's own and is already what the rest of core uses, so the IEEE-11073 decoders use it. Worth knowing for anything else added to core: a green Maven build is not evidence that core compiles for a device. `ant core` is, and it needs `ant -f Ports/CLDC11 jar` first or it checks against a stale CLDC jar and reports unrelated failures in the calendar package. Verified: ant core BUILD SUCCESSFUL against a freshly built CLDC11.jar, and 4304 core tests still green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 12 screenshots: 12 matched. |
|
Compared 148 screenshots: 148 matched. Benchmark Results
Detailed Performance Metrics
|
|
Compared 181 screenshots: 181 matched. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b7cc3488aa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…version - The scan-failure forwarding added in the previous commit called the listener directly. That callback runs wherever the scan failed -- the caller's thread for a synchronous start failure, the platform's for a later onScanFailed -- while SensorDiscoveryListener promises the EDT, so a listener touching the UI there would have raced. It goes through the same hop the unsupported-BLE path already used. - A quantity converted to the preferred unit on write keeps its source. The read-side conversion already copied it; the write path did not, so the same measurement was filterable or not depending on which unit the caller happened to use -- a weight in kilograms kept its source, one in pounds came back excluded from every addSource() query and every source-filtered aggregate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ac446435e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rrections - AndroidHealthStore reports no native aggregate metrics. It listed COUNT, DURATION, TOTAL and the discrete summaries, but doAggregate is deliberately not overridden, so every one of them is derived by the shared base class from raw samples read with an effectively unbounded limit -- a caller deciding from this that a year-wide aggregate would stay platform-side got a large read instead. The iOS store already answered empty for the same reason, so this was a parity gap. - WorkoutSession filters nulls out of addSamples. rollUp ignored one but the list was stored whole and doEnd dereferences what it stored, so a single null threw out of end() after the session had already moved to STOPPED -- leaving the caller with neither a result nor a session it could retry. - BLOOD_PRESSURE no longer claims the iOS port assembles HealthKit correlations. There is no blood_pressure entry in the iOS map, so the type is refused before any correlation handling; the guide already said local and simulator only and the type documentation now agrees. - WorkoutSession no longer claims live OS-owned sessions on watchOS, iOS 26 and Wear OS. isLive() is false everywhere in this release and RecordedWorkoutSession is the only implementation, so an app could otherwise assume the OS keeps it alive when backgrounding ends the process and takes the workout with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by checking a claim I had already posted on the review thread rather than before posting it. The workout class documentation was not the last place describing live sessions as though they run here: - SensorSessionOptions explained the write-through default by a workout the OS is recording, without saying that in this release such a workout is one the user started elsewhere rather than one of yours. - WorkoutSessionListener described state transitions the app did not request; every transition is requested here. - WorkoutLocationType said it selects GPS use on watchOS; nothing drives a live session, so it is recorded and read back. - WorkoutSession's rollUp contrasted a recorded session with a live one as though both existed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 387057f733
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- LocalHealthStore holds a mutation lock across the mutation, the encoding, the storage write, the rollback and the lastGood update. The sample-list lock was released between changing the list and encoding it, so two concurrent mutations could each snapshot and then write in the opposite order: the older snapshot landed last, both callers were told they had succeeded, and a record deleted before the restart was back after it. Taken before the sample lock, never the other way round; reads take the sample lock alone and are not blocked by a save. - The iOS drain and the developer guide say plainly that a timestamp cursor cannot see backdated data. It finds samples by when they were measured, so a watch that syncs late or a reading the user backdates lands behind the cursor and no later drain returns it. HKAnchoredObjectQuery is the only fix -- HealthKit exposes no insertion time through HKSampleQuery -- and it is not written yet, so an app that must be complete rather than current is told to re-read its range periodically. - One LanguageTool finding in the guide, introduced by the previous fix to the same sentence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 745167fb5c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…tion Every save failure was reported as a denial, so a write attempted before the device's first unlock -- exactly when a background drain runs -- sent the app to the permission screen, or made it discard buffered sensor and workout data, for a condition that clears itself. The authorization request had the same hardcoded mapping. The two query paths already distinguished HKErrorDatabaseInaccessible, so the mapping is now one helper used by all four callers rather than three copies and an omission. Authorization errors are still reported as denials, which they are; anything unrecognised stays UNKNOWN rather than being guessed at. No job in this repo compiles CN1Health.m -- the clang syntax check covers only the ARC natives -- so the helper was syntax-checked on its own against the real HealthKit headers for iphoneos arm64. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e427ee6d24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- SensorSession re-arms its store flush only while the session is nonterminal. The guard tested for STOPPED alone, but the reconnect ladder retires a session as FAILED -- so exactly the session nobody holds a handle to any more, already gone from the manager's registry, went on firing writes and errors on a timer nothing could cancel. - SampleQuery.setFlattenSeries no longer says iOS returns one-point series when flattening is off. HealthKit stores each measurement separately and the port returns ordinary QuantitySample objects either way, so code that turned flattening off and cast to SeriesSample failed there. - BloodPressureSample says local and simulator only, matching the fix made to HealthDataType two rounds ago. Arriving through the concrete class, a developer could still design around an iOS correlation codec that is not written. The retry test took three attempts. The first two passed against the unfixed build: no write ever happened, because HealthStore.isTypeSupported defaults to false and shared validation rejected every batch before doWrite. Asserting the precondition -- that a write was attempted at all -- is what exposed it. Reverting the guard now shows the retry loop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The developer guide was corrected two rounds ago -- metadata is not written to HealthKit or Health Connect, so an identifier kept there is gone the moment a phone reads the sample back. HealthSample.getMetadata() still gave the old advice, which is the place a developer reads while doing it, and two comments in HealthStore justified copying metadata by citing advice that no longer exists. Found by running the grep I had just claimed on a review thread, which is the right order to do those two things in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 73e588f062
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… entitlement - IPhoneBuilder collects health background listeners, writes the generated bindings into the stub source tree so javac and ParparVM pick them up, and installs the factory from the app stub. The scanner callback was empty on the reasoning that only Health Connect delivery survives process death -- but a cold launch needs the bindings too: a restored subscription carries only the listener's class name, and without a factory the runtime cannot turn that back into an instance, so even a manual drainChanges() after a restart delivered nothing. The runtime already logged "on device the build server generates them", which was true of Android alone. - The HealthKit background-delivery entitlement is no longer inferred from subscribe() or drainChanges(). Neither registers an HKObserverQuery, so the entitlement bought nothing while demanding a provisioning-profile capability a polling-only app has no reason to hold -- and getting that wrong fails codesign with an opaque message. The explicit hint still turns it on. The now-dead flag is gone. Both changes are mirrored in the BuildDaemon twin, and two parity tests fail against the previous state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e85e1fd6a9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The iOS binding generation I added an hour ago bound whatever declared the interface. That is wrong in three ways, and the Android path in the daemon already knew all three: an abstract base or an app's own intermediate interface can be the declarer, so the generated source said `new Base()` and failed javac while the usable subclass was never bound; a class with no public no-argument constructor cannot be built at all; and a public nested class inside a package-private outer one cannot be named from the generated package. Rather than copy that logic a third time -- copying it is what let the two platforms disagree -- it moves into HealthListenerScan, which both builders now feed and query. The plugin's Executor grew the two callbacks it was missing, so it can report whether a class is public and constructible at all; without them the resolver had nothing to work with. Nine unit tests cover the resolution directly, including the two cases reported: an abstract declarer resolving to its concrete subclass, and a listener without a usable constructor being reported rather than bound. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0de7d9e72e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…tions - AndroidHealthStore takes and persists the change token when the subscription is registered, not when it is first drained. A Health Connect token describes changes from the moment it is issued, so everything written between subscribe() and the first drain fell before it and the empty baseline delivery advanced the cursor past changes that would never be reported -- an app that subscribed at launch and drained on the next foreground silently missed the gap. - SensorSession drops samples of a type the store refuses outright instead of requeueing them. An Android cycling-power or cadence session produces types Health Connect reads but cannot write, so validation refused every batch and the retry resent it for as long as the session streamed: an error every storeBatchMillis, for ever, and a buffer that only grew. Retrying still applies to a store that is busy or locked. - SeriesSample's class documentation says iOS returns QuantitySample either way, matching the SampleQuery correction it contradicted. - The guide no longer says Android sleep reads work. Sleep is absent from the readable set and the bridge has no record class for it, so it is refused on both phones, not just iOS. - getAuthorizationRequestStatus documents that both ports answer UNKNOWN, what would implement it, and what to do meanwhile -- an explainer gated on SHOULD_REQUEST would never have shown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38c9dac714
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| store.storeAnchor(subscriptionId, | ||
| HealthAnchor.of(payload.trim())); |
There was a problem hiding this comment.
Seed the live handle with the baseline token
Fresh evidence after the earlier subscription-start finding is that BaselineToken.onSuccess() calls storeAnchor() only, which updates Preferences, while drainFrom() reads the token from the existing HealthSubscription via sub.getAnchor(). Consequently, even after this callback succeeds, the first drain still sees a null anchor and obtains a newer baseline, permanently skipping changes made between subscribe() and that drain; update the still-registered live handle as well as the persisted anchor.
Useful? React with 👍 / 👎.
| public void onReady(BlePeripheral value, Throwable err) { | ||
| if (err == null) { | ||
| discover(null); | ||
| } |
There was a problem hiding this comment.
Bound repeated reconnect failures
When a previously streaming sensor remains unavailable, a failed connect() transitions the peripheral to DISCONNECTED; Reconnector accepts that event while this session is CONNECTING and immediately invokes reconnect() again. Because this callback ignores err, these failures never increment reconnectFailures, so the advertised three-failure limit applies only to discovery/subscription failures and a lost sensor can trigger unbounded connection attempts without backoff until the caller manually stops the session.
Useful? React with 👍 / 👎.
Adds a first-class, cross-platform health API: HealthKit on iOS and watchOS, Health Connect on Android, live streaming from standard Bluetooth GATT health sensors, workout recording, and a scriptable simulator.
com.codename1.healthis greenfield — a repo-wide grep forhealthkit|health.connect|HKQuantity|androidx.healthreturned zero hits before this. Apps needing steps, heart rate, sleep, weight or workouts had no portable path and had to drop to per-platform native interfaces.Structurally this follows the Bluetooth API (#5399): a never-null facade over a no-op base class, reached through
Display.getHealth(), withCodenameOneImplementation.getHealth()defaulting tonullso no existing port breaks.What's here
HealthDataType/HealthUnit, four sample kinds,HealthStorewith afinal/SPI split, subscriptions with framework-owned anchorsRecordedWorkoutSession; sparseNutritionSampleLocalHealthStorefor desktop/JS/simulator, tvOS weak-link, watchOS plistHealthManifestFragments,HealthListenerBindings,AiDependencyTableentries, both buildersHealth.asciidoc+ 8 compiled snippetsDesign decisions worth reviewing
No reflection for background listeners. The first draft resolved a persisted listener class with
Class.forName, followingGeofenceManager. That was the wrong precedent to copy —LocationManagerneeds it to work around obfuscation, and reusing the shape inherited the fragility without the reason. A class referenced only by a string is invisible to the iOS and JavaScript translators' dead-code elimination, so it can be stripped from the very build that needs it. The build server now scans forHealthBackgroundListenerimplementations and generates a factory that constructs each with a directnew. This required extendingExecutor.ClassScannerwithimplementsInterface— the visitor already had the implementing class name and was discarding it.Strict privacy posture, unlike camera/bluetooth. The build fails when a health app declares no
NSHealthShareUsageDescription, rather than defaulting a placeholder. Apple reviews health purpose strings against what the app actually does, so a generic placeholder is what gets an app rejected — it would not even achieve the "keeps the build working" goal.Android permissions are hint-driven, not inferred. A data type is referenced as a constant, which compiles to a field read, and
Executor.visitFieldInsnis an empty override — so the permission set genuinely cannot be derived from bytecode. This also matches Play policy on declaring exactly what you use.The sensor subpackage is deliberately kept out of the health-data path.
AiDependencyTablematches withstartsWith, socom/codename1/health/also matchescom/codename1/health/sensors/. Putting theandroidx.healthdependency there would give an app that only reads a heart-rate strap a Health Connect dependency and a Play health-permissions review it has no business needing. The dependency lives inAndroidGradleBuilder, gated on health usage outside sensors; a test pins this.Aggregation is never delegated to the platform. Bucket boundaries and the duration-weighted average are computed once in shared code, so the two ports cannot drift apart on a user's weekly total.
Things the API refuses to claim
HealthKit deliberately does not disclose read authorization — a denied read is indistinguishable from having no data, so an app cannot infer what a user is hiding. Consequently there is no
hasReadPermission(),requestAuthorizationresolvingtruemeans the user was asked rather than that anything was granted, and the docs say to render "no data available" rather than accusing the user of denying access. The simulator defaults to reproducing this trap, since it is the behaviour real users produce and a permissive store never shows.Also: aggregate buckets return
nullrather than a synthetic0(a day with no data and a day with no steps are different facts);HealthSubscription.isPushDelivery()isfalseon Android because Health Connect never wakes an app; andWorkoutManager.isLiveSessionSupported()andisSensorCollectionSupported()are separate queryable facts.Verification
Both native layers were built with the real toolchains rather than checked by inspection — which found three defects inspection had missed, including
CN1Health.momitting the ParparVM instance parameter so every native argument was read one slot early, and the bindings generator emittingnew Outer$Inner(), which is not valid Java source and would have broken the common nested-listener case.xcodebuildarm64 against the iOS 26.2 SDK: BUILD SUCCEEDED. The generated project confirmsCN1_INCLUDE_HEALTHand bothINCLUDE_HEALTH*_USAGEdefines flipped andHealthKit.frameworklinked.#elsetrampolines link, which is the tvOS and Mac Catalyst path.assembleDebugat compileSdk 36: BUILD SUCCESSFUL. The shipped dex containsCN1HealthConnectBridge, the generatedCN1HealthListenerBindings,AndroidHealthSupportandandroidx.health.connect.HealthConnectClient, so the Kotlin bridge genuinely compiled rather than being skipped. The merged manifest carries exactly the declared permission set plus the rationale activity, the API-34 alias and the provider<queries>.hellocodenameonenow references the API from main sources and declares the build hints, so these paths stay exercised.Not verified
tvOS and watchOS platforms are not installed in the Xcode used, so those target builds — and therefore the
TV_OPTIONAL_FRAMEWORKSweak-link andWKBackgroundModesemission — are untested end to end. The compile-out path was verified by flipping the define, which exercises the same branch.No runtime execution. Everything is compile-and-link; nothing ran on a device, so the HealthKit query paths and the coroutine bridge are unproven at runtime. Dedicated
health-android.yml/health-ios.ymlCI legs are the natural follow-up, as neither bridge is compiled by any existing job.Coverage inside the bridges is partial by design: iOS implements availability, authorization, sample read and write; Android covers steps, heart rate and weight. Background delivery (
HKObserverQueryplus its completion watchdog), liveHKWorkoutSessionand glucose RACP are designed and documented but not implemented — the API reports them honestly rather than pretending.🤖 Generated with Claude Code