Skip to main content
Package com.neurofit.vitals, Maven com.neurofit:vitals-sdk:1.0.0, minSdk 28, compileSdk 36, JVM 17, Kotlin compiler 2.2.20 with apiVersion and languageVersion 2.0, explicitApi(). The engine is compiled into the AAR; nothing in the public API exposes an engine type (the ppgcore.* classes inside the AAR are not a supported API and may be renamed by your release build). Engine version 1.3.0. Dependencies declared api (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, 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. Java callers: every VitalsSDK member is @JvmStatic (VitalsSDK.activate(context, key); the VitalsSDK.INSTANCE.activate(...) form works too), createSession, start and cancelReading carry @JvmOverloads, VitalsConfig has a Builder, and VitalsListener replaces the flows. awaitResult and getResult are Kotlin-only. This page is hand-maintained from the SDK sources and updated with every SDK change; the iOS page documents the mirrored API.

VitalsSDK

Notes:
  • activate compares the key’s apps with the application id (context.packageName). When the host app is debuggable (ApplicationInfo.FLAG_DEBUGGABLE, a runtime check) a key that does not list the id is accepted with a note in LicenseStatus.warnings; a release build rejects it. See License keys.
  • deviceSupport reads Camera2 characteristics only. A rear camera whose AE target fps ranges do not include 30 is unsupported; when the ranges cannot be read the device counts as supported and the runtime frame-rate gate decides. Whether the camera offers manual exposure controls does not affect support.
  • requestCameraPermission: when the permission is already granted, onResult(true) is called synchronously and nothing is launched. Otherwise the SDK registers a launcher under its own key, launches the system prompt, delivers onResult on the main thread and unregisters. The registration lives on that activity instance: if the activity is recreated while the dialog is showing (rotation, split-screen resize), onResult is never called, so also check hasCameraPermission in onResume or lock the orientation of the screen that asks.
  • createSession returns a session bound to the application context. VitalsSession has no public constructor.
  • loggingEnabled logs state transitions and capture decisions only: never metric values, frames or license contents.

LicenseStatus and LicenseType

DeviceSupport

Exactly five fields.
reason is one of: "no rear camera", "no torch (camera flash)", "camera does not offer a 30 fps range".

VitalsConfig

AdvancedConfig

Capture and gating parameters with the Android production defaults. Changing them is unvalidated. The Reading lifecycle page lists both platforms side by side.

Configuration validation

createSession validates the configuration once and throws VitalsException.InternalError with detail "invalid configuration: <reason>" for a value the capture layer cannot run with:

VitalsSession

One session runs one reading. Create a new session for each reading screen through VitalsSDK.createSession.
start(owner, previewView) checks its preconditions and throws, in this order: A failed start() does not consume the session. start() does not throw CameraUnavailable: the camera opens asynchronously, and a camera that cannot be opened or bound fails the session with Failed(CameraUnavailable) through state and events (awaitResult throws it). The state is Positioning as soon as start() returns. Other behaviours:
  • Measuring ticks. While measuring, state.value is set to Measuring(elapsed) about once per second without a VitalsEvent.State; the Preview event carries the same elapsed.
  • Cancel without auto-restart. With autoRestartAfterCancel = false a cancel emits Cancelled, then State(Idle), unbinds the camera (torch off) and stops the accelerometer; start(owner, previewView) may be called again on the same session. The owner stays observed.
  • Lifecycle owner. The session observes owner. ON_STOP while positioning or measuring fails the session with Interrupted("The app moved to the background"); while finalizing the result is kept. ON_DESTROY calls stop(). Lock the reading screen’s orientation so a configuration change cannot destroy the owner mid-reading.
  • stop(). Before a terminal state, stop() sets state to Idle and fails a pending awaitResult / getResult with Interrupted("session stopped before a result was produced"); a terminal state is kept. stop() clears the Camera2 interop options it set (AE/AF off, manual exposure, AWB lock, tonemap) before unbinding, so the host’s next use of the rear camera starts from CameraX defaults. It never calls unbindAll().
  • getResult delivers on the listener executor when one is set, otherwise on the main thread. kotlin.Result is a value class, so Java callers use VitalsEvent.Completed / Failed through setListener instead.
  • events emits with tryEmit: a collector more than 64 events behind loses the oldest Heartbeat / Preview events, never the newest ones (Completed, Failed, Cancelled always land). A listener executor that rejects work (shut down) drops that callback rather than failing the engine thread.

VitalsSessionState

VitalsEvent

Completed and Failed arrive twice: as their own event and inside the following State(...) event.

Guidance

Evaluation order per preview: NO_CONTACT, then FRAME_DROP, then WEAK_SIGNAL, then LOW_QUALITY, else COMPLIANT. The SDK ships no strings; see Reading lifecycle for the NEUROFIT app’s copy.

LivePreview

rmssdMs is sticky: once shown it keeps the last good value until the attempt ends. abortRisk is WARN from preview 21 of the 35 s discard deadline (60%, rounded up), IMMINENT from preview 30 (the last 5 s), and NONE as soon as a live estimate clears the gate.

CancelReason and CancelContext

CancelContext is a plain class, not a data class (no copy() or componentN): the cancel reason’s wire form travels inside it for toMap() but is not part of the eight-field public shape. toMap() always contains these ten keys, in this order: reason ("contact_lost", "low_quality", "low_live_signal_quality", "no_low_discard_estimate", or the host’s string), quality_score, elapsed_s, engine ("neurofit_vitals"), refine_mode (a per-platform engine constant), effective_fps, device_model, red_dc, exposure_iso, exposure_tune_steps. Absent numbers are -1.0 (-1 for exposure_tune_steps). exposure_in_band is added only when exposureInBand is known.

VitalsRecoverableError

The SDK recovers from the first recoverable error in a camera session (the attempt is abandoned, the session re-arms in Positioning, a stall also rebinds the camera) and fails on the second, whatever its kind: FrameRateUnsustainable fails with UnsupportedDevice("camera cannot sustain 30 fps (measured <fps> fps)"), CameraStalled with CameraUnavailable.

VitalsException

A sealed class whose subclasses are ordinary classes (each throw carries its own stack trace), so match them with is, never with ==.
message is one English sentence per subclass with the associated value (for example "Reading interrupted: The app moved to the background", "Internal error: invalid configuration: readingDuration must be in (0, 3600.0] seconds, got NaN").

VitalsResult

toMap() is the shape the wrappers deliver. Field meanings and units are on the Metrics page.

VitalsDiagnostics

One camelCase property per key; toMap() serialises to the snake_case keys shown. Nullable properties are absent from the map (not -1) when the step that produces them did not run.
See Diagnostics for every key’s meaning. The iOS record differs in a few keys (the iOS-only camera-format keys such as red_dc_mean and active_format_width do not exist here; est_android_estimator_enabled and est_gm_ratio exist only here).

VitalsChange

Provisional helper for pre/post comparisons; see Outcomes best practices.
A metric’s entry is null when either reading lacks the metric.

Threading

  • VitalsSDK.activate, licenseStatus, loggingEnabled, deviceSupport, hasCameraPermission, createSession, VitalsSession.cancelReading, stop, close, setListener, awaitResult and getResult are safe from any thread.
  • VitalsSDK.requestCameraPermission and VitalsSession.start must be called on the main thread; start() throws InternalError otherwise.
  • state and events are updated and emitted on the SDK’s single engine thread. Collect them on the dispatcher you need (for example lifecycleScope.launch { session.events.collect { ... } }, which moves them to the main thread). setListener callbacks run on the executor you pass.
  • Camera frames are extracted on a CameraX analysis thread, the accelerometer is read on its own handler thread, the engine’s finalize runs on its own executor and the live HRV (RMSSD) preview on another. Nothing blocks the main thread.

ProGuard / R8

The AAR ships consumer-rules.pro. It keeps the public API in the top-level package only, with the single wildcard (com.neurofit.vitals.*, not .**): the internals under com.neurofit.vitals.capture, .license, .reading and the engine under ppgcore.* may be shrunk and obfuscated. If your build ignores consumer rules, add:

Logging

VitalsSDK.loggingEnabled (default false) turns on android.util.Log messages with tag NeurofitVitals (debug and warning levels): state transitions and capture decisions only. No metric values, frames or license contents are ever logged.

Manifest

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