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

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()`).

    ```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 `Flow` without declaring them. If you add the AAR
    file directly instead (copied to `libs/vitals-sdk.aar` here), add its dependencies yourself, at these exact
    versions:

    ```kotlin build.gradle.kts theme={null}
    dependencies {
      implementation(files("libs/vitals-sdk.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. It
    throws `VitalsException.LicenseInvalid` 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; a release build rejects it.

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

      class App : Application() {
        override fun onCreate() {
          super.onCreate()
          try {
            VitalsSDK.activate(this, BuildConfig.NEUROFIT_VITALS_LICENSE_KEY)
          } catch (e: VitalsException.LicenseInvalid) {
            Log.e("Vitals", "Activation failed: ${e.reason}")
          }
        }
      }
      ```

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

      public class App extends Application {
        @Override public void onCreate() {
          super.onCreate();
          try {
            VitalsSDK.activate(this, BuildConfig.NEUROFIT_VITALS_LICENSE_KEY);
          } 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. See
    [License keys](/license-keys) for the key format and `updatesUntil`.
  </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" 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. `events` is emitted on the SDK's own thread, so
    collect it inside `lifecycleScope` (which runs on the main thread) before touching views, and subscribe
    **before** `start()`: events are not replayed. `start()` must be called on the main thread.

    ```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.Job
    import kotlinx.coroutines.launch

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

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

          eventsJob = 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 about 20 s
                is VitalsEvent.Heartbeat -> pulseAnimation(event.ibiMs)
                is VitalsEvent.Cancelled -> showRestartNote(event.reason)   // the session starts again by itself
                is VitalsEvent.MeasurementComplete -> showFinalizing()
                is VitalsEvent.Completed -> showResult(event.result)
                is VitalsEvent.Failed -> showFailure(event.error)
              }
            }
          }

          // previewView is optional; pass null for a reading without a camera preview.
          session.start(this, findViewById<PreviewView>(R.id.previewView))
        } catch (e: VitalsException) {
          showFailure(e)
        }
      }

      private fun showFailure(error: VitalsException) {
        when (error) {
          is VitalsException.NoHeartRate -> showRetry("No pulse found. Rest your fingertip flat on the camera.")
          is VitalsException.CameraUnavailable -> showRetry("Close other camera apps and try again.")
          is VitalsException.CameraPermissionDenied -> showPermissionRationale()
          else -> showRetry(error.message ?: "Unexpected error")
        }
      }

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

    `VitalsSDK.createSession` throws `NotActivated` or `UnsupportedDevice`. `start(lifecycleOwner, previewView)`
    binds the camera to the given `LifecycleOwner`, turns on the torch and begins positioning. It throws
    `NotActivated`, `CameraPermissionDenied`, `UnsupportedDevice`, or `InternalError` when called off the main
    thread or on a session that is already running or has ended. The camera itself opens asynchronously: a camera
    that cannot be opened ends the session with `VitalsEvent.Failed(CameraUnavailable)` rather than an exception
    from `start()`.

    The `events` flow does not complete when the session ends, so cancel your collecting coroutine when you are
    done. A `lifecycleScope` job is cancelled with the activity.
  </Step>

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

    `VitalsResult` carries eleven plain properties (`capturedAt` is epoch milliseconds), so you can store it with
    the serialiser you already use. Field meanings, units, how to treat the `WITHHELD` tier and what the
    breathing-rate confidence tiers mean 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`, which cancels the collection when it leaves the composition.

```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()` 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. Once `MeasurementComplete` has arrived, a stopped session
  still delivers `Completed` (or `Failed`) through `events`.
* The SDK binds and unbinds only its own CameraX use cases (never `unbindAll()`), and clears the camera settings
  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`) or another app taking the camera does not end the session. A
  reading in progress is cancelled with `Cancelled(INTERRUPTED)` and the session resumes positioning by itself on
  `ON_START` or when the camera is free again. A rotation recreates the activity, which destroys the owner and
  stops the session, so **lock the reading screen's orientation** and keep the screen on
  (`FLAG_KEEP_SCREEN_ON`).
* To keep the camera positioning without starting a reading (an intro or a sheet covers the screen, say), set
  `session.isAutoStartEnabled = false`; set it back to `true` when the user is ready. A reading already measuring
  is unaffected.
* After `stop()`, `Completed` or `Failed`, create a new session for the next reading.
* Battery Saver can lower the camera frame rate. While positioning, `FRAME_DROP` guidance holds the start until
  the rate recovers; the session never fails for it. See [Troubleshooting](/troubleshooting).

## Next steps

* [Reading lifecycle](/reading-lifecycle): every event and guidance value, with suggested copy.
* [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.