DocsSmartSpectra
Browse documentation
Android / Kotlin

Android Migration Guide

View MarkdownView source

Applies to SmartSpectra Android SDK v3.x. Migrating from a v3.0 release-candidate prior to rc.12, or from v2.x.

Android SDK v3.4.0 Migration

Camera selection

Camera selection is opt-in. Existing applications retain the front-camera default and the behavior of config.cameraPosition, including active-camera changes. That property, CameraPosition, and the original useCamera() method remain available, with deprecation warnings. To adopt discovery and strict selection, call useCamera(selection) while stopped:

import com.presagetech.smartspectra.CameraSelection
import com.presagetech.smartspectra.SmartSpectraSdk

// From a coroutine. Discovery needs only application context, not an SDK instance:
val cameras = SmartSpectraSdk.availableCameras(context)

// Select an input while stopped; useCamera does not open a camera.
sdk.useCamera(CameraSelection.Default)
sdk.useCamera(CameraSelection.Front)
sdk.useCamera(CameraSelection.Back)
cameras.firstOrNull()?.let { camera ->
    sdk.useCamera(CameraSelection.ById(camera.id))
}

Discovery returns CameraInfo(id, name, facing, lensType). IDs are opaque and device-local; name is optional and currently null on Android. Facing is FRONT, BACK, or UNKNOWN; external cameras report UNKNOWN. Discovery does not prompt for permission or start capture. Visibility depends on platform permissions, and a snapshot does not guarantee that capture will succeed later. Discovery failure throws SmartSpectraException with INPUT_UNAVAILABLE.

lensType is WIDE_ANGLE, ULTRA_WIDE, TELEPHOTO, or UNKNOWN. Android classification is approximate, relative to the default camera with the same facing. Missing or ambiguous metadata and logical cameras combining multiple lenses report UNKNOWN. Classification does not describe digital zoom or change camera selection; continue selecting by id.

Default explicitly opts into preferring front, then the first camera in discovery order. Explicit Front, Back, and ById requests never fall back to a different facing or ID. Camera resolution occurs at startup; an unavailable selection fails start() with INPUT_UNAVAILABLE. After stopping, select an available camera and start again.

useCamera(selection) throws INVALID_STATE during startup, processing, stopping, or reset, and CONFIGURATION_FAILED for an empty ID. A rejected request preserves the previous source. To change cameras, await stop(), call useCamera(selection), and call start() again. Stop/start/reset retain the selected camera until you choose another source. Typed selection does not switch a running camera implicitly. Once selected, changes to the deprecated cameraPosition property do not override it. Calling the deprecated useCamera() returns to that property’s selection behavior.

The demo app’s Select camera control lists discovered cameras with their name, facing, lens type, and ID. Choose a camera for the next measurement, Default to restore automatic selection, or Refresh to discover connected cameras again. The checkup, headless, and capture screens share this picker; selection is disabled while processing. Discovery and selection errors appear in the picker, and capture errors appear on the measurement screen. The minimal app also provides a Select camera control, with Default and Refresh actions in both portrait and landscape layouts. After a failed Start, Select camera remains available in both apps. Opening it awaits stop() to recover the SDK from ERROR before allowing a new selection.

Use the public custom-input API

Apps that supply frames now use the public useCustomInput() API. Select custom input while the SDK is stopped, retain the returned CustomInput, await start(), and submit frames from a worker thread:

val input = sdk.useCustomInput()
sdk.start()

val result = input.sendFrame(imageProxy)
if (result is FrameSubmissionResult.Rejected) {
    // Handle result.error.
}

Existing test integrations should replace the @SmartSpectraTestingApi setVideoInputEnabled(true) and addVideoFrame(...) calls with useCustomInput() and CustomInput.sendFrame(...). The public handle supports ImageProxy, Bitmap, and VideoFrame. It survives stop/start/reset; selecting another source invalidates it. SDK camera permission is not required for custom input, but the host app remains responsible for permissions required by its own capture code.

