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

# Android quickstart

> Add the vitals-sdk AAR, activate your license, request the camera permission and run a reading in Kotlin or Java.

This page takes you from an empty project to a completed reading (heart rate, HRV (RMSSD), breathing rate and
related measures) on Android 9 (API 28) or later. The SDK is the Kotlin package `com.neurofit.vitals`, published
as `com.neurofit:vitals-sdk:1.0.0`. It compiles against `compileSdk 36`, targets JVM 17 and is built with
CameraX 1.5.0.

<Steps>
  <Step title="Add the dependency">
    **Binary license.** Your delivery contains the AAR and a Maven layout you can publish to your artifact
    repository (or install to `mavenLocal()` with the included `build_aar.sh`, which runs `publishToMavenLocal`).

    ```kotlin build.gradle.kts theme={null}
    dependencies {
      implementation("com.neurofit:vitals-sdk:1.0.0")
    }
    ```

    The Maven artifact declares the dependencies the SDK's public API uses as `api`, so your app compiles against
    `PreviewView`, `LifecycleOwner`, `ComponentActivity` and the coroutine flows without declaring them. If you add
    the AAR file directly instead, add its dependencies yourself, at these exact versions:

    ```kotlin build.gradle.kts theme={null}
    dependencies {
      implementation(files("libs/vitals-sdk-release.aar"))
      // api scope in the SDK (types in its public API)
      implementation("androidx.camera:camera-view:1.5.0")
      implementation("androidx.activity:activity-ktx:1.11.0")
      implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4")
      implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
      // implementation scope in the SDK (capture pipeline)
      implementation("androidx.camera:camera-core:1.5.0")
      implementation("androidx.camera:camera-camera2:1.5.0")
      implementation("androidx.camera:camera-lifecycle:1.5.0")
    }
    ```

    **Source license.** Include the `:vitals-sdk` module from the delivered Gradle project with `includeBuild`
    or by copying the module into your project; it compiles the engine sources from `ppgcore/ports/kotlin` in
    place.

    Your app needs `minSdk 28` or higher and Kotlin 2.0 or newer. The SDK's `consumer-rules.pro` keeps the public
    API (`com.neurofit.vitals.*`) under R8.
  </Step>

  <Step title="Declare the camera permission">
    The AAR's manifest already declares the camera permission and marks the camera and flash features optional,
    and manifest merging brings them into your app. Declaring them yourself is harmless:

    ```xml AndroidManifest.xml theme={null}
    <uses-permission android:name="android.permission.CAMERA" />

    <!-- Optional: keep the app installable on devices without a camera or flash.
         VitalsSDK.deviceSupport(context) reports at runtime whether a reading is possible. -->
    <uses-feature android:name="android.hardware.camera" android:required="false" />
    <uses-feature android:name="android.hardware.camera.flash" android:required="false" />
    ```

    The SDK does not need `INTERNET` or any other permission. It reads the accelerometer, which needs no
    permission, to help estimate breathing rate.
  </Step>

  <Step title="Activate the SDK">
    Call `activate` once, early in the app's life, before you create a session. Verification is offline. The
    call throws a `VitalsException` when the key is malformed, signed by another key, issued for a different
    application id, or does not cover this SDK build's date. A debuggable build accepts a key that does not list
    its application id and reports it in `warnings`; a release build rejects it.

    <CodeGroup>
      ```kotlin Kotlin theme={null}
      import com.neurofit.vitals.VitalsSDK
      import com.neurofit.vitals.VitalsException

      class App : Application() {
        override fun onCreate() {
          super.onCreate()
          try {
            val status = VitalsSDK.activate(this, BuildConfig.NEUROFIT_VITALS_LICENSE_KEY)
            Log.i("Vitals", "Licensed to ${status.licensee}, updates until ${status.updatesUntil}")
            status.warnings.forEach { Log.w("Vitals", "License warning: $it") }
          } catch (e: VitalsException) {
            // LicenseInvalid, LicenseNotValidForApp, LicenseUpdatesExpired
            Log.e("Vitals", "Activation failed", e)
          }
        }
      }
      ```

      ```java Java theme={null}
      import com.neurofit.vitals.VitalsSDK;
      import com.neurofit.vitals.VitalsException;
      import com.neurofit.vitals.LicenseStatus;

      public class App extends Application {
        @Override public void onCreate() {
          super.onCreate();
          try {
            LicenseStatus status = VitalsSDK.activate(this, BuildConfig.NEUROFIT_VITALS_LICENSE_KEY);
            Log.i("Vitals", "Licensed to " + status.getLicensee());
          } catch (VitalsException e) {
            Log.e("Vitals", "Activation failed", e);
          }
        }
      }
      ```
    </CodeGroup>

    `VitalsSDK` is a Kotlin `object` whose members are `@JvmStatic`, so Java calls them as statics
    (`VitalsSDK.activate(...)`); the `VitalsSDK.INSTANCE.activate(...)` form works too. See
    [License keys](/license-keys) for the key format, `updatesUntil` and the debuggable-build behaviour.
  </Step>

  <Step title="Check device support and request the camera permission">
    The SDK registers its own permission launcher through the activity's `ActivityResultRegistry`, so there is
    no launcher to declare in your activity. Call it on the main thread. When the permission is already granted
    the callback runs synchronously with `true`.

    ```kotlin theme={null}
    val support = VitalsSDK.deviceSupport(this)
    if (!support.isSupported) {
      // support.reason explains why: "no rear camera", "no torch (camera flash)", or
      // "camera does not offer a 30 fps range".
      return
    }

    if (VitalsSDK.hasCameraPermission(this)) {
      startReading()
    } else {
      VitalsSDK.requestCameraPermission(this) { granted ->
        if (granted) startReading() else showPermissionRationale()
      }
    }
    ```

    The launcher is registered on this activity instance: if the activity is recreated while the system dialog is
    showing (a rotation), the callback is never called. Lock the orientation of the screen that asks, and check
    `hasCameraPermission` again in `onResume`.
  </Step>

  <Step title="Create a session and collect events">
    A `VitalsSession` owns the camera, the torch and one reading at a time. `events` (a `SharedFlow`) and `state`
    (a `StateFlow`) are emitted on the SDK's engine thread, so collect them inside `lifecycleScope` (which runs on
    the main thread) before touching views, and subscribe **before** `start()`: events are not replayed. Java
    callers use `setListener` with the executor the callbacks should run on. `start()` must be called on the main
    thread.

    <CodeGroup>
      ```kotlin Kotlin theme={null}
      import android.view.WindowManager
      import androidx.activity.ComponentActivity
      import androidx.camera.view.PreviewView
      import androidx.lifecycle.lifecycleScope
      import com.neurofit.vitals.*
      import kotlinx.coroutines.launch

      class ReadingActivity : ComponentActivity() {
        private var session: VitalsSession? = null

        private fun startReading() {
          window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
          val session = VitalsSDK.createSession(this, VitalsConfig())
          this.session = session

          lifecycleScope.launch {
            session.events.collect { event ->
              when (event) {
                is VitalsEvent.Guidance -> showGuidance(event.guidance, event.startProgress)
                is VitalsEvent.Started -> showMeasuring()
                is VitalsEvent.Preview -> showLive(event.preview)     // rmssdMs from ~20 s, breathingRate from ~30 s
                is VitalsEvent.Heartbeat -> pulseAnimation(event.ibiMs)
                is VitalsEvent.Cancelled -> showRestartNote(event.reason)   // the session re-arms by itself
                is VitalsEvent.RecoverableError -> Log.w("Vitals", "Recovering: ${event.error}")
                is VitalsEvent.MeasurementComplete -> showFinalizing()
                is VitalsEvent.Completed -> showResult(event.result)
                is VitalsEvent.Failed -> showFailure(event.error)
                else -> Unit   // State, ReconfiguringCamera (never emitted on Android)
              }
            }
          }

          // The measuring clock ticks on state only (no State event per tick).
          lifecycleScope.launch {
            session.state.collect { state ->
              if (state is VitalsSessionState.Measuring) showElapsed(state.elapsed)
            }
          }

          // previewView is optional; pass null for a reading without a camera preview.
          session.start(owner = this, previewView = findViewById<PreviewView>(R.id.previewView))
        }

        private fun showFailure(error: VitalsException) {
          when (error) {
            is VitalsException.Interrupted -> showRetry("The reading was interrupted: ${error.reason}")
            is VitalsException.NoHeartRate -> showRetry("No pulse found. Rest your fingertip flat on the camera.")
            is VitalsException.UnsupportedDevice -> showUnsupported(error.reason)
            else -> showRetry(error.message ?: "Unexpected error")
          }
        }

        override fun onDestroy() {
          super.onDestroy()
          session?.stop()   // idempotent; torch off, the SDK's own camera use cases unbound
        }
      }
      ```

      ```java Java theme={null}
      import androidx.activity.ComponentActivity;
      import androidx.core.content.ContextCompat;
      import com.neurofit.vitals.*;

      public class ReadingActivity extends ComponentActivity {
        private VitalsSession session;

        private void startReading() {
          VitalsConfig config = new VitalsConfig.Builder()
              .readingDuration(60.0)
              .autoRestartAfterCancel(true)
              .build();
          session = VitalsSDK.createSession(this, config);

          // Callbacks run on the executor you pass; the result arrives as VitalsEvent.Completed,
          // a terminal error as VitalsEvent.Failed.
          session.setListener(event -> {
            if (event instanceof VitalsEvent.Guidance g) {
              showGuidance(g.getGuidance(), g.getStartProgress());
            } else if (event instanceof VitalsEvent.Preview p) {
              showLive(p.getPreview());
            } else if (event instanceof VitalsEvent.Completed c) {
              showResult(c.getResult());
            } else if (event instanceof VitalsEvent.Failed f) {
              showFailure(f.getError());
            }
          }, ContextCompat.getMainExecutor(this));

          session.start(this, findViewById(R.id.previewView));
        }

        @Override protected void onDestroy() {
          super.onDestroy();
          if (session != null) session.stop();
        }
      }
      ```
    </CodeGroup>

    `start(owner, previewView)` binds the camera to the given `LifecycleOwner`, turns on the torch and moves the
    session to `Positioning` before it returns. It throws `VitalsException.InternalError` when called off the
    main thread or on a stopped or finished session, `NotActivated`, `CameraDenied`, `UnsupportedDevice` and
    `AlreadyRunning`. The camera itself opens asynchronously: a camera that cannot be opened fails the session
    with `Failed(CameraUnavailable)` through `state` and `events` rather than an exception from `start()`.
    `VitalsSDK.createSession` throws `NotActivated`, `UnsupportedDevice` or `InternalError("invalid configuration:
            ...")`.

    For a one-shot flow you can also `val result = session.awaitResult()` from a coroutine; it suspends until
    `Completed` and throws the `VitalsException` on `Failed` (or `Interrupted` if `stop()` runs first).
    `session.getResult { }` is the callback form for Kotlin code outside a coroutine; its callback receives a
    Kotlin `Result`, so from Java handle `VitalsEvent.Completed` and `VitalsEvent.Failed` in the listener instead.
  </Step>

  <Step title="Use the result">
    ```kotlin theme={null}
    fun showResult(result: VitalsResult) {
      if (result.quality == SignalQuality.WITHHELD) {
        // Metrics may be null. Offer a retry rather than showing numbers.
        return
      }
      val hr = result.heartRateBpm?.let { "${it.roundToInt()} bpm" } ?: "n/a"
      val rmssd = result.rmssdMs?.let { "${it.roundToInt()} ms" } ?: "n/a"
      val rr = result.breathingRateBrpm?.let { "%.1f brpm".format(it) } ?: "n/a"
      val rrNote = if ((result.breathingRateConfidence ?: 0.0) < 0.5) " (estimate)" else ""
      Log.i("Vitals", "HR $hr, HRV (RMSSD) $rmssd, breathing $rr$rrNote")
    }
    ```

    `VitalsResult` is a data class with a `toMap()` for JSON serialisation (`capturedAt` as ISO 8601, `quality`
    as `"clean"` / `"usable"` / `"withheld"`). Field meanings, units and how to treat the `WITHHELD` tier are on
    the [Metrics](/metrics) page.
  </Step>
