Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
d97767b
Health API: core vocabulary, store, BLE sensors and workout recording
shai-almog Jul 26, 2026
360c059
Health API: local store for ports without a platform health provider
shai-almog Jul 26, 2026
df68fd3
Health API: bind background listeners at build time, not by reflection
shai-almog Jul 26, 2026
66dca06
Health API: nutrition types and Android/iOS build fragments
shai-almog Jul 26, 2026
c2c7d06
Health API: wire the health fragments into the Android and iOS builders
shai-almog Jul 26, 2026
e52df39
Health API: scriptable simulator with the read-authorization trap
shai-almog Jul 26, 2026
e83e984
Health API: developer guide chapter and compiled snippets
shai-almog Jul 26, 2026
e3767fc
Health API: Android port over Health Connect
shai-almog Jul 26, 2026
8c0f8f5
Health API: iOS port over HealthKit, plus tvOS and watchOS slices
shai-almog Jul 26, 2026
2b9c207
Health API: remaining test tiers, on-device conformance, paging fix
shai-almog Jul 26, 2026
a9dc191
Health API: fix CN1Health.m against a real Xcode build
shai-almog Jul 26, 2026
11247cb
Health API: fix generated bindings for nested listeners, verified on …
shai-almog Jul 26, 2026
5b5a3c1
Health API: fix the SpotBugs findings CI surfaced
shai-almog Jul 26, 2026
ea61ba3
Health API: fix the Vale prose gate on the guide chapter
shai-almog Jul 26, 2026
1f758eb
Health API: use only the CLDC subset of Calendar and Math
shai-almog Jul 26, 2026
6a44b4a
Health: address PR review -- honest failures over silent no-ops
shai-almog Jul 26, 2026
ea9a839
Health: make the advertised behaviour the real behaviour
shai-almog Jul 26, 2026
a33790c
Health: fix Checkstyle indentation in two callback blocks
shai-almog Jul 26, 2026
716ef99
Health: second review round -- wire up what was only configurable
shai-almog Jul 26, 2026
d1a550c
Health: third review round -- fix the reconnect NPE and stop overclai…
shai-almog Jul 26, 2026
44ce735
Health: fourth review round -- stop losing data at the cursor
shai-almog Jul 27, 2026
2011661
Health: enforce the read limit before writing a series
shai-almog Jul 27, 2026
11d4fbb
Health: sixth review round -- stop advertising what is not there
shai-almog Jul 27, 2026
67c2b61
Health: seventh review round -- fix the paging I added last round
shai-almog Jul 27, 2026
8e9f13b
Health: eighth review round -- fix the crash and the facade regression
shai-almog Jul 27, 2026
ab422bc
Health: ninth review round -- half-open ranges and the paused workout
shai-almog Jul 27, 2026
b946893
Health: last docs contraction, and run Vale locally
shai-almog Jul 27, 2026
f60896f
Health: tenth review round -- deliver the capped tail instead of looping
shai-almog Jul 27, 2026
a1406bb
Health: test the invariants that keep breaking
shai-almog Jul 27, 2026
db96eec
Health: sequential chunk delivery, persisted options, local snapshots
shai-almog Jul 27, 2026
20a5b50
Health: reschedule requeued writes, bound Android pages, signal iOS t…
shai-almog Jul 27, 2026
3de9a33
Health: make the partial write result transient
shai-almog Jul 27, 2026
9a314d4
Merge remote-tracking branch 'origin/master' into first-class-health
shai-almog Jul 27, 2026
8e2d009
Health: percent-unit corruption, local-store contract, builder gating
shai-almog Jul 27, 2026
55a9050
Health: arm the timeout, gate drains on delivery, reject BP composites
shai-almog Jul 27, 2026
3716fb1
Health: fix four regressions from my own last round
shai-almog Jul 27, 2026
e13adc9
Health: workout facade needs a data token; policy URL only when reque…
shai-almog Jul 28, 2026
be27fd6
Health: never split a series record at the page limit
shai-almog Jul 28, 2026
2200d81
Health: count abandoned chunks, fail timeouts on the EDT
shai-almog Jul 28, 2026
61cefea
Health: report zero cadence when a sensor stops
shai-almog Jul 28, 2026
c6692e5
Health: three more in my own recent code
shai-almog Jul 28, 2026
7e2fe26
Health: detect store calls through a handle, and workout purpose strings
shai-almog Jul 28, 2026
10f9ea7
Health: fail store rejections on the calling thread, like every other…
shai-almog Jul 28, 2026
3cbbdc4
Health: six review findings across the wire format, iOS reads and the…
shai-almog Jul 28, 2026
da0c409
Health: reject a whitespace-only privacy policy URL
shai-almog Jul 28, 2026
60a4ab0
Health: cursor loss on re-subscribe, a stale binding, and the iOS res…
shai-almog Jul 28, 2026
3f111e7
Health: four defects the series-write fix introduced, and watch twin …
shai-almog Jul 28, 2026
70a7f7d
Health: never trim a single-type page, and two directional gaps
shai-almog Jul 28, 2026
8a00576
Health: seven more, from local snapshots to Android capability gating
shai-almog Jul 28, 2026
de0063b
Health: trim watch purpose strings, reject contradictory watch hints
shai-almog Jul 28, 2026
49ff38e
Health: aggregate boundary, local id mutation, iOS registry leak
shai-almog Jul 28, 2026
5a0b77b
Health: require workout tokens, emit series points in the query's dir…
shai-almog Jul 28, 2026
8e85b14
Health: size the first series page, reject partial subscriptions, nar…
shai-almog Jul 28, 2026
bc08b57
Health: count series measurements, and honour the type on id deletes
shai-almog Jul 28, 2026
284d701
Health: count the special reads as permissions in the policy gate
shai-almog Jul 28, 2026
403944d
Health: workouts say what they could not keep, and stop promising liv…
shai-almog Jul 28, 2026
18c5bee
Health: coalesce drains, validate series points, and stop over-promising
shai-almog Jul 28, 2026
b40a3eb
Health: naming a health class is not using the store
shai-almog Jul 28, 2026
bbaee2c
Health: never trim inside a record, and four narrower fixes
shai-almog Jul 28, 2026
7845041
Health: reject only the tokens no operation can service
shai-almog Jul 28, 2026
8dd753c
Health: series measurements in aggregates and workout statistics
shai-almog Jul 28, 2026
770888f
Scanner: read the whole class name out of a field descriptor
shai-almog Jul 28, 2026
5f6d94b
Health: a series needs a numeric type, and three build-side corrections
shai-almog Jul 28, 2026
ed483c5
Health: reject counts outside the range of a 64-bit integer
shai-almog Jul 28, 2026
7a4fdf4
Health: series spans, foreground deadlines, and an honest threading note
shai-almog Jul 28, 2026
1d511f3
Health: read the full budget per type, and demand both watch purpose …
shai-almog Jul 28, 2026
316d07d
Health: arm the drain timeout, bound series points, resolve auth afte…
shai-almog Jul 28, 2026
2bd5cc6
Health: recover from an expired change token, and two smaller correct…
shai-almog Jul 28, 2026
8b7ed43
Health: obtaining the store is not data access
shai-almog Jul 28, 2026
99df7e5
Health: resync ordering, sensor write-through detection, the cuff claim
shai-almog Jul 28, 2026
4637d9f
Health: sensor write-through on iOS, the iOS 12 Bluetooth key, a pari…
shai-almog Jul 28, 2026
e1b90d7
Health: Android background delivery is unsupported, and the limit is …
shai-almog Jul 28, 2026
2679aec
Health: reject failed weight readings, and three read/doc corrections
shai-almog Jul 28, 2026
093a240
Health: a HealthKit sub-capability needs the base entitlement
shai-almog Jul 28, 2026
d98213f
Health: count only delivered batches, and bound sleep stages
shai-almog Jul 28, 2026
3d117f9
Health: reject non-finite values, keep the pedal reference, explain RR
shai-almog Jul 28, 2026
b6c8fff
Health: eight review fixes across sleep, sensors, subscriptions and t…
shai-almog Jul 28, 2026
d30c13d
Health: fix a broken Android port, count deliveries, and five review …
shai-almog Jul 28, 2026
4f335f3
Health: bound the active duration, and honour the weight scale's sent…
shai-almog Jul 28, 2026
170d931
Health: count covered time once, and name listeners from InnerClasses
shai-almog Jul 28, 2026
7436f92
Health: persist the local store, empty per type, and clear the qualit…
shai-almog Jul 28, 2026
cb354d4
Remove CI artifacts committed by accident
shai-almog Jul 28, 2026
bf95df2
Health: hide records before the limit, keep the food name, scope nutr…
shai-almog Jul 28, 2026
38c0c94
Health: refuse the keepalive request, fail unpersisted writes, scope …
shai-almog Jul 28, 2026
cc595b2
Health: keep the old records when a save fails, and two smaller corre…
shai-almog Jul 28, 2026
b7cc348
Health: report failed scans, reject reserved years, fix the correlati…
shai-almog Jul 28, 2026
4ac4464
Health: marshal scan failures to the EDT, keep the source through con…
shai-almog Jul 28, 2026
b088ea0
Health: stop advertising native Android aggregates, and three more co…
shai-almog Jul 28, 2026
387057f
Health: correct four more live-session claims in the javadoc
shai-almog Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions .github/workflows/health-android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
---
name: Health (Health Connect) integration