Check custom-input timestamp rejections in the submission result

CustomInput.sendFrame() returns FrameSubmissionResult.Rejected with NON_MONOTONIC_TIMESTAMP or TIMESTAMP_GAP when it rejects a timestamp. These rejections leave the session active and do not publish a global sdk.error. Check each submission result even if you also observe sdk.error.

A rejected timestamp does not advance the accepted timeline. Correct the next timestamp, or stop and start the SDK to begin a fresh timeline after an interruption.

Usage failures now report specific errors

Usage entitlement checking continues throughout a measurement as before. What changes in v3.4.0 is how a terminal usage failure is reported. Observe sdk.error and handle:

  • AUTHENTICATION_FAILED for rejected credentials
  • CREDIT_EXHAUSTED for an explicit quota denial
  • NETWORK_ERROR when entitlement cannot be refreshed in time

The SDK also includes short measurements and the final partial interval before stop() in usage reporting. No public API changes are required.

Android SDK v3.3.0 Migration

Default log verbosity is now warnings and errors only

The SDK previously emitted informational logcat chatter by default. From v3.3.0 the default level is SmartSpectraLogLevel.WARNING, covering both the SDK’s Kotlin-side logging and the native engine. If you relied on the informational output, restore it via SmartSpectraConfig.logLevel:

val config = SmartSpectraConfig().apply { logLevel = SmartSpectraLogLevel.INFO }
SmartSpectraSdk.initialize(context, config)

Edge Metrics Migration

The metricsBuffer pathway has been removed. Android apps should now read all vitals data from metrics.

What Changed

  • MetricsBuffer-based APIs were removed
  • On-device Metrics is now the single vitals data source

Field Mappings

Old (metricsBuffer)New (metrics)
pulse.rateListcardio.pulseRateList
pulse.traceListcardio.arterialPressureTraceList
breathing.rateListbreathing.rateList
breathing.upperTraceListbreathing.upperTraceList
breathing.lowerTraceListbreathing.lowerTraceList
faceface

Important

Cardio fields now require explicit opt-in. If your app displays pulse rate, arterial pressure trace, or HRV, enable cardio metrics explicitly.

import com.presagetech.smartspectra.SmartSpectraConfig

val sdk = SmartSpectraSdk.shared
sdk.config.requestedMetrics =
    SmartSpectraConfig.breathingMetrics + SmartSpectraConfig.cardioMetrics

Removed:

  • sdk.metricsBuffer
  • setMetricsBufferObserver()
  • setMetricsBuffer()
  • clearMetricsBuffer()

Replace with:

// Before:
smartSpectraSdk.metricsBuffer.observe(viewLifecycleOwner) { buffer ->
    val pulse = buffer?.pulse?.rateList?.lastOrNull()?.value
}
// After:
smartSpectraSdk.metrics.observe(viewLifecycleOwner) { metrics ->
    val pulse = metrics?.cardio?.pulseRateList?.lastOrNull()?.value
}

Protobuf Java/Kotlin Package Rename

The generated protobuf classes moved from the old Physiology Java package to the SmartSpectra package:

BeforeAfter
com.presage.physiology.proto.*com.presagetech.smartspectra.proto.*

Update imports for generated types such as Metrics, MetricType, StatusCode, and Insight:

// Before:
import com.presage.physiology.proto.MetricsProto.Metrics
import com.presage.physiology.proto.MetricTypesProto.MetricType

// After:
import com.presagetech.smartspectra.proto.MetricsProto.Metrics
import com.presagetech.smartspectra.proto.MetricTypesProto.MetricType

The proto package also changed from presage.physiology to presagetech.smartspectra. The binary wire format is unchanged because field numbers did not change, but Any.type_url, JSON @type values, and descriptor lookups use the fully-qualified message name. Regenerate or re-emit any persisted Any-wrapped or JSON-typed payloads with the new SDK before mixing old and new builds.

