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
activatecompares the key’sappswith 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 inLicenseStatus.warnings; a release build rejects it. See License keys.deviceSupportreads 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, deliversonResulton 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),onResultis never called, so also checkhasCameraPermissioninonResumeor lock the orientation of the screen that asks.createSessionreturns a session bound to the application context.VitalsSessionhas no public constructor.loggingEnabledlogs 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 throughVitalsSDK.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.valueis set toMeasuring(elapsed)about once per second without aVitalsEvent.State; thePreviewevent carries the sameelapsed. - Cancel without auto-restart. With
autoRestartAfterCancel = falsea cancel emitsCancelled, thenState(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_STOPwhile positioning or measuring fails the session withInterrupted("The app moved to the background"); while finalizing the result is kept.ON_DESTROYcallsstop(). Lock the reading screen’s orientation so a configuration change cannot destroy the owner mid-reading. - stop(). Before a terminal state,
stop()setsstatetoIdleand fails a pendingawaitResult/getResultwithInterrupted("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 callsunbindAll(). getResultdelivers on the listener executor when one is set, otherwise on the main thread.kotlin.Resultis a value class, so Java callers useVitalsEvent.Completed/FailedthroughsetListenerinstead.eventsemits withtryEmit: a collector more than 64 events behind loses the oldestHeartbeat/Previewevents, never the newest ones (Completed,Failed,Cancelledalways 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
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
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 withis, 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.
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.null when either reading lacks the metric.
Threading
VitalsSDK.activate,licenseStatus,loggingEnabled,deviceSupport,hasCameraPermission,createSession,VitalsSession.cancelReading,stop,close,setListener,awaitResultandgetResultare safe from any thread.VitalsSDK.requestCameraPermissionandVitalsSession.startmust be called on the main thread;start()throwsInternalErrorotherwise.stateandeventsare updated and emitted on the SDK’s single engine thread. Collect them on the dispatcher you need (for examplelifecycleScope.launch { session.events.collect { ... } }, which moves them to the main thread).setListenercallbacks 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 shipsconsumer-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 declaresandroid.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.