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. Every callback arrives on the EDT. 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 long[]bucketBoundaries(AggregateQuery query) Computes the bucket boundaries for a query, asn+1timestamps boundingnbuckets.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, HealthAnchor anchor) Registers a platform observer, resuming fromanchorwhen one was persisted.protected voiddoUnsubscribe(String subscriptionId) Tears down a platform observer.protected voiddoWrite(List<HealthSample> samples, AsyncResource<HealthWriteResult> out) Writes a chunk no larger thangetMaxWriteBatchSize(), already validated and converted intogetPreferredWriteUnit(HealthDataType).final AsyncResource<Integer> Immediately drains pending changes for every active subscription, resolving with the number of batches delivered.protected final voidfireChanges(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) Whether samples oftypecan be deleted.booleanWhether the OS wakes the app on new data -- seeHealthSubscription.isPushDelivery().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 query) 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.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. -
isDeletable
Whether samples oftypecan be deleted. Both platforms restrict deletion to data this app wrote. -
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 -- seeHealthRequestStatus. Useful for deciding whether to show your own explainer screen first. -
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. -
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
Immediately drains pending changes for every active subscription, resolving with the number of batches delivered.
Called automatically at app start and when the app returns to the foreground. On Android you should also call it from your background-fetch handler -- Health Connect never wakes the app on its own, so nothing else will notice new data while your app is closed.
-
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.
-
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. -
storeAnchor
Persists a cursor. Called byfireChanges(HealthChangeBatch)after successful delivery; ports rarely need it directly. -
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. -
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. -
doSubscribe
Registers a platform observer, resuming fromanchorwhen one was persisted. Called on registration and again onrestoreSubscriptions(). -
doUnsubscribe
Tears down a platform observer. -
doDrainChanges
Polls for pending changes and delivers them throughfireChanges(HealthChangeBatch), resolving with the number of batches delivered.
-