> ## Documentation Index
> Fetch the complete documentation index at: https://developer.neurofit.app/llms.txt
> Use this file to discover all available pages before exploring further.

# API reference (Android)

> Every public type in the com.neurofit.vitals Kotlin package, version 1.0.0.

Package `com.neurofit.vitals`, Maven `com.neurofit:vitals-sdk:1.0.0`, `minSdk 28`, `compileSdk 36`, JVM 17,
Kotlin `apiVersion` and `languageVersion` 2.0, explicit API mode.

Dependencies declared `api`, so your app compiles against them without declaring them:
`androidx.camera:camera-view:1.5.0`, `androidx.activity:activity-ktx:1.11.0`,
`androidx.lifecycle:lifecycle-runtime-ktx:2.9.4` and `org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2`.
Declared `implementation` (runtime only): `androidx.camera:camera-core`, `camera-camera2` and
`camera-lifecycle`, all 1.5.0.

The API is deliberately small: `VitalsSDK` for setup, `VitalsSession` for one reading, eight events and the value
types they carry. There is no configuration. Every session runs the validated 60-second reading with the same
settings the NEUROFIT app uses. The [iOS page](/api-reference-ios) documents the same API in Swift.

Java callers: every `VitalsSDK` member is `@JvmStatic` (`VitalsSDK.activate(context, key)`; the
`VitalsSDK.INSTANCE` form works too). `events` is a Kotlin `Flow`, so collect it from Kotlin code.

## VitalsSDK

```kotlin theme={null}
public object VitalsSDK {
  /** The SDK version, "1.0.0". Also stamped on every result as VitalsResult.sdkVersion. */
  public val version: String

  /** Verifies licenseKey offline against the public key compiled into the SDK. */
  @Throws(VitalsException::class)   // VitalsException.LicenseInvalid
  public fun activate(context: Context, licenseKey: String)

  /** Whether this device can take a reading. Reads the camera's characteristics only; does not open it. */
  public fun deviceSupport(context: Context): DeviceSupport

  /** Whether android.permission.CAMERA is granted. Never prompts. */
  public fun hasCameraPermission(context: Context): Boolean

  /** Requests android.permission.CAMERA. Main thread. onResult runs on the main thread. */
  public fun requestCameraPermission(activity: ComponentActivity, onResult: (Boolean) -> Unit)

  /** Creates a session for one reading. */
  @Throws(VitalsException::class)   // NotActivated, UnsupportedDevice
  public fun createSession(context: Context): VitalsSession
}
```

Notes:

* `activate` checks the key's `apps` against the application id (`context.packageName`). A debuggable build
  (`ApplicationInfo.FLAG_DEBUGGABLE`) accepts a key that does not list its application id, so you can run debug
  flavours under other ids; a release build rejects it. See [License keys](/license-keys).
* `requestCameraPermission` registers its own launcher in the activity's `ActivityResultRegistry`, so there is
  nothing to declare in your activity. When the permission is already granted, `onResult(true)` runs
  synchronously and nothing is launched. The launcher belongs to that activity instance: if the activity is
  recreated while the system dialog shows (a rotation, a split-screen resize), `onResult` is never called. Lock the
  orientation of the screen that asks and also check `hasCameraPermission` in `onResume`.

## DeviceSupport

```kotlin theme={null}
public data class DeviceSupport(
  public val isSupported: Boolean,
  /** Why isSupported is false; null when supported. A short English string for your logs. */
  public val reason: String?,
)
```

A device is supported when it has a rear camera that offers a 30 fps capture rate. A torch is used when present
but not required. The `reason` values are listed on [Device support](/device-support).

## VitalsSession

One session runs one reading. Create a new session for each reading screen with `VitalsSDK.createSession`.

```kotlin theme={null}
public interface VitalsSession {
  /** Every event, in order, emitted on the SDK's own thread. Subscribe before start(); no replay. */
  public val events: Flow<VitalsEvent>

  /** While false, positioning continues (camera, torch and guidance) but no reading starts. Default true. */
  public var isAutoStartEnabled: Boolean

  /** Binds the camera to lifecycleOwner, turns the torch on and starts positioning. Main thread. */
  @Throws(VitalsException::class)
  public fun start(lifecycleOwner: LifecycleOwner, previewView: PreviewView? = null)

  /** Releases the camera and torch and ends the session. Idempotent. Any thread. */
  public fun stop()

  /** Cancels the reading in progress with Cancelled(CANCELLED_BY_APP). No-op unless measuring. Any thread. */
  public fun cancelReading()
}
```

`start(lifecycleOwner, previewView)` throws:

| Exception | When |
| - | - |
| `InternalError("start() must be called on the main thread")` | Called off the main thread. Nothing is touched. |
| `NotActivated` | `VitalsSDK.activate` has not succeeded. |
| `CameraPermissionDenied` | `android.permission.CAMERA` is not granted. |
| `UnsupportedDevice(reason)` | `VitalsSDK.deviceSupport(context).isSupported` is false. |
| `InternalError(message)` | The session is already running, was stopped, or has finished. Create a new session for the last two. |

A failed `start()` does not use up the session. `start()` does not throw `CameraUnavailable`: the camera opens
asynchronously, and a camera that cannot be opened ends the session with `VitalsEvent.Failed(CameraUnavailable)`.
`previewView` is optional; a fingertip covers the lens during a reading.

Behaviour:

* **Events.** `events` is a hot flow: collect it before `start()`, because events are not replayed. It is
  emitted on the SDK's own thread, so collect it on the dispatcher you need (`lifecycleScope.launch { ... }`
  moves it to the main thread). Each collector buffers 64 events and drops the oldest when it falls further
  behind, so the latest events (`Completed`, `Failed`, `Cancelled`) always arrive. The flow does not complete
  when the session ends; cancel your collecting coroutine when you are done (a `lifecycleScope` job ends with the
  activity).
* **Cancel.** `cancelReading()` applies while a reading is measuring: the session emits
  `Cancelled(CANCELLED_BY_APP)` and returns to positioning. While positioning there is nothing to cancel and the
  call does nothing.
* **Auto-start hold.** While `isAutoStartEnabled` is false (Java: `setAutoStartEnabled(false)`) the session keeps
  positioning, `Guidance` events keep flowing and `startProgress` fills, but no reading starts. Setting it back to
  true starts the next reading as soon as the fingertip is steady. A reading already measuring is unaffected. Any
  thread, any time, and it belongs to that session only.
* **stop().** Final: a stopped session cannot be started again. While positioning or measuring it ends the session
  without a result. Once `MeasurementComplete` has arrived, `stop()` releases the camera and torch at once and the
  session still delivers `Completed` (or `Failed(NoHeartRate)`). The SDK binds and unbinds only its own CameraX
  use cases (never `unbindAll()`) and clears the camera settings it applied, so your own camera features start
  from CameraX defaults afterwards.
* **Lifecycle.** The session observes the `lifecycleOwner`. Its `ON_STOP`, or another app taking the camera, is
  an interruption, never a failure: a reading in progress is cancelled with `Cancelled(INTERRUPTED)`, the session
  waits, and positioning resumes by itself on `ON_START` or when the camera is free again. If the camera then
  delivers no frames for 10 seconds of recovery while the app is in the foreground, the session ends with
  `Failed(CameraUnavailable)`. `ON_DESTROY` stops the session; call `stop()` yourself as well. An interruption
  after `MeasurementComplete` does not affect the result.
* **Camera health.** While positioning, a camera below its frame rate (Battery Saver or thermal throttling) shows
  `FRAME_DROP` guidance and the reading waits until the rate recovers. A camera that stops delivering frames is
  restarted once (a reading in progress is cancelled with `Cancelled(INTERRUPTED)`); a second stall, or a camera
  that cannot be restored, ends the session with `Failed(CameraUnavailable)`. A low frame rate never ends a
  session.
* A rotation recreates the activity, which destroys the owner and stops the session: lock the reading screen's
  orientation and keep the screen on (`FLAG_KEEP_SCREEN_ON`).

## VitalsEvent

```kotlin theme={null}
public sealed interface VitalsEvent {
  public data class Guidance(val guidance: com.neurofit.vitals.Guidance, val startProgress: Double) : VitalsEvent
  public data object Started : VitalsEvent
  public data class Preview(val preview: LivePreview) : VitalsEvent
  public data class Heartbeat(val ibiMs: Double?) : VitalsEvent
  public data class Cancelled(val reason: CancelReason) : VitalsEvent
  public data object MeasurementComplete : VitalsEvent
  public data class Completed(val result: VitalsResult) : VitalsEvent
  public data class Failed(val error: VitalsException) : VitalsEvent
}
```

| Event | When |
| - | - |
| `Guidance(guidance, startProgress)` | About once per second while positioning. `startProgress` (0 to 1) fills while the fingertip is steady; the reading starts on its own when it is full. |
| `Started` | A reading started. A `Preview` at progress 0 with no values follows at once. |
| `Preview(preview)` | About once per second while measuring. |
| `Heartbeat(ibiMs)` | Once per detected beat while measuring. `ibiMs` is the interval since the previous beat, `null` for the first. |
| `Cancelled(reason)` | The reading was abandoned. The session returns to positioning and guidance flows again. |
| `MeasurementComplete` | The full 60 seconds were captured. `Completed` or `Failed` follows within a few seconds. |
| `Completed(result)` | The result. The session has ended. |
| `Failed(error)` | The session ended without a result. |

The `when` over `VitalsEvent` is exhaustive with these eight cases. [Reading lifecycle](/reading-lifecycle)
describes the flow in detail, with suggested copy for each moment.

## Guidance

```kotlin theme={null}
public enum class Guidance {
  NO_CONTACT,    // no fingertip on the camera
  LOW_QUALITY,   // contact, but the signal is not yet steady
  WEAK_SIGNAL,   // contact, but the pulse is faint
  COMPLIANT,     // steady; startProgress is filling
  FRAME_DROP,    // the camera is below its frame rate; the reading waits until it recovers
}
```

The SDK ships no strings; map each value to your own localized copy.

## CancelReason

```kotlin theme={null}
public enum class CancelReason {
  CONTACT_LOST,       // the fingertip left the camera
  POOR_SIGNAL,        // the signal was too poor to finish a reliable reading
  INTERRUPTED,        // the capture was interrupted or restarted; the session resumes on its own
  CANCELLED_BY_APP,   // your app called cancelReading()
}
```

## LivePreview and SignalLevel

```kotlin theme={null}
public data class LivePreview(
  /** 0..1 of the reading's 60 seconds. */
  public val progress: Double,
  /** Provisional heart rate, beats per minute. */
  public val heartRateBpm: Double?,
  /** Provisional HRV (RMSSD), ms; available from about 20 s into the reading. */
  public val rmssdMs: Double?,
  /** Ready to display; null until the reading's first scored preview. */
  public val signalLevel: SignalLevel?,
)

public enum class SignalLevel { STRONG, GOOD, FAIR, WEAK }
```

The live values are provisional and for display only; store the final `VitalsResult`. `signalLevel` is smoothed
over the last few seconds and lowered ahead of a poor-signal cancel, so you can show it as it is.

## VitalsResult, SignalQuality and BreathingRateConfidence

```kotlin theme={null}
public open class VitalsResult(
  public val id: String,                    // UUID string, unique per reading
  public val capturedAt: Long,              // epoch milliseconds when the reading finished
  public val heartRateBpm: Double,          // always present on a completed reading
  public val rmssdMs: Double?,              // HRV (RMSSD), ms
  public val sdnnMs: Double?,               // SDNN, ms
  public val baevskyStressIndex: Double?,
  public val sd2sd1: Double?,               // Poincaré SD2:SD1 ratio
  public val breathingRateBrpm: Double?,    // breaths per minute
  public val breathingRateConfidence: BreathingRateConfidence?,   // null exactly when breathingRateBrpm is null
  public val quality: SignalQuality,
  public val sdkVersion: String,            // the SDK that produced the reading
)

public enum class SignalQuality(public val value: Int) {
  CLEAN(3),
  USABLE(2),
  WITHHELD(1),
}

public enum class BreathingRateConfidence {
  HIGH,     // measured from the phone's motion
  MEDIUM,   // a strong camera estimate
  LOW,      // a weaker camera estimate; show it as approximate
}
```

The public constructor is for your tests and for results restored from your own storage. `equals`, `hashCode` and
`toString` cover the eleven properties. `SignalQuality.value` matches the iOS raw value, and the
`BreathingRateConfidence` names match the iOS cases (`HIGH` is `.high`). Every value is finite. Results the SDK
produces have `breathingRateConfidence` exactly when they have `breathingRateBrpm`; the public constructor stores
what you pass. Field meanings, units and the quality and breathing-rate tiers are on [Metrics](/metrics).

## VitalsException

```kotlin theme={null}
public sealed class VitalsException(message: String) : Exception(message) {
  public class NotActivated : VitalsException(...)
  public class LicenseInvalid(public val reason: String) : VitalsException(...)
  public class CameraPermissionDenied : VitalsException(...)
  public class CameraUnavailable : VitalsException(...)
  public class UnsupportedDevice(public val reason: String) : VitalsException(...)
  public class NoHeartRate : VitalsException(...)
  public class InternalError(override val message: String) : VitalsException(...)
}
```

| Subclass | Meaning | Where it comes from |
| - | - | - |
| `NotActivated` | `activate` has not succeeded in this process. | `createSession`, `start` |
| `LicenseInvalid(reason)` | The key was rejected. `reason` names the check that failed: the key is malformed or its signature does not verify, it does not cover this application id, or this SDK build is newer than the key's `updatesUntil`. | `activate` |
| `CameraPermissionDenied` | `android.permission.CAMERA` is not granted. | `start` |
| `CameraUnavailable` | The camera could not be opened, or it stopped and could not be restored. | `Failed` |
| `UnsupportedDevice(reason)` | The device cannot take a reading; `reason` says why. | `createSession`, `start` |
| `NoHeartRate` | The 60 seconds were captured but no heart rate could be recovered. | `Failed` |
| `InternalError(message)` | A misuse, such as `start()` off the main thread, twice, or after the session ended, or an unexpected condition. | `start`, `Failed` |

Each subclass is a fresh instance with its own stack trace. `message` is a short English sentence for your logs;
show your own localized copy to users.

## Threading

* `VitalsSDK.activate`, `version`, `deviceSupport`, `hasCameraPermission`, `createSession`,
  `VitalsSession.cancelReading`, `isAutoStartEnabled` and `stop` are safe from any thread.
* `VitalsSDK.requestCameraPermission` and `VitalsSession.start` must be called on the main thread.
* `events` is emitted on the SDK's own thread; collect it on the dispatcher you need.
* Frame processing runs on the SDK's own threads. Nothing blocks the main thread.

## ProGuard / R8

The AAR ships `consumer-rules.pro`, which keeps the public API in the top-level package `com.neurofit.vitals`
(the single wildcard `com.neurofit.vitals.*`, not `.**`). Everything else inside the AAR is internal and may be
shrunk and renamed by your release build; it is not a supported API. If your build ignores consumer rules, add:

```
-keep public class com.neurofit.vitals.* { public *; protected *; }
-keep public interface com.neurofit.vitals.* { *; }
-keepclassmembers enum com.neurofit.vitals.* {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}
-keep class kotlin.Metadata { *; }
-keepattributes Signature, InnerClasses, EnclosingMethod, Exceptions, *Annotation*
```

The rules do not keep `SourceFile` or `LineNumberTable`. For readable SDK stack traces add
`-keepattributes SourceFile,LineNumberTable` to your own rules.

## Manifest

The AAR's manifest declares `android.permission.CAMERA` and marks `android.hardware.camera` and
`android.hardware.camera.flash` as `required="false"`, so your app stays installable on devices without them;
`VitalsSDK.deviceSupport(context)` reports at runtime whether a reading is possible. No other permission is
requested; the SDK does not use `INTERNET`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.