# Scoped to the health feature. The load-bearing artifact here is
# CN1HealthConnectBridge.kt: it is a plugin *resource* compiled by the app's
# own Gradle build, so no job in this repository compiles it. Without this
# workflow a Kotlin syntax error, a wrong androidx.health.connect API or a
# record class that does not exist ships straight to customers.
#
# The health smoke source deliberately lives outside the Hello Codename One
# source root and is copied in here. Referencing com.codename1.health raises
# minSdkVersion to 26, and at 26 the platform supplies java.time instead of
# the desugared library -- which changes behaviour for unrelated tests. The
# shared conformance suite must keep running at the framework's normal floor.
'on':
pull_request:
paths:
- '.github/workflows/health-android.yml'
- 'CodenameOne/src/com/codename1/health/**'
- 'CodenameOne/src/com/codename1/impl/health/**'
- 'Ports/Android/src/com/codename1/impl/android/AndroidHealth*.java'
- 'Ports/Android/src/com/codename1/impl/android/HealthConnectDelegate.java'
- 'Ports/Android/src/com/codename1/health/**'
- 'maven/codenameone-maven-plugin/src/main/resources/com/codename1/builders/health/**'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/HealthManifestFragments.java'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/HealthListenerBindings.java'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java'
- 'scripts/hellocodenameone/health-smoke/**'
push:
branches:
- master
paths:
- '.github/workflows/health-android.yml'
- 'CodenameOne/src/com/codename1/health/**'
- 'CodenameOne/src/com/codename1/impl/health/**'
- 'Ports/Android/src/com/codename1/impl/android/AndroidHealth*.java'
- 'Ports/Android/src/com/codename1/impl/android/HealthConnectDelegate.java'
- 'Ports/Android/src/com/codename1/health/**'
- 'maven/codenameone-maven-plugin/src/main/resources/com/codename1/builders/health/**'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/HealthManifestFragments.java'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/HealthListenerBindings.java'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java'
- 'scripts/hellocodenameone/health-smoke/**'

jobs:
health-android:
name: Health Connect Android
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- name: Set TMPDIR
run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV
- name: Cache codenameone-tools
uses: actions/cache@v5
with:
path: ${{ runner.temp }}/codenameone-tools
key: ${{ runner.os }}-cn1-tools-${{ hashFiles('scripts/setup-workspace.sh') }}
restore-keys: |
${{ runner.os }}-cn1-tools-
- name: Cache Maven repository
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-health-android-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2-health-android-
${{ runner.os }}-m2-
- name: Cache Gradle
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-health-${{ hashFiles('scripts/hellocodenameone/**/build.gradle*', 'Ports/Android/build.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-health-
${{ runner.os }}-gradle-
- name: Setup workspace
run: ./scripts/setup-workspace.sh -q -DskipTests
- name: Build Android port
run: ./scripts/build-android-port.sh -q -DskipTests
- name: Make the smoke app a health app
run: |
set -euo pipefail
dest=scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone
mkdir -p "$dest"
cp scripts/hellocodenameone/health-smoke/HealthSmokeDemo.java "$dest/"
# The android.health.* hints are already in the checked-in
# settings and stay inert until this file is copied in: the
# builder only demands them once the class scanner sees a
# com.codename1.health reference. They cannot be derived from
# bytecode -- the scanner never sees a field read, so
# HealthDataType.STEPS is invisible to it.
# This is the step that earns the workflow. build-android-app.sh runs
# gradlew assembleDebug, which is the only place
# CN1HealthConnectBridge.kt is ever compiled against a real
# androidx.health.connect artifact.
- name: Build Hello Codename One Android app (health-enabled)
id: build-android-app
run: |
mkdir -p ~/.codenameone
cp maven/UpdateCodenameOne.jar ~/.codenameone/
./scripts/build-android-app.sh -q -DskipTests
- name: Assert Health Connect wiring was injected
run: |
set -euo pipefail
dir="${{ steps.build-android-app.outputs.gradle_project_dir }}"
echo "Gradle project: $dir"
find "$dir" -name 'CN1HealthConnectBridge.kt' | grep -q . \
|| { echo "::error::Health Connect bridge not copied into project"; exit 1; }
grep -Rqs --include='build.gradle*' "androidx.health.connect:connect-client" "$dir" \
|| { echo "::error::connect-client dependency not injected"; exit 1; }
grep -Rqs --include=AndroidManifest.xml \
"android.permission.health.READ_STEPS" "$dir" \
|| { echo "::error::per-type read permission missing from manifest"; exit 1; }
grep -Rqs --include=AndroidManifest.xml \
"android.permission.health.WRITE_STEPS" "$dir" \
|| { echo "::error::per-type write permission missing from manifest"; exit 1; }
grep -Rqs --include=AndroidManifest.xml \
"com.google.android.apps.healthdata" "$dir" \
|| { echo "::error::Health Connect package query missing from manifest"; exit 1; }
# READ_HEART_RATE is a prefix of READ_HEART_RATE_VARIABILITY, so a
# substring match would pass even when dedup is broken. Match the
# closing quote to prove the exact permission was emitted.
grep -Rqs --include=AndroidManifest.xml \
'android.permission.health.READ_HEART_RATE"' "$dir" \
|| { echo "::error::exact READ_HEART_RATE permission missing"; exit 1; }
echo "Health Connect wiring present."
- name: Assert the bridge reached the dex
run: |
set -euo pipefail
dir="${{ steps.build-android-app.outputs.gradle_project_dir }}"
apk="$(find "$dir" -name '*debug*.apk' | head -1)"
[ -n "$apk" ] || { echo "::error::no debug APK produced"; exit 1; }
work="$(mktemp -d)"
unzip -qo "$apk" -d "$work"
dexdump="$(find "$ANDROID_HOME/build-tools" -name dexdump | sort | tail -1)"
found=0
for dex in "$work"/*.dex; do
if "$dexdump" -f "$dex" 2>/dev/null \
| grep -q "Lcom/codename1/health/CN1HealthConnectBridge;"; then
found=1
break
fi
done
[ "$found" = 1 ] \
|| { echo "::error::bridge compiled but was shrunk out of the dex"; exit 1; }
echo "Bridge present in the dex."
- name: Upload build output
if: always()
uses: actions/upload-artifact@v7
with:
name: health-android-artifacts
path: |
${{ steps.build-android-app.outputs.gradle_project_dir }}/app/build/outputs/apk/debug/*.apk
${{ steps.build-android-app.outputs.gradle_project_dir }}/app/src/main/AndroidManifest.xml
${{ steps.build-android-app.outputs.gradle_project_dir }}/app/src/main/java/com/codename1/health/CN1HealthConnectBridge.kt
if-no-files-found: warn
retention-days: 14
62 changes: 62 additions & 0 deletions CodenameOne/src/com/codename1/health/AggregateMetric.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.health;

/// What to compute over an aggregation bucket. Which metrics are
/// meaningful depends on the type's [HealthAggregationStyle]; asking for a
/// nonsensical combination -- the total of a body-mass series, say --
/// fails with [HealthError#INVALID_ARGUMENT] rather than returning a
/// number nobody should trust.
public enum AggregateMetric {

/// The sum across the bucket. Meaningful for
/// [HealthAggregationStyle#CUMULATIVE] types only.
TOTAL,

/// The mean across the bucket. Meaningful for
/// [HealthAggregationStyle#DISCRETE] types.
///
/// Interval samples are weighted by duration and instantaneous samples
/// are weighted equally, so a heart rate held for ten minutes counts
/// for more than a single spot reading.
AVERAGE,

/// The smallest value in the bucket. [HealthAggregationStyle#DISCRETE]
/// types.
MINIMUM,

/// The largest value in the bucket. [HealthAggregationStyle#DISCRETE]
/// types.
MAXIMUM,

/// How many samples fell in the bucket. Meaningful for every type.
COUNT,

/// The total time covered by samples in the bucket, in milliseconds.
/// Meaningful for every type; the natural metric for sessions.
DURATION,

/// The most recent value in the bucket.
/// [HealthAggregationStyle#DISCRETE] types.
LATEST
}
Loading
Loading