# 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.

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

  Fields
  Modifier and Type
  Field
  Description
  `protected static final int`
  `DEFAULT_AUTHORIZATION_TIMEOUT`
  Default safety timeout for authorization flows, which involve a user tapping through a sheet.
  `protected static final int`
  `DEFAULT_OPERATION_TIMEOUT`
  Default per-operation safety timeout for reads and writes.

- ## Constructor Summary

  Constructors
  Modifier
  Constructor
  Description
  `protected`
  `HealthStore()`
  Ports construct subclasses.

- ## Method Summary

  Modifier and Type
  Method
  Description
  `final AsyncResource<List<AggregateResult>>`
  `aggregate(AggregateQuery query)`
  Summarizes data into time buckets.
  `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.
  `protected final List<AggregateResult>`
  `aggregateSamples(AggregateQuery query, long[] boundaries, List<HealthSample> samples)`
  Aggregates raw samples into the query's buckets.
  `protected final void`
  `armTimeout(AsyncResource resource)`
  Fails `resource` with `HealthError.TIMEOUT` if the platform never answers.
  `protected final void`
  `armTimeout(AsyncResource resource, int millis)`
  `protected final long[]`
  `bucketBoundaries(AggregateQuery query)`
  Computes the bucket boundaries for a query, as `n+1` timestamps bounding `n` buckets.
  `protected final void`
  `clearAnchor(String subscriptionId)`
  Drops a subscription's cursor, in memory and on disk.
  `final AsyncResource<Integer>`
  `delete(HealthDeleteRequest request)`
  Deletes samples this app wrote.
  `protected void`
  `doAggregate(AggregateQuery query, long[] boundaries, AsyncResource<List<AggregateResult>> out)`
  Computes bucketed aggregates.
  `protected void`
  `doDelete(HealthDeleteRequest request, AsyncResource<Integer> out)`
  Deletes samples matching an already-validated request.
  `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.
  `protected void`
  `doGetAuthorizationRequestStatus(List<HealthAccess> access, AsyncResource<HealthRequestStatus> out)`
  Reports whether the authorization sheet would show anything.
  `protected void`
  `doReadSamples(SampleQuery query, AsyncResource<SamplePage> out)`
  Reads one page of raw platform samples.
  `protected void`
  `doRequestAuthorization(List<HealthAccess> access, AsyncResource<Boolean> out)`
  Presents the platform authorization UI.
  `protected void`
  `doSubscribe(SubscriptionRequest request, HealthSubscription subscription)`
  Registers a platform observer for `subscription`, resuming from `HealthSubscription.getAnchor()` when a cursor was persisted.
  `protected void`
  `doUnsubscribe(HealthSubscription subscription)`
  Tears down the platform observer belonging to `subscription`.
  `protected void`
  `doWrite(List<HealthSample> samples, AsyncResource<HealthWriteResult> out)`
  Writes a chunk no larger than `getMaxWriteBatchSize()`, already validated and converted into `getPreferredWriteUnit(HealthDataType)`.
  `final AsyncResource<Integer>`
  `drainChanges()`
  Drains pending changes for every active subscription, resolving with the number of batches delivered.
  `protected final int`
  `fireChanges(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 -- see `HealthRequestStatus`.
  `int`
  `getMaxWriteBatchSize()`
  The largest number of samples one platform write call accepts.
  `protected final int`
  `getOperationTimeout()`
  The current per-operation safety timeout.
  `HealthUnit`
  `getPreferredWriteUnit(HealthDataType type)`
  The unit this platform prefers to receive `type` in.
  `HealthAuthorizationStatus`
  `getReadAuthorizationStatus(HealthDataType type)`
  This app's read authorization for `type`.
  `final List<HealthSubscription>`
  `getSubscriptions()`
  Every currently registered subscription.
  `List<AggregateMetric>`
  `getSupportedMetrics(HealthDataType type)`
  The metrics this platform can compute natively for `type`.
  `List<HealthDataType>`
  `getSupportedTypes()`
  The types this platform can read.
  `HealthAuthorizationStatus`
  `getWriteAuthorizationStatus(HealthDataType type)`
  This app's write authorization for `type`.
  `final AsyncResource<Boolean>`
  `hasAnyData(HealthDataType type, HealthTimeRange range)`
  Runs a bounded query and reports whether any sample came back.
  `boolean`
  `isBackgroundDeliverySupported()`
  Whether this platform can deliver changes without the app asking.
  `boolean`
  `isDeletable(HealthDataType type)`
  `boolean`
  `isPushDelivery()`
  Whether the OS wakes the app on new data -- see `HealthSubscription.isPushDelivery()`.
  `boolean`
  `isSourceDeduplicationSupported()`
  Whether samples of `type` can be deleted.
  `boolean`
  `isSupported()`
  `true` when this store can do anything at all.
  `boolean`
  `isTypeSupported(HealthDataType type)`
  Whether this platform exposes `type`.
  `boolean`
  `isWritable(HealthDataType type)`
  Whether this app may write `type`.
  `protected final HealthAnchor`
  `loadAnchor(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 void`
  `restoreSubscriptions()`
  Re-registers every subscription persisted by a previous launch.
  `protected final boolean`
  `seedAnchor(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 void`
  `setAuthorizationTimeout(int millis)`
  Sets the authorization safety timeout in milliseconds.
  `static void`
  `setBackgroundListenerFactory(HealthBackgroundListenerFactory factory)`
  Installs the build-generated factory that constructs background listeners after a process relaunch.
  `protected final void`
  `setOperationTimeout(int millis)`
  Sets the per-operation safety timeout in milliseconds.
  `protected final void`
  `storeAnchor(String subscriptionId, HealthAnchor anchor)`
  Persists a cursor.
  `final HealthSubscription`
  `subscribe(SubscriptionRequest request, HealthChangeListener listener)`
  Subscribes with an in-memory listener, dropped when the process ends.
  `final HealthSubscription`
  `subscribe(SubscriptionRequest request, Class backgroundListenerClass)`
  Subscribes with a listener that survives the app being killed.
  `final void`
  `unsubscribe(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 to `getMaxWriteBatchSize()`.

  ### Methods inherited from class Object

  `clone, equals, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait`

- ## Field Details

  - ### DEFAULT_OPERATION_TIMEOUT

    protected static final int DEFAULT_OPERATION_TIMEOUT
    Default per-operation safety timeout for reads and writes.
    See Also:
      - Constant Field Values

  - ### 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:
      - Constant Field Values

- ## 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.

  - ### isSourceDeduplicationSupported

    public boolean isSourceDeduplicationSupported()

    Whether samples of `type` can 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

    public boolean isDeletable(HealthDataType type)

  - ### 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.

    Answers `HealthRequestStatus.UNKNOWN` on both phones in this release. Neither port implements the query yet -- HealthKit's `getRequestStatusForAuthorization` and a Health Connect grant comparison are what would answer it -- so the documented `SHOULD_REQUEST` and `UNNECESSARY` answers 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 treat `UNKNOWN` as "ask anyway": requesting authorization that is already granted is harmless on both platforms.

  - ### 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 caller)
    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.

  - ### armTimeout

    protected final void armTimeout(AsyncResource resource)

    Fails `resource` with `HealthError.TIMEOUT` if 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

    protected final void armTimeout(AsyncResource resource, int millis)

  - ### 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()

    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 `HKObserverQuery` either, 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

    protected final int 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.

    ###### Returns

    How many deliveries were queued -- `0` when nothing was, and more than one when the subscription's per-batch cap split the page. Ports add this to the count they resolve `drainChanges` with, 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

    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.

  - ### seedAnchor

    protected final boolean seedAnchor(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.

    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.

  - ### storeAnchor

    protected final void storeAnchor(String subscriptionId, HealthAnchor anchor)
    Persists a cursor. Called by `fireChanges(HealthChangeBatch)` after successful delivery; ports rarely need it directly.

  - ### clearAnchor

    protected final void clearAnchor(String subscriptionId)

    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 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.

  - ### 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

    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.

    No mobile port overrides this yet, so both phones answer `UNKNOWN`; the public method says so. HealthKit answers it through `getRequestStatusForAuthorization`, 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

    protected void doSubscribe(SubscriptionRequest request, HealthSubscription subscription)

    Registers a platform observer for `subscription`, resuming from `HealthSubscription.getAnchor()` when a cursor was persisted. Called on registration and again on `restoreSubscriptions()`.

    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

    protected void doUnsubscribe(HealthSubscription subscription)

    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

    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.