Processing Status Migration

SmartSpectraSdk now exposes the aligned lifecycle model through processingStatus.

New Enum Values

  • IDLE
  • STARTING
  • RUNNING
  • STOPPING
  • ERROR

Enum Mapping

Previous valueNew value
IDLEIDLE
COUNTDOWNSTARTING
RUNNINGRUNNING
PREPROCESSEDRUNNING
DONERUNNING
DISABLEIDLE
ERRORERROR

Semantic Updates

  • RUNNING means the pipeline is active and measurement is running
  • STOPPING is a transient shutdown state on the path back to IDLE
// After:
SmartSpectraSdk.shared.processingStatus.observe(viewLifecycleOwner) { status ->
    when (status) {
        ProcessingStatus.IDLE -> showIdleUi()
        ProcessingStatus.STARTING -> showLoadingUi()
        ProcessingStatus.RUNNING -> showRecordingUi()
        ProcessingStatus.STOPPING -> showStoppingUi()
        ProcessingStatus.ERROR -> showErrorUi()
    }
}

UI Surface Removal

The screening UI (Activities, Fragments, custom Views) no longer ships with the SDK. The SDK is now a pure data/lifecycle plane; integrators own all UI.

What was removed from the SDK

  • SmartSpectraView, SmartSpectraButton, SmartSpectraResultView
  • SmartSpectraActivity, OnboardingTutorialActivity
  • All ui/screening/... Fragments and custom Views (CameraProcessFragment, ConfigurationErrorFragment, PermissionsRequestFragment, ScreeningPlotView, ExpressionStatusView, FaceMetricsStatusView)
  • ui/summary/UploadingFragment
  • All UI layouts, drawables, tutorial assets, and screening strings
  • The two SDK manifest <activity> declarations
  • LiveDatas / helpers removed from SmartSpectraSdk: timeLeft, hintText, roundedFps, attachPreview, detachPreview, flipCamera, resetMetrics. None were public, and the only in-tree consumers were the relocated UI files. The internal SmartSpectraVitalsProcessor keeps timeLeft / hintText for instrumented tests; the SDK class no longer mirrors them.
  • Config flags removed from SmartSpectraConfig (all were internal): showFps, showOutputFps, showControlsInScreeningView
  • Derived helpers cardioMeasurementsEnabled, facialExpressionEnabled, edaMeasurementsEnabled (replicate app-side from requestedMetrics if needed)

Migration paths

Option A — Copy the reference UI from the demo-app sample

The sample at android/samples/demo-app (in the public Presage-Security/SmartSpectra repo) mirrors the SDK’s old package layout. Copy the relevant files into your app and adjust R.* references:

samples/demo-app/src/main/java/com/presagetech/smartspectra_example/
  SmartSpectraView.kt
  SmartSpectraButton.kt
  SmartSpectraResultView.kt
  ui/
    OnboardingTutorialActivity.kt
    SmartSpectraActivity.kt
    AmbientLightBrightnessController.kt
    screening/...
    summary/...
samples/demo-app/src/main/res/  (layouts, drawables, strings)
samples/demo-app/src/main/AndroidManifest.xml  (activity declarations)

Option B — Build your own UI against SmartSpectraSdk

Recommended for new integrations. The public surface gives you everything you need:

val sdk = SmartSpectraSdk.shared
sdk.config.apiKey = "YOUR_API_KEY"
sdk.useCamera(CameraSelection.Front)
sdk.config.requestedMetrics = SmartSpectraConfig.breathingMetrics + SmartSpectraConfig.cardioMetrics

// Drive the lifecycle from your own button.
lifecycleScope.launch { sdk.start() }
lifecycleScope.launch { sdk.stop() }

// Render preview frames from a public LiveData<Bitmap?>.
sdk.imageOutput.observe(this) { bitmap ->
    findViewById<ImageView>(R.id.preview).setImageBitmap(bitmap)
}

// React to status / metrics / errors.
sdk.processingStatus.observe(this) { renderStatus(it) }
sdk.metrics.observe(this) { renderMetrics(it) }
sdk.error.observe(this) { renderError(it) }

See samples/minimal-app for the smallest end-to-end headless example, and samples/demo-app for a richer reference UI built on top of the public surface.

Lost reference UX

Mid-session camera flip, hint-text overlays, FPS overlay, the configuration-error route, the bundled onboarding tutorial, and the in-app EULA modals are no longer part of the SDK. The demo-app sample keeps a compatible reference UX; integrators can copy it or roll their own.

start() clears observable state

SmartSpectraSdk.start() now resets the observable LiveData surface (metrics, imageOutput, validationStatus, error, insight) to null before kicking off processing.

Previously, the screening UI inside the SDK called an internal resetMetrics() from its onResume. With the UI moved out and that internal hook removed, callers re-entering a screening would see the previous session’s values flash on screen until fresh metrics arrived. Resetting at the lifecycle boundary (start()) is the right place — it keeps the contract uniform regardless of whether the host app toggles processing in place or navigates away and back.

If your app accumulates derived state from these LiveData (chart buffers, latest-rate caches, etc.), make sure your own clear logic runs on start() or processingStatus == STARTING so it stays in sync with the SDK’s reset. The samples/demo-app and samples/minimal-app already follow this pattern.

Public surface narrowing for cross-platform parity

A small batch of Android-only or asymmetric-with-iOS public API is now internal. None had a non-cosmetic reason to be public; the changes bring the two SDKs closer to a shared contract.

Metrics observers → requested metric bundles

Older Android releases did not expose public requested-metric selection. Apps attached setMetricsBufferObserver for pulse and breathing output, and setEdgeMetricsObserver for edge metrics such as dense face landmarks. The current SDK uses requestedMetrics directly. Public @JvmField bundles on SmartSpectraConfig include breathingMetrics, cardioMetrics, faceMetrics, and edaMetrics. The Metrics suffix matches the C++ SDK’s SmartSpectraConfig::CardioMetrics() etc.; the SmartSpectraConfig namespace matches the iOS Swift surface.

// Before
sdk.setMetricsBufferObserver { metricsBuffer ->
    val pulse = metricsBuffer.pulse.rateList.lastOrNull()?.value
}

// After
sdk.config.requestedMetrics =
    SmartSpectraConfig.breathingMetrics + SmartSpectraConfig.cardioMetrics

SDK permission screen → host-app responsibility

Older integrations usually relied on SmartSpectraView / SmartSpectraButton to present the camera permission flow. Host apps now drive the runtime camera prompt themselves — the modern pattern is ActivityResultLauncher. The SDK still publishes a SmartSpectraError(code = INPUT_UNAVAILABLE, retryable = true) to sdk.error when the validator flags a missing manifest declaration or when start() fails because the runtime permission is denied.

// Before
setContentView(R.layout.activity_main)
val smartSpectraView = findViewById<SmartSpectraView>(R.id.smart_spectra_view)

// After
private val cameraPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission(),
) { granted -> if (granted) startProcessing() }

private fun startProcessing() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED
    ) {
        cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
        return
    }
    lifecycleScope.launch { runCatching { sdk.start() } }
}

ABI checks → SDK-managed error

Older SDK UI checked ABI support before showing the measurement view. The current SDK owns that check and publishes SmartSpectraError(code = CONFIGURATION_FAILED, retryable = false) to sdk.error when initialization detects an unsupported ABI.

// Before
SmartSpectraView(context, attrs) // SDK view handled unsupported ABI UI

// After
sdk.error.observe(this) { error ->
    if (error?.code == SmartSpectraError.Code.CONFIGURATION_FAILED) {
        showUnsupportedDeviceUi()
    }
}

Search quick starts, measurements, and API references.