</Steps>

## Jetpack Compose

Wrap CameraX's `PreviewView` in an `AndroidView` and pass it to `start` from the main thread. Collect
`session.events` inside a `LaunchedEffect` and `session.state` with `collectAsStateWithLifecycle`.

```kotlin theme={null}
AndroidView(factory = { context -> PreviewView(context) }) { previewView ->
  if (!started) { session.start(lifecycleOwner, previewView); started = true }
}
```

## Guidance copy

`Guidance` is an enum (`NO_CONTACT`, `LOW_QUALITY`, `WEAK_SIGNAL`, `COMPLIANT`, `FRAME_DROP`); the SDK does not
ship strings. Map each value to your own localized copy. The NEUROFIT app's copy is listed on the
[Reading lifecycle](/reading-lifecycle) page as a starting point.

## Lifecycle notes

* Call `stop()` (or `close()`, the session is `AutoCloseable`) when the reading screen goes away. The SDK also
  stops the session when the lifecycle owner you passed is destroyed, but call it yourself as well.
* The SDK binds and unbinds only its own CameraX use cases (never `unbindAll()`), and clears the Camera2 options
  it applied when it stops, so your own camera features are untouched and open with default exposure afterwards.
* Leaving the foreground (the owner's `ON_STOP`) while positioning or measuring fails the session with
  `Interrupted("The app moved to the background")`; a finalizing session still delivers its result. A rotation
  recreates the activity and passes through `ON_STOP`, so **lock the reading screen's orientation** and keep the
  screen on (`FLAG_KEEP_SCREEN_ON`).
* After a cancel with `autoRestartAfterCancel = false` the session is `Idle` with the camera off, and
  `start(owner, previewView)` works again on the same session. After `stop()`, `Completed` or `Failed`, create a
  new session.
* Battery Saver can lower the camera frame rate. While positioning, the session reports a recoverable
  `FrameRateUnsustainable` once and re-arms; a second recoverable error fails with `UnsupportedDevice`. See
  [Troubleshooting](/troubleshooting).
* For debugging, `VitalsSDK.loggingEnabled = true` logs state transitions with tag `NeurofitVitals`; it is off by
  default and never logs values or frames.

## Next steps

* [Reading lifecycle](/reading-lifecycle): every state, event and guidance value.
* [Metrics](/metrics): definitions, units and validated accuracy.
* [API reference (Android)](/api-reference-android): every public type.


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