Class HealthStore

java.lang.Object
com.codename1.health.HealthStore

public class HealthStore extends Object

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 Details

    • DEFAULT_OPERATION_TIMEOUT

      protected static final int DEFAULT_OPERATION_TIMEOUT
      Default per-operation safety timeout for reads and writes.
      See Also:
    • DEFAULT_AUTHORIZATION_TIMEOUT

      protected static final int DEFAULT_AUTHORIZATION_TIMEOUT
      Default 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 from Health.getStore().
  • Method Details

    • isSupported

      public boolean isSupported()
      true when this store can do anything at all. false on the fallback base class.
    • isTypeSupported

      public boolean isTypeSupported(HealthDataType type)
      Whether this platform exposes type. Even a fully supported platform covers a subset of HealthDataType.values().
    • getSupportedTypes

      public List<HealthDataType> getSupportedTypes()
      The types this platform can read. Empty on the fallback.
    • isWritable

      public boolean isWritable(HealthDataType type)
      Whether this app may write type. Some types are read-only on some platforms -- derived metrics the OS computes itself.
    • isDeletable

      public boolean isDeletable(HealthDataType type)
      Whether samples of type can be deleted. Both platforms restrict deletion to data this app wrote.
    • getSupportedMetrics

      public List<AggregateMetric> getSupportedMetrics(HealthDataType type)
      The metrics this platform can compute natively for type. 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 -- see HealthSubscription.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

      public HealthUnit getPreferredWriteUnit(HealthDataType type)
      The unit this platform prefers to receive type in. The base class converts before calling doWrite(List,AsyncResource), so ports never do unit arithmetic.
    • requestAuthorization

      public final AsyncResource<Boolean> requestAuthorization(HealthAccess... access)

      Presents the platform authorization UI for the requested access.

      Resolving true means 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. Treat true as "the user has now been asked".

      Only Android distinguishes an explicit dismissal, which fails with HealthError.USER_CANCELED.

    • getWriteAuthorizationStatus

      public HealthAuthorizationStatus getWriteAuthorizationStatus(HealthDataType type)
      This app's write authorization for type. Truthful on both platforms.
    • getReadAuthorizationStatus

      public HealthAuthorizationStatus getReadAuthorizationStatus(HealthDataType type)

      This app's read authorization for type.

      Returns HealthAuthorizationStatus.UNKNOWN on 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.
    • hasAnyData

      public final AsyncResource<Boolean> hasAnyData(HealthDataType type, HealthTimeRange range)

      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 false means "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 true does not prove you can see other apps' data.

    • readSamples

      public final AsyncResource<List<HealthSample>> readSamples(SampleQuery query)

      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

      public final AsyncResource<SamplePage> readSamplePage(SampleQuery query)
      Reads a single page of samples. Prefer this over readSamples(SampleQuery) for high-frequency types, so peak memory stays bounded regardless of how much history exists.
    • aggregate

      public final AsyncResource<List<AggregateResult>> aggregate(AggregateQuery query)

      Summarizes data into time buckets.

      Returns one AggregateResult per 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 on AggregateQuery before 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

      protected final long[] bucketBoundaries(AggregateQuery query)

      Computes the bucket boundaries for a query, as n+1 timestamps bounding n buckets.

      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

      public final AsyncResource<HealthWriteResult> write(HealthSample sample)
      Writes one sample.
    • write

      public final AsyncResource<HealthWriteResult> write(List<HealthSample> samples)

      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

      public final AsyncResource<Integer> delete(HealthDeleteRequest request)
      Deletes samples this app wrote. See HealthDeleteRequest.
    • subscribe

      public final HealthSubscription subscribe(SubscriptionRequest request, Class backgroundListenerClass)

      Subscribes with a listener that survives the app being killed.

      backgroundListenerClass must be a public top-level class with a public no-argument constructor implementing HealthBackgroundListener -- see that interface for the full contract.

      Throws
      • IllegalArgumentException: if the class is null or does not implement HealthBackgroundListener. 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 the Class overload if you need delivery after the app has been killed.
    • unsubscribe

      public final void unsubscribe(String subscriptionId)
      Cancels a subscription and discards its persisted cursor. Idempotent.
    • getSubscriptions

      public final List<HealthSubscription> getSubscriptions()
      Every currently registered subscription.
    • drainChanges

      public final AsyncResource<Integer> 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

      protected final void fireChanges(HealthChangeBatch batch)

      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

      public static void setBackgroundListenerFactory(HealthBackgroundListenerFactory factory)

      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 HealthBackgroundListenerFactory for why resolving a class by name is the wrong mechanism on these targets.

    • loadAnchor

      protected final HealthAnchor loadAnchor(String subscriptionId)
      The persisted cursor for a subscription, or null to start fresh.
    • storeAnchor

      protected final void storeAnchor(String subscriptionId, HealthAnchor anchor)
      Persists a cursor. Called by fireChanges(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 to doSubscribe, 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 as HealthError.TIMEOUT instead of an operation that hangs forever.
    • getOperationTimeout

      protected final int getOperationTimeout()
      The current per-operation safety timeout.
    • doReadSamples

      protected void doReadSamples(SampleQuery query, AsyncResource<SamplePage> out)
      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. boundaries holds n+1 timestamps bounding n buckets, 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

      protected void doWrite(List<HealthSample> samples, AsyncResource<HealthWriteResult> out)
      Writes a chunk no larger than getMaxWriteBatchSize(), already validated and converted into getPreferredWriteUnit(HealthDataType).
    • doDelete

      protected void doDelete(HealthDeleteRequest request, AsyncResource<Integer> out)
      Deletes samples matching an already-validated request.
    • doRequestAuthorization

      protected void doRequestAuthorization(List<HealthAccess> access, AsyncResource<Boolean> out)
      Presents the platform authorization UI.
    • doGetAuthorizationRequestStatus

      protected void doGetAuthorizationRequestStatus(List<HealthAccess> access, AsyncResource<HealthRequestStatus> out)
      Reports whether the authorization sheet would show anything.
    • doSubscribe

      protected void doSubscribe(SubscriptionRequest request, HealthAnchor anchor)
      Registers a platform observer, resuming from anchor when one was persisted. Called on registration and again on restoreSubscriptions().
    • doUnsubscribe

      protected void doUnsubscribe(String subscriptionId)
      Tears down a platform observer.
    • doDrainChanges

      protected void doDrainChanges(List<HealthSubscription> subscriptions, AsyncResource<Integer> out)
      Polls for pending changes and delivers them through fireChanges(HealthChangeBatch), resolving with the number of batches delivered.