> For the complete documentation index, see [llms.txt](https://docs.tryterra.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tryterra.co/mobile-sdks/android-sdk-2.md).

# Android SDK

Install the terra-android SDK, connect Health Connect or Samsung Health, and keep their data flowing to Terra in the background.

This page shows how to use `terra-android` 2.x in your Android app: install it, build the client, connect a user, request permissions and set up background delivery.

The SDK reads the health store, uploads it to Terra in the background, and delivers it to your [data destination](/destinations/setting-up-data-destinations.md) like any other provider. It supports:

* Health Connect
* Samsung Health, through Health Connect or directly with the Samsung add-on. See [Samsung Health](/mobile-sdks/samsung-health.md).

{% hint style="info" %}
This page covers `terra-android` 2.x, whose Kotlin package is `co.tryterra.sdk`. For 1.x, read [Android SDK 1.x](/mobile-sdks/android-kotlin.md). To move an existing app, read [Android upgrade guide](/mobile-sdks/android-migrating-to-2.md).
{% endhint %}

## Before you begin

* `minSdk` 28 (Android 9) or later. The Samsung Health add-on needs 29 (Android 10).
* `compileSdk` 36 or later and Android Gradle Plugin 8.9.1 or later, required by the SDK's Health Connect dependency.
* Health Connect on the device.
* A backend endpoint that mints Terra auth tokens. See [Generate an auth token](#generate-an-auth-token).

## Install the SDK

### Add the dependency

* Inside the dependencies section of your `build.gradle.kts` file, add the following line of code.

{% code title="app/build.gradle.kts" %}

```kotlin
dependencies {
    implementation("co.tryterra:terra-android:2.2.1")
}
```

{% endcode %}

* You can find the latest version number on [Maven Central](https://central.sonatype.com/artifact/co.tryterra/terra-android/).

### Sync your project

Sync your project with Gradle by clicking on the "**Sync Project with Gradle Files**" button in the toolbar. This downloads the SDK and makes it available to your project.

{% hint style="danger" %}
**Do not add `terra-android` 2.x to an app that still uses the 1.x React Native or Flutter wrapper (`terra-react` or `terra-flutter`).**
{% endhint %}

### Configure health permissions

The SDK's own manifest declares no health permission and no component. Your app declares exactly what it uses.

{% tabs %}
{% tab title="Health Connect" %}
**1. Declare the Health Connect permissions**

For each category your app requests, declare every permission in its row below, and nothing else. Health Connect never grants a permission that is missing from your manifest, and Google Play rejects apps that declare data types they do not use. A permission shared by two of your categories is declared once.

| Category       | Permissions (each prefixed with `android.permission.health.`)                                                                                                                                                                                                                                                                                                                                  |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DAILY`        | `READ_STEPS`, `READ_DISTANCE`, `READ_FLOORS_CLIMBED`, `READ_ELEVATION_GAINED`, `READ_ACTIVE_CALORIES_BURNED`, `READ_TOTAL_CALORIES_BURNED`, `READ_BASAL_METABOLIC_RATE`, `READ_HEART_RATE`, `READ_RESTING_HEART_RATE`, `READ_HEART_RATE_VARIABILITY`                                                                                                                                           |
| `ACTIVITY`     | `READ_EXERCISE`, `READ_STEPS`, `READ_DISTANCE`, `READ_SPEED`, `READ_POWER`, `READ_FLOORS_CLIMBED`, `READ_ELEVATION_GAINED`, `READ_ACTIVE_CALORIES_BURNED`, `READ_TOTAL_CALORIES_BURNED`, `READ_BASAL_METABOLIC_RATE`, `READ_HEART_RATE`, `READ_HEART_RATE_VARIABILITY`                                                                                                                         |
| `SLEEP`        | `READ_SLEEP`, `READ_HEART_RATE`, `READ_RESTING_HEART_RATE`, `READ_HEART_RATE_VARIABILITY`, `READ_OXYGEN_SATURATION`, `READ_RESPIRATORY_RATE`                                                                                                                                                                                                                                                   |
| `BODY`         | `READ_WEIGHT`, `READ_HEIGHT`, `READ_BODY_FAT`, `READ_LEAN_BODY_MASS`, `READ_BONE_MASS`, `READ_BODY_WATER_MASS`, `READ_BODY_TEMPERATURE`, `READ_BASAL_BODY_TEMPERATURE`, `READ_SKIN_TEMPERATURE`, `READ_BLOOD_GLUCOSE`, `READ_BLOOD_PRESSURE`, `READ_OXYGEN_SATURATION`, `READ_VO2_MAX`, `READ_RESPIRATORY_RATE`, `READ_BASAL_METABOLIC_RATE`, `READ_HEART_RATE`, `READ_HEART_RATE_VARIABILITY` |
| `NUTRITION`    | `READ_NUTRITION`, `READ_HYDRATION`                                                                                                                                                                                                                                                                                                                                                             |
| `MENSTRUATION` | `READ_MENSTRUATION`, `READ_INTERMENSTRUAL_BLEEDING`, `READ_OVULATION_TEST`, `READ_CERVICAL_MUCUS`, `READ_SEXUAL_ACTIVITY`                                                                                                                                                                                                                                                                      |

For example, an app that requests only `DAILY` declares:

{% code title="AndroidManifest.xml" lineNumbers="true" %}

```xml
<uses-permission android:name="android.permission.health.READ_STEPS" />
<uses-permission android:name="android.permission.health.READ_DISTANCE" />
<!-- ...and the other eight DAILY permissions -->

<!-- Optional: keep syncing while the app is closed -->
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND" />
<!-- Optional: read more than 30 days back on the first sync -->
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_HISTORY" />
```

{% endcode %}

[`HealthConnectPermissions.readPermissions(categories)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#healthconnectpermissions.readpermissions) returns the same list, for a test that keeps your manifest in step with the categories you request.

If you write to Health Connect, [`HealthConnectPermissions.writePermissions`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#healthconnectpermissions.writepermissions) is the matching write set. Writing `ACTIVITY` needs `WRITE_EXERCISE`, `WRITE_ACTIVE_CALORIES_BURNED` and `WRITE_DISTANCE`, because a workout's energy and distance are separate records in Health Connect.

**2. Add the privacy policy screen, declared twice**

Health Connect opens your privacy policy screen with a different action below and above Android 14, so declare both:

{% code title="AndroidManifest.xml" lineNumbers="true" %}

```xml
<activity android:name=".PermissionsRationaleActivity" android:exported="true">
    <intent-filter>
        <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
    </intent-filter>
</activity>

<activity-alias
    android:name=".ViewPermissionUsageActivity"
    android:exported="true"
    android:targetActivity=".PermissionsRationaleActivity"
    android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
    <intent-filter>
        <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
        <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
    </intent-filter>
</activity-alias>
```

{% endcode %}

Without the alias, Android 14 and above refuse every read with "Incorrect health permission state", including reads that were granted. The screen must show the same privacy policy as your Play listing.

**3. Allow network access and Health Connect visibility**

The SDK needs `INTERNET` to link and upload. On Android 13 and below, Health Connect is a separate app, and without the package query the SDK cannot see it and reports it as unavailable.

{% code title="AndroidManifest.xml" lineNumbers="true" %}

```xml
<uses-permission android:name="android.permission.INTERNET" />

<queries>
    <package android:name="com.google.android.apps.healthdata" />
</queries>
```

{% endcode %}

{% hint style="info" %}
WorkManager, which the SDK uses, merges `WAKE_LOCK`, `ACCESS_NETWORK_STATE`, `RECEIVE_BOOT_COMPLETED` and `FOREGROUND_SERVICE` into your manifest. None of them needs a Play declaration. Do not remove them.
{% endhint %}

{% hint style="warning" %}
**Apply for Health Connect access.** Before release, submit the Health Connect declaration in the Play Console. Select only the health features your permissions support, and give one sentence per data type on how it benefits the user.
{% endhint %}
{% endtab %}

{% tab title="Samsung Health" %}
Samsung Health's data reaches the SDK through Health Connect once the person lets Samsung Health share with Health Connect. Follow the Health Connect tab for that.

To read Samsung Health's own store directly, add the `terra-android-samsung` add-on. It needs Samsung's partner approval and Samsung's own SDK, which your app packages. Follow [Samsung Health](/mobile-sdks/samsung-health.md) for the approval, the dependencies and the store registration.
{% endtab %}
{% endtabs %}

## Initialize the SDK

The first step is to **build the Terra client.**

Build it **once**, in `Application.onCreate`, and keep it for the life of the process. Background runs start your process with no activity in it, so a client built in a screen does not exist when they arrive.

### Import `Terra` and `BackgroundSync`

{% code title="YourApp.kt" lineNumbers="true" %}

```kotlin
import co.tryterra.sdk.Terra
import co.tryterra.sdk.background.BackgroundSync
```

{% endcode %}

### Build the Terra client

Call [`Terra(...)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#terra.invoke) with the following arguments:

* `context`: Any context. The SDK keeps the application context.
* `devId`: Your **Developer ID** provided by Terra.
* `tokenProvider`: A suspend function that returns a **one-time authentication token** from your backend. See [Generate an auth token](#generate-an-auth-token) below.

Then register background sync with [`BackgroundSync.register`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#backgroundsync.register). See [Set up background delivery](#set-up-background-delivery).

{% code title="YourApp.kt" lineNumbers="true" %}

```kotlin
import android.app.Application
import android.util.Log
import co.tryterra.sdk.Terra
import co.tryterra.sdk.background.BackgroundSync

class YourApp : Application() {
    lateinit var terra: Terra

    override fun onCreate() {
        super.onCreate()
        terra = Terra(
            this,
            devId = BuildConfig.TERRA_DEV_ID,
            tokenProvider = { yourBackend.mintTerraToken() },
        )
        if (!BackgroundSync.register(this, terra)) {
            Log.w("Terra", "Background sync was refused; nothing is scheduled")
        }
    }
}
```

{% endcode %}

{% hint style="warning" %}
Name the `tokenProvider` argument. The factory's last parameter is `stores`, so a trailing lambda binds there instead, and the client has no token provider at all.
{% endhint %}

## Connect a user

Once the client exists, you can create a connection by calling `connect()`, then request permissions with `permissions.request()` to show the permission screen.

### Call `connect()`

Call [`connect()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#terra.connect) with the following arguments:

* `source`: `Source.HEALTH` for Health Connect, or `Source.SAMSUNG_HEALTH` for Samsung Health directly, with the add-on.
* `forUser`: The person the connection is for. [`EndUser.Reference(id)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#enduser) with the same value your backend sends as `reference_id`, or `EndUser.Anonymous` if your backend sends none.
* `token` (optional): A token you already hold, for example from a QR code. A token passed this way always creates a fresh link.

`connect` returns the connection. [`connection.userId`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#storedconnection) is the Terra `user_id` that appears on your webhooks.

### Call `permissions.request()`

Call [`permissions.request()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#permissions.request) with the categories to read and write, and a [`PermissionLauncher`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#permissionlauncher) to present the sheet. The categories are `DAILY`, `BODY`, `SLEEP`, `ACTIVITY`, `NUTRITION` and `MENSTRUATION`. `connect` never shows a permission screen on its own.

Create the launcher in `onCreate`, before anything that suspends: Android refuses the registration once the activity is started.

{% tabs %}
{% tab title="Health Connect" %}
{% code title="ConnectActivity.kt" lineNumbers="true" %}

```kotlin
import co.tryterra.sdk.DataCategory
import co.tryterra.sdk.EndUser
import co.tryterra.sdk.Source
import co.tryterra.sdk.permissions.PermissionLauncher

class ConnectActivity : ComponentActivity() {
    private val terra get() = (application as YourApp).terra
    private lateinit var launcher: PermissionLauncher

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        launcher = terra.permissions.launcher(this)

        connectButton.setOnClickListener {
            lifecycleScope.launch {
                val connection = terra.connect(Source.HEALTH, EndUser.Reference(signedInUser.id))
                Log.i("Terra", "user_id ${connection.userId}")
                val granted = terra.permissions.request(
                    read = setOf(DataCategory.DAILY, DataCategory.SLEEP, DataCategory.ACTIVITY),
                    launcher = launcher,
                )
            }
        }
    }

    override fun onDestroy() {
        launcher.unregister()
        super.onDestroy()
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Samsung Health" %}
Follow [Samsung Health](/mobile-sdks/samsung-health.md) first. Samsung Health has its own permission sheet, so it has its own launcher, reached through [`terra.on(Source.SAMSUNG_HEALTH)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#terra.on).

{% code title="ConnectActivity.kt" lineNumbers="true" %}

```kotlin
// in onCreate
samsungLauncher = terra.on(Source.SAMSUNG_HEALTH).permissions.launcher(this)

// on tap
terra.connect(Source.SAMSUNG_HEALTH, EndUser.Reference(signedInUser.id))
terra.on(Source.SAMSUNG_HEALTH).permissions.request(
    read = setOf(DataCategory.DAILY, DataCategory.SLEEP, DataCategory.ACTIVITY),
    launcher = samsungLauncher,
)
```

{% endcode %}
{% endtab %}
{% endtabs %}

### What `permissions.request()` returns

What the sheet granted, narrowed to this call. Check it rather than assuming the person said yes.

When the request reads anything, the sheet also asks for background read and history read, if your manifest declares them.

{% hint style="info" %}
**Request `DAILY` even if you show no step data.** Heart rate is uploaded on the daily route.
{% endhint %}

### The same person keeps their link

A stored link is restored only for the person it was made for. A normal launch for the same person restores it with no network call and never calls `tokenProvider`. When someone else signs in on the same phone, the SDK retires the old link the way [`terra.disconnect`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#terra.disconnect) does and links again, so their data does not arrive under the first person's `reference_id`. A link made before connections recorded their end user is kept and recorded as this person's.

The SDK stores a salted hash of the reference, never the reference itself. Terra still trusts only the reference inside the token, so mint the token for the same person you pass to `connect`.

### Write permissions and workout scopes

`write = setOf(DataCategory.ACTIVITY)` asks for the exercise session, active calories and distance. If your app logs workouts without energy or distance, leave `ACTIVITY` out of `write` and pass [`WorkoutScope`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#workoutscope)s in `workouts` instead. The sheet then asks only for what the scopes name:

{% code title="ConnectActivity.kt" lineNumbers="true" %}

```kotlin
terra.permissions.request(
    read = setOf(DataCategory.DAILY, DataCategory.SLEEP, DataCategory.ACTIVITY),
    workouts = setOf(WorkoutScope(WorkoutActivity.Running, distance = true)),
    launcher = launcher,
)
```

{% endcode %}

[`writeStatus(ACTIVITY)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#permissions.writestatus) is `GRANTED` only when every type you asked to write there is granted. A workout `log` checks only the grants its entries use, so a run without energy can still log while `writeStatus(ACTIVITY)` is `DENIED`.

### Check permissions without prompting

{% code lineNumbers="true" %}

```kotlin
terra.permissions.status(DataCategory.SLEEP)    // GRANTED, DENIED, NOT_DETERMINED, ...
terra.permissions.writeStatus(DataCategory.ACTIVITY)
terra.permissions.backgroundReadStatus()
terra.permissions.historyReadStatus()
terra.permissions.requested()                   // everything asked for so far
```

{% endcode %}

GPS routes need `READ_EXERCISE_ROUTES`, which Health Connect only grants in Settings or through its own route consent screen. The SDK never asks for it. If your app holds it and requests `ACTIVITY`, routes are uploaded with workouts. In the background they are usually withheld unless the app also holds background read.

## Generate an auth token

`tokenProvider` needs a **single-use token** to link the device to Terra. It is called only while `connect` makes a new link:

* the first time the device links,
* after a reinstall, or anything else that cleared the app's stored link,
* when a different person connects than the one the stored link was made for,
* when Terra no longer accepts the device's key and the SDK has to link again.

Generate the token with `POST https://api.tryterra.co/v2/auth/generateAuthToken` from **your backend server**, with your Terra `x-api-key` and `dev-id` in the headers and your own user identifier as `reference_id`. Then return it to your app from your own API.

{% code title="Your backend" %}

```bash
curl --request POST \
  --url 'https://api.tryterra.co/v2/auth/generateAuthToken?reference_id=<YOUR_USER_ID>' \
  --header 'dev-id: <YOUR_DEV_ID>' \
  --header 'x-api-key: <YOUR_API_KEY>'
```

{% endcode %}

{% hint style="warning" %}

* Tokens expire after three minutes and are spent on first use.
* **DO NOT** expose your API key in your mobile app. Make this call **only** from your **backend**.
  {% endhint %}

## Validate the connection

To check that a connection is still working on the client side, use [`diagnostics.state()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#diagnostics.state). [`terra.connections()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#terra.connections) lists the connections on the device, with the `userId` of each.

{% code title="YourApp.kt" lineNumbers="true" %}

```kotlin
when (val state = terra.diagnostics.state()) {
    is DiagnosticsState.LinkRevoked -> Unit                    // connect again with the same end user
    is DiagnosticsState.BackgroundRestricted -> state.reason   // OEM_RESTRICTED, BACKGROUND_READ_DENIED, TICKS_NOT_RUNNING, ...
    is DiagnosticsState.BackgroundRefreshUnscheduled -> Unit   // register was never called
    is DiagnosticsState.Quiet -> state.since
    else -> Unit
}
```

{% endcode %}

{% hint style="info" %}
**Always validate the connection on app start.** If `diagnostics.state()` is `LinkRevoked`, Terra stopped accepting this device's key, for example because another device linked the same user. Call `connect(Source.HEALTH, forUser)` again with the same end user. The token provider is called to link again.

When the SDK gives up on something, it sends a redacted report to Terra so support can see the device's side. The report never contains a health value.
{% endhint %}

## Set up background delivery

Background delivery allows data to be synced even when your app is not in the foreground. There is no call that starts a sync: after the permission sheet is answered, the SDK uploads on its own, in the foreground and in the background.

[`BackgroundSync.register`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#backgroundsync.register) from [Initialize the SDK](#initialize-the-sdk) schedules it. Call it once, from `Application.onCreate`. Calling it again from another launch path costs nothing.

* `context`: Any context. The SDK keeps the application context.
* `terra`: The client to run on each background tick.
* `processExit` (optional): [`ProcessExitPolicy`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#processexitpolicy). Defaults to `KEEP_PROCESS`. `EXIT_WHEN_IDLE` ends your process after a background tick when nothing else is running in it, which stops some OEM battery managers from freezing it. Use it only if your process exists to sync: the SDK cannot see push handlers, bound services or content providers you run.

It returns `false` when another client with the same dev ID already owns background sync and does not serve this client's health stores. Nothing is scheduled in that case, so check the return value.

What it schedules:

* A periodic run every 15 minutes, the platform's minimum, whenever the device has a network connection.
* A run each time the app comes to the foreground, at most once every 15 minutes.
* Each run stops itself after 8 minutes and leaves the rest for the next one.

Declare `READ_HEALTH_DATA_IN_BACKGROUND` in your manifest so the permission sheet asks for background read. Without it granted, background runs read nothing and `diagnostics.state()` reports `BackgroundRestricted(BACKGROUND_READ_DENIED)`.

### OEM battery restrictions

{% hint style="warning" %}
Some manufacturers stop background work regardless of the permissions an app holds. On Xiaomi, Redmi and POCO phones, for example, apps install with Autostart off, and swiping the app away cancels every scheduled job until the app is opened again. The phone needs both Autostart **on** and the battery setting at **No restrictions**.

Show the person one screen after they connect, with a button to the right settings page:

```kotlin
BackgroundSetup.oemSetupIntents(context).firstOrNull()?.let { activity.startActivity(it) }
```

The list is most specific first, and only contains screens that exist on the device. Start them from an `Activity`. Do not declare `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` or `QUERY_ALL_PACKAGES`: Play restricts both, and the SDK does not need them.
{% endhint %}

From here, health data events reach your data destination, such as a webhook, automatically.

You can also [request historical data](/unified-api/requesting-historical-data.md) from your backend to backfill.

Check out the [Android SDK reference](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2) for details about all the functions in the SDK.

## What syncs and when

The SDK decides when to upload:

| Trigger          | When                                                                               |
| ---------------- | ---------------------------------------------------------------------------------- |
| Permission grant | Straight after a `permissions.request` that grants a category with an upload route |
| Foreground       | Each time the app comes to the foreground, at most once every 15 minutes           |
| Background       | The periodic WorkManager run, every 15 minutes at best                             |
| Relink           | After `connect` creates a new link, if the app had already requested access        |

`connect` does not upload anything. On a first link nothing has been requested yet, so no data moves until the permission sheet is answered. When the app already had access, a run starts after the link, and `connect` returns without waiting for it.

### How far back

The first sync reaches two years (730 days) back, newest first: today arrives in the first upload, and the history fills in behind it across as many runs as it takes. Health Connect only serves the last 30 days unless the app holds `READ_HEALTH_DATA_HISTORY`.

After that, every run uploads what changed since the last successful one. The SDK reads Health Connect's change log, which lists what was **written** since the last sync, not the date the data is about. A workout added today for last Tuesday still syncs, and the day it belongs to is uploaded again.

To backfill further, [request historical data](/unified-api/requesting-historical-data.md) for the user from your backend with `to_webhook=true`. Terra queues the window for the device, and the next run uploads it.

### What is uploaded

| Category       | What is uploaded                                                                                                                                                                                                                                        |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DAILY`        | Steps, heart rate, resting heart rate, active energy, distance, floors and day totals, as whole local days                                                                                                                                              |
| `SLEEP`        | Sleep sessions and stages                                                                                                                                                                                                                               |
| `ACTIVITY`     | Workouts with stats, laps and segments, speed, power and cadence, and GPS routes when the route permission is held                                                                                                                                      |
| `BODY`         | Weight, height, body fat, lean mass, bone mass, body water mass, basal metabolic rate, blood pressure, blood glucose, oxygen saturation, body, basal and skin temperature, HRV (RMSSD), respiratory rate and VO2 max. Heart rate rides the daily upload |
| `NUTRITION`    | Nutrition records and hydration                                                                                                                                                                                                                         |
| `MENSTRUATION` | Menstrual flow, and cycle events such as ovulation tests, cervical mucus, intermenstrual bleeding and sexual activity                                                                                                                                   |

Health Connect has no record for the athlete profile, so Android uploads none.

A run only uploads the categories your app requested. A permission another part of your app holds is not uploaded unless this client asked for its category.

The SDK never reports deletions. A record deleted in Health Connect stays in Terra. To remove a user's data, deauthenticate the user from your backend.

## Read and write on the device

Local reads go to Health Connect and never touch the network:

{% code lineNumbers="true" %}

```kotlin
terra.body.heartRate(TimeRange.LastHours(6))
terra.daily.steps(TimeRange.Today)
terra.sleep.segments(TimeRange.LastDays(7))
terra.activity.workouts(TimeRange.Between(from, to))
```

{% endcode %}

Reading a category your app has not requested throws [`TerraError.AuthorizationNotDetermined`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#terraerror) rather than returning an empty list. [`daily.steps`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#dailymetrics.steps) returns one total per day per writing app: do not add them up.

These reads need SDK 2.2.1 or later. Each takes a `TimeRange` and checks the category in the last column:

| Read                                                                                                                                           | Returns                                                                                                                               | Category            |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| [`daily.totals`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#dailymetrics.totals)                   | Health Connect's own total for each local day: walking and running distance, active and basal energy, and floors climbed              | `DAILY`             |
| [`daily.intraday`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#dailymetrics.intraday)               | The active energy, distance and floors records those totals are made of                                                               | `DAILY`             |
| [`body.restingHeartRate`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#bodymetrics.restingheartrate) | Health Connect's resting heart rate                                                                                                   | `DAILY`, not `BODY` |
| [`body.readings`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#bodymetrics.readings)                 | Weight, height, body fat, lean mass, HRV, VO2 max, blood pressure, blood glucose, oxygen saturation, temperature and respiratory rate | `BODY`              |
| [`activity.routes`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#activitymetrics.routes)             | Each session's GPS track, where the person allowed it                                                                                 | `ACTIVITY`          |
| [`activity.intensity`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#activitymetrics.intensity)       | Speed, power and cycling cadence. Health Connect does not link them to a session                                                      | `ACTIVITY`          |
| [`nutrition.quantities`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#nutritionmetrics.quantities)   | One value per nutrient a meal recorded                                                                                                | `NUTRITION`         |
| [`nutrition.hydration`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#nutritionmetrics.hydration)     | Water, one record per drink                                                                                                           | `NUTRITION`         |
| [`menstruation.flow`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#menstruationmetrics.flow)         | One flow value per logged day                                                                                                         | `MENSTRUATION`      |
| [`menstruation.events`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#menstruationmetrics.events)     | Ovulation tests, cervical mucus and intermenstrual bleeding                                                                           | `MENSTRUATION`      |

Except `daily.totals`, each of these reads asks Health Connect only for its own record types, so a refused grant throws `TerraError.PermissionDenied` even when another type in the same category is allowed. `daily.totals` leaves a refused metric out instead. In 2.2.1 and later, a [`Workout`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#workout) also carries `title`, and [`terra.availability()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#terra.availability) answers whether Health Connect is installed and current, with no connection needed.

{% code lineNumbers="true" %}

```kotlin
if (terra.availability() != StoreAvailability.Available) return

val totals = terra.daily.totals(TimeRange.LastDays(7))
val readings = terra.body.readings(TimeRange.LastDays(30))
val meals = terra.nutrition.quantities(TimeRange.Today).groupBy { it.foodCorrelationId }
val flow = terra.menstruation.flow(TimeRange.LastDays(90))
```

{% endcode %}

To write data your app measured itself, use `log` on the `activity`, `body`, `daily`, `nutrition` and `sleep` namespaces, and `delete` with the handle it returns. Deleting a workout also deletes the active calories and distance logged with it.

{% hint style="warning" %}
If the person revokes a write grant after your app used it, a correction that drops that field and a `delete` both succeed, but the earlier record of that type stays in Health Connect until the grant is restored. For example, revoking the active calories grant after a run with energy was logged leaves that energy record behind when the run is deleted.
{% endhint %}

## Disconnect a user

{% code lineNumbers="true" %}

```kotlin
terra.disconnect(Source.HEALTH)
```

{% endcode %}

`disconnect` forgets the link on the device and tells Terra to stop accepting its key. To remove the user from Terra, call [deauthenticateUser](https://docs.tryterra.co/reference/health-and-fitness-api/readme#delete-auth-deauthenticateuser) from your backend.

## Troubleshooting

| Symptom                                                                                                                                                            | Cause                                                                                              | Fix                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| App crashes with missing `co.tryterra.terra` classes                                                                                                               | `terra-react` or `terra-flutter` 1.x and `terra-android` 2.x in one app                            | Remove `terra-android` 2.x until you move off the old wrapper                                                                                                                                                    |
| `connect` throws `TokenRejected`                                                                                                                                   | The token expired (three minutes) or was already used                                              | Mint a fresh token for every link                                                                                                                                                                                |
| `connect` or `permissions.request` throws `HealthDataUnavailable`                                                                                                  | Health Connect is missing or needs an update                                                       | Send the person to install or update Health Connect from Google Play                                                                                                                                             |
| [`permissions.launcher`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#permissions.launcher) throws `TerraError.Platform` | It was called after the activity started, or after it was destroyed                                | Call `permissions.launcher(this)` in `onCreate`, before anything that suspends, `connect` included                                                                                                               |
| Every read fails with "Incorrect health permission state"                                                                                                          | The Android 14 rationale `activity-alias` is missing                                               | Add both rationale declarations from [Configure health permissions](#configure-health-permissions)                                                                                                               |
| `BackgroundRestricted(OEM_RESTRICTED)`                                                                                                                             | The manufacturer's battery manager is stopping background work                                     | Show [`BackgroundSetup.oemSetupIntents`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#backgroundsetup.oemsetupintents) to the person                                   |
| `BackgroundRestricted(TICKS_NOT_RUNNING)`                                                                                                                          | Background runs are scheduled but have not run for 48 hours                                        | Usually an OEM restriction. Same fix                                                                                                                                                                             |
| [`BackgroundRefreshUnscheduled`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#diagnosticsstate)                          | `BackgroundSync.register` was never called, or returned `false`                                    | Call it from `Application.onCreate` and check its return value                                                                                                                                                   |
| `LinkRevoked`                                                                                                                                                      | Terra stopped accepting this device's key, for example because another device linked the same user | Call `connect(Source.HEALTH, forUser)` again with the same end user. The token provider is called to link again                                                                                                  |
| First sync only reaches 30 days                                                                                                                                    | `READ_HEALTH_DATA_HISTORY` not declared or not granted                                             | Declare it, and check [`permissions.historyReadStatus()`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#permissions.historyreadstatus)                                  |
| A category is missing in Terra                                                                                                                                     | The app never requested it, its permission is not in the manifest, or the person did not grant it  | Check [`permissions.status(category)`](https://docs.tryterra.co/reference/health-and-fitness-api/sdk-references/android-sdk-2#permissions.status), and declare every permission in its row of the manifest table |

{% hint style="info" %}
**The mobile SDKs are only for Apple Health, Health Connect and Samsung Health.** These have no web API, so your app reads them on the phone and sends the data to Terra. Every other provider connects through the [Unified API](https://docs.tryterra.co/unified-api/getting-started) with no SDK.
{% endhint %}

## Next steps

* [Samsung Health](/mobile-sdks/samsung-health.md): read Samsung Health directly with the add-on.
* [Health data events](/unified-api/receiving-data-updates.md): handle the data the SDK uploads.
* [Historical data requests](/unified-api/requesting-historical-data.md): backfill beyond the first sync.
