Class HealthStore
Reads, writes and watches the platform health store -- HealthKit on iOS, Health Connect on Android, a local store elsewhere.
Obtain one from Health.getStore(); it is never null. On a port with
no health support this base class is returned as-is and every operation
fails fast with HealthError.NOT_SUPPORTED.
Division of labour
The public methods here are final. They validate requests, normalize
units, page through results, compute bucket boundaries, own the
subscription registry and persist cursors -- all of it once, in shared
code. Ports implement only the do* methods, which receive
already-validated input and hand back raw platform data.
That split is deliberate: it is why a malformed query fails the same way on every platform, why unit conversion cannot drift between ports, and why no port can forget to check that the health provider is actually available.
Threading
Every method may be called from the EDT and returns immediately.
Change deliveries -- HealthChangeListener and
HealthBackgroundListener -- always arrive on the EDT.
Results of the operations you start arrive on the EDT on iOS and Android: both ports marshal every platform answer onto it, and the post-processing below hops back to it when it is done.
The local-backed ports do not carry that guarantee. On desktop,
JavaScript and the simulator a result is delivered on the thread that
asked for it, so a read started from a worker calls you back on that
worker and touching the UI from there needs your own callSerially.
Health and the developer guide say the same. This section has been
wrong in both directions -- it claimed an unconditional EDT guarantee
the local store never offered, and then denied the one the mobile
ports do make.
Post-processing of large result sets -- unit conversion across a hundred thousand samples -- happens on a shared background thread, so nothing here blocks the UI.
-
Field Summary
FieldsModifier and TypeFieldDescriptionprotected static final intDefault safety timeout for authorization flows, which involve a user tapping through a sheet.protected static final intDefault per-operation safety timeout for reads and writes. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionfinal AsyncResource<List<AggregateResult>> aggregate(AggregateQuery query) Summarizes data into time buckets.protected final voidaggregateByReadingSamples(AggregateQuery query, long[] boundaries, AsyncResource<List<AggregateResult>> out) The fallback aggregation: read every requested type over the query's range, then bucket the samples locally.protected final List<AggregateResult> aggregateSamples(AggregateQuery query, long[] boundaries, List<HealthSample> samples) Aggregates raw samples into the query's buckets.protected final voidarmTimeout(AsyncResource resource) FailsresourcewithHealthError.TIMEOUTif the platform never answers.protected final voidarmTimeout(AsyncResource resource, int millis) protected final long[]bucketBoundaries(AggregateQuery query) Computes the bucket boundaries for a query, asn+1timestamps boundingnbuckets.protected final voidclearAnchor(String subscriptionId) Drops a subscription's cursor, in memory and on disk.final AsyncResource<Integer> delete(HealthDeleteRequest request) Deletes samples this app wrote.protected voiddoAggregate(AggregateQuery query, long[] boundaries, AsyncResource<List<AggregateResult>> out) Computes bucketed aggregates.protected voiddoDelete(HealthDeleteRequest request, AsyncResource<Integer> out) Deletes samples matching an already-validated request.protected voiddoDrainChanges(List<HealthSubscription> subscriptions, AsyncResource<Integer> out) Polls for pending changes and delivers them throughfireChanges(HealthChangeBatch), resolving with the number of batches delivered.protected voidReports whether the authorization sheet would show anything.protected voiddoReadSamples(SampleQuery query, AsyncResource<SamplePage> out) Reads one page of raw platform samples.protected voiddoRequestAuthorization(List<HealthAccess> access, AsyncResource<Boolean> out) Presents the platform authorization UI.protected voiddoSubscribe(SubscriptionRequest request, HealthSubscription subscription) Registers a platform observer forsubscription, resuming fromHealthSubscription.getAnchor()when a cursor was persisted.protected voiddoUnsubscribe(HealthSubscription subscription) Tears down the platform observer belonging tosubscription.protected voiddoWrite(List<HealthSample> samples, AsyncResource<HealthWriteResult> out) Writes a chunk no larger thangetMaxWriteBatchSize(), already validated and converted intogetPreferredWriteUnit(HealthDataType).final AsyncResource<Integer> Drains pending changes for every active subscription, resolving with the number of batches delivered.protected final intfireChanges(HealthChangeBatch batch) Delivers a batch to whichever listener the subscription registered, on the EDT.final AsyncResource<HealthRequestStatus> getAuthorizationRequestStatus(HealthAccess... access) Whether presenting the authorization sheet would show the user anything -- seeHealthRequestStatus.intThe largest number of samples one platform write call accepts.protected final intThe current per-operation safety timeout.The unit this platform prefers to receivetypein.This app's read authorization fortype.final List<HealthSubscription> Every currently registered subscription.The metrics this platform can compute natively fortype.The types this platform can read.This app's write authorization fortype.final AsyncResource<Boolean> hasAnyData(HealthDataType type, HealthTimeRange range) Runs a bounded query and reports whether any sample came back.booleanWhether this platform can deliver changes without the app asking.booleanisDeletable(HealthDataType type) booleanWhether the OS wakes the app on new data -- seeHealthSubscription.isPushDelivery().booleanWhether samples oftypecan be deleted.booleantruewhen this store can do anything at all.booleanWhether this platform exposestype.booleanisWritable(HealthDataType type) Whether this app may writetype.protected final HealthAnchorloadAnchor(String subscriptionId) The persisted cursor for a subscription, or null to start fresh.final AsyncResource<SamplePage> readSamplePage(SampleQuery caller) Reads a single page of samples.final AsyncResource<List<HealthSample>> readSamples(SampleQuery query) Reads samples, following paging until the query's limit is reached or the data runs out.final AsyncResource<Boolean> requestAuthorization(HealthAccess... access) Presents the platform authorization UI for the requested access.protected final voidRe-registers every subscription persisted by a previous launch.protected final booleanseedAnchor(HealthSubscription subscription, HealthAnchor anchor) Seeds a subscription's starting cursor in memory and on disk, for a port that establishes one at registration -- the Health Connect baseline token, say.protected final voidsetAuthorizationTimeout(int millis) Sets the authorization safety timeout in milliseconds.static voidInstalls the build-generated factory that constructs background listeners after a process relaunch.protected final voidsetOperationTimeout(int millis) Sets the per-operation safety timeout in milliseconds.protected final voidstoreAnchor(String subscriptionId, HealthAnchor anchor) Persists a cursor.final HealthSubscriptionsubscribe(SubscriptionRequest request, HealthChangeListener listener) Subscribes with an in-memory listener, dropped when the process ends.final HealthSubscriptionsubscribe(SubscriptionRequest request, Class backgroundListenerClass) Subscribes with a listener that survives the app being killed.final voidunsubscribe(String subscriptionId) Cancels a subscription and discards its persisted cursor.final AsyncResource<HealthWriteResult> write(HealthSample sample) Writes one sample.final AsyncResource<HealthWriteResult> write(List<HealthSample> samples) Writes several samples, chunking togetMaxWriteBatchSize().
-
Field Details
-
DEFAULT_OPERATION_TIMEOUT
protected static final int DEFAULT_OPERATION_TIMEOUTDefault per-operation safety timeout for reads and writes.- See Also:
-
DEFAULT_AUTHORIZATION_TIMEOUT
protected static final int DEFAULT_AUTHORIZATION_TIMEOUTDefault safety timeout for authorization flows, which involve a user tapping through a sheet.- See Also:
-
-
Constructor Details
-
HealthStore
protected HealthStore()Ports construct subclasses. Application code obtains the active store fromHealth.getStore().
-
-
Method Details
-
isSupported
public boolean isSupported()truewhen this store can do anything at all.falseon the fallback base class. -
isTypeSupported
Whether this platform exposestype. Even a fully supported platform covers a subset ofHealthDataType.values(). -
getSupportedTypes
The types this platform can read. Empty on the fallback. -
isWritable
Whether this app may writetype. Some types are read-only on some platforms -- derived metrics the OS computes itself. -
isSourceDeduplicationSupported
public boolean isSourceDeduplicationSupported()Whether samples of
typecan be deleted. Both platforms restrict deletion to data this app wrote. Whether this store can de-duplicate overlapping sources when it aggregates.False everywhere by default, and the honest answer for most stores: the shared rollup computes every metric from raw samples, so a phone and a watch that both recorded one walk contribute it twice.
HealthKit's statistics engine does de-duplicate, which is a real capability this API refused to expose while it levelled the two platforms down to the behaviour of the weaker one. A store that overrides this to true honours
AggregateQuery.setDeduplicateSources(boolean); one that does not rejects the request rather than quietly ignoring it, so an app can tell the difference between "de-duplicated" and "counted twice" instead of comparing totals and guessing. -
isDeletable
-
getSupportedMetrics
The metrics this platform can compute natively fortype. Metrics outside this set are computed in shared code from raw samples, which is slower but portable. -
isBackgroundDeliverySupported
public boolean isBackgroundDeliverySupported()Whether this platform can deliver changes without the app asking. -
isPushDelivery
public boolean isPushDelivery()Whether the OS wakes the app on new data -- seeHealthSubscription.isPushDelivery(). -
getMaxWriteBatchSize
public int getMaxWriteBatchSize()The largest number of samples one platform write call accepts. Health Connect caps this at 1000; the base class chunks larger writes automatically. -
getPreferredWriteUnit
The unit this platform prefers to receivetypein. The base class converts before callingdoWrite(List,AsyncResource), so ports never do unit arithmetic. -
requestAuthorization
Presents the platform authorization UI for the requested access.
Resolving
truemeans the flow completed, not that access was granted. On iOS the sheet completes identically whether the user enabled every switch or none of them, and HealthKit will not say which. Treattrueas "the user has now been asked".Only Android distinguishes an explicit dismissal, which fails with
HealthError.USER_CANCELED. -
getWriteAuthorizationStatus
This app's write authorization fortype. Truthful on both platforms. -
getReadAuthorizationStatus
This app's read authorization for
type.Returns
HealthAuthorizationStatus.UNKNOWNon iOS in every case. HealthKit deliberately refuses to disclose read authorization, because an app that could tell the difference between "denied" and "no data" could infer that a user is hiding a pregnancy or a prescription. Android answers truthfully, because its read permissions are ordinary runtime grants.The Android port does not paper over the difference by pretending iOS behaves the same way, and neither should your UI: never tell a user "you denied access" on the strength of this. Say "no data available" and offer
Health.openHealthSettings().See
hasAnyData(HealthDataType,HealthTimeRange)for the only honest probe. -
getAuthorizationRequestStatus
public final AsyncResource<HealthRequestStatus> getAuthorizationRequestStatus(HealthAccess... access) Whether presenting the authorization sheet would show the user anything -- see
HealthRequestStatus. Useful for deciding whether to show your own explainer screen first.Answers
HealthRequestStatus.UNKNOWNon both phones in this release. Neither port implements the query yet -- HealthKit'sgetRequestStatusForAuthorizationand a Health Connect grant comparison are what would answer it -- so the documentedSHOULD_REQUESTandUNNECESSARYanswers never arrive there and an explainer gated on them would never show. Show the explainer on your own terms until this reports something else, and treatUNKNOWNas "ask anyway": requesting authorization that is already granted is harmless on both platforms. -
hasAnyData
Runs a bounded query and reports whether any sample came back.
This measures data presence, not permission, and the name says so on purpose. On iOS a
falsemeans "denied, or genuinely no data" and the two are indistinguishable -- that is a platform privacy guarantee, not a gap in this API.Note also that on iOS your own writes remain readable to you even when read access was denied, so
truedoes not prove you can see other apps' data. -
readSamples
Reads samples, following paging until the query's limit is reached or the data runs out.
Results are normalized: values arrive in the query's unit or the type's canonical unit, and series are flattened unless
SampleQuery.setFlattenSeries(boolean)says otherwise. -
readSamplePage
Reads a single page of samples. Prefer this overreadSamples(SampleQuery)for high-frequency types, so peak memory stays bounded regardless of how much history exists. -
armTimeout
Fails
resourcewithHealthError.TIMEOUTif the platform never answers.getOperationTimeout()was settable and documented and nothing ever armed a timer, so a native call that lost its callback left the operation pending forever -- which is precisely the case the setting exists for. -
armTimeout
-
aggregate
Summarizes data into time buckets.
Returns one
AggregateResultper bucket, in chronological order, including buckets that held no data -- those come back empty rather than being omitted, so a chart can render a gap where a gap belongs. Read the double-counting warning onAggregateQuerybefore trusting a cross-platform total. -
aggregateSamples
protected final List<AggregateResult> aggregateSamples(AggregateQuery query, long[] boundaries, List<HealthSample> samples) Aggregates raw samples into the query's buckets.
This is the one implementation of the arithmetic. Ports that have a native aggregation API can override
doAggregate(AggregateQuery, long[], AsyncResource)and use it; ports that do not get this for free, which is what stops a total computed on iOS from disagreeing with the same total computed on Android because two implementations rounded differently. -
bucketBoundaries
Computes the bucket boundaries for a query, as
n+1timestamps boundingnbuckets.Exposed to ports because both platforms want the boundaries up front, and because getting daylight-saving right exactly once is better than getting it wrong twice.
-
write
Writes one sample. -
write
Writes several samples, chunking to
getMaxWriteBatchSize().Rejects before touching the platform: types this app cannot write, instantaneous samples of interval-only types such as
HealthDataType.STEPS, and quantities whose unit measures the wrong dimension. Catching these here turns what would be an opaque platform exception into a message that names the offending sample. -
delete
Deletes samples this app wrote. SeeHealthDeleteRequest. -
subscribe
public final HealthSubscription subscribe(SubscriptionRequest request, Class backgroundListenerClass) Subscribes with a listener that survives the app being killed.
backgroundListenerClassmust be a public top-level class with a public no-argument constructor implementingHealthBackgroundListener-- see that interface for the full contract.Throws
IllegalArgumentException: if the class is null or does not implementHealthBackgroundListener. This is checked eagerly, because the alternative is discovering the mistake weeks later when a background relaunch silently does nothing.
-
subscribe
public final HealthSubscription subscribe(SubscriptionRequest request, HealthChangeListener listener) Subscribes with an in-memory listener, dropped when the process ends. Use theClassoverload if you need delivery after the app has been killed. -
unsubscribe
Cancels a subscription and discards its persisted cursor. Idempotent. -
getSubscriptions
Every currently registered subscription. -
drainChanges
Drains pending changes for every active subscription, resolving with the number of batches delivered.
Your app decides when this happens. Nothing here hooks the application lifecycle, so call it when you come to the foreground, and from your background-fetch handler -- Health Connect never wakes the app on its own, and this release registers no
HKObserverQueryeither, so nothing else will notice new data while your app is closed.HealthSubscription.isPushDelivery()answers false everywhere for the same reason.Overlapping calls are coalesced: a drain started while one is already running does not read the same change window a second time, it resolves alongside the one in flight.
-
fireChanges
Delivers a batch to whichever listener the subscription registered, on the EDT.
The cursor is persisted only after the listener returns. That ordering means a crash inside a listener costs one redelivered batch rather than losing the data permanently -- the opposite ordering would advance past data the app never actually processed.
Returns
How many deliveries were queued --
0when nothing was, and more than one when the subscription's per-batch cap split the page. Ports add this to the count they resolvedrainChangeswith, which is documented as a number of batches: returning a yes/no counted a page of 250 additions capped at 100 as one batch while the listener was called three times. -
setBackgroundListenerFactory
Installs the build-generated factory that constructs background listeners after a process relaunch.
Called by code the build server injects into app startup, in the same way the Android port's native bridges are registered. There is deliberately no reflective fallback -- see
HealthBackgroundListenerFactoryfor why resolving a class by name is the wrong mechanism on these targets. -
loadAnchor
The persisted cursor for a subscription, or null to start fresh. -
seedAnchor
Seeds a subscription's starting cursor in memory and on disk, for a port that establishes one at registration -- the Health Connect baseline token, say.
Both halves are needed. Persisting alone looked correct and did nothing: the next drain still found a null anchor on the handle, took a fresh cursor, and skipped exactly the window the seed existed to cover.
The seed is dropped, not applied, in two cases, because the cursor it carries comes from a platform call that started earlier and may land arbitrarily late:
- the handle is no longer the registered one. It was stopped, or
replaced by a new subscription reusing the id. Applying it would
restore a cursor
unsubscribe()promised to discard, or hand a fresh subscription a cursor issued for a different set of types. - the handle already has a cursor. Something has advanced past this seed -- a drain that ran while it was in flight, or a restored cursor from a previous launch -- and rewinding to it would re-deliver changes the app has already been told about.
Returns whether the seed was applied, so a port can tell a dropped seed from an accepted one.
- the handle is no longer the registered one. It was stopped, or
replaced by a new subscription reusing the id. Applying it would
restore a cursor
-
storeAnchor
Persists a cursor. Called byfireChanges(HealthChangeBatch)after successful delivery; ports rarely need it directly. -
clearAnchor
Drops a subscription's cursor, in memory and on disk.
For a cursor the platform has rejected outright -- a Health Connect change token that has aged out, say. Clearing only the persisted copy is not enough: the drains read the live handle, so the next one would resend the very token that just failed and keep failing identically forever.
-
restoreSubscriptions
protected final void restoreSubscriptions()Re-registers every subscription persisted by a previous launch.
This is what makes a subscription survive the process being killed: without it, an app relaunched into the background has no idea it was ever watching anything.
It runs on its own, the first time anything touches subscriptions -- see
ensureSubscriptionsRestored(). It used to be documented as something ports had to call from their constructor, which no port did, so persisted subscriptions were silently never restored on any platform. A constructor could not have called it safely anyway: it dispatches todoSubscribe, which a subclass has not finished initialising at that point. -
setOperationTimeout
protected final void setOperationTimeout(int millis) Sets the per-operation safety timeout in milliseconds. Ports use it so that a platform callback that never arrives surfaces asHealthError.TIMEOUTinstead of an operation that hangs forever. -
getOperationTimeout
protected final int getOperationTimeout()The current per-operation safety timeout. -
setAuthorizationTimeout
protected final void setAuthorizationTimeout(int millis) Sets the authorization safety timeout in milliseconds.
Separate from the operation timeout because this one is waiting on a person rather than on a platform call, so it is far longer by default. It also bounds how long a queued authorization can wait: the screen is released when the active flow settles, and this is what guarantees that happens.
-
doReadSamples
Reads one page of raw platform samples. Input is already validated; units and series flattening are handled by the caller. -
doAggregate
protected void doAggregate(AggregateQuery query, long[] boundaries, AsyncResource<List<AggregateResult>> out) Computes bucketed aggregates.
boundariesholdsn+1timestamps boundingnbuckets, already daylight-saving correct.Unlike the rest of the port SPI this does not default to NOT_SUPPORTED. Neither HealthKit nor Health Connect gives us an aggregation we can use directly for every metric this API exposes, so the default reads the raw samples and runs the shared arithmetic in
aggregateSamples(AggregateQuery, long[], List). A port only overrides this when its native aggregation is genuinely better -- and if it does, it owes the same answers. -
aggregateByReadingSamples
protected final void aggregateByReadingSamples(AggregateQuery query, long[] boundaries, AsyncResource<List<AggregateResult>> out) The fallback aggregation: read every requested type over the query's range, then bucket the samples locally.
Reads one type at a time because HealthKit accepts only one type per query, and it is the narrower contract of the two.
-
doWrite
Writes a chunk no larger thangetMaxWriteBatchSize(), already validated and converted intogetPreferredWriteUnit(HealthDataType). -
doDelete
Deletes samples matching an already-validated request. -
doRequestAuthorization
Presents the platform authorization UI. -
doGetAuthorizationRequestStatus
protected void doGetAuthorizationRequestStatus(List<HealthAccess> access, AsyncResource<HealthRequestStatus> out) Reports whether the authorization sheet would show anything.
No mobile port overrides this yet, so both phones answer
UNKNOWN; the public method says so. HealthKit answers it throughgetRequestStatusForAuthorization, and Health Connect through comparing the granted permission set against the requested one -- each of which is a real piece of work rather than a mapping. -
doSubscribe
Registers a platform observer for
subscription, resuming fromHealthSubscription.getAnchor()when a cursor was persisted. Called on registration and again onrestoreSubscriptions().The live handle is passed rather than the bare anchor because a port that establishes a starting cursor asynchronously has to be able to tell, when its answer arrives, whether the registration it answers for is still the current one -- see
seedAnchor(HealthSubscription,HealthAnchor). -
doUnsubscribe
Tears down the platform observer belonging to
subscription.The handle, not the id: another registration under the same id can land between the removal and this call, and a teardown that works by id alone then dismantles the replacement's platform state. A port must key its own bookkeeping on this instance so it tears down the generation that was actually cancelled.
-
doDrainChanges
Polls for pending changes and delivers them throughfireChanges(HealthChangeBatch), resolving with the number of batches delivered.
-