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_FAILEDfor rejected credentialsCREDIT_EXHAUSTEDfor an explicit quota denialNETWORK_ERRORwhen 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
Metricsis now the single vitals data source
Field Mappings
Old (metricsBuffer) | New (metrics) |
|---|---|
pulse.rateList | cardio.pulseRateList |
pulse.traceList | cardio.arterialPressureTraceList |
breathing.rateList | breathing.rateList |
breathing.upperTraceList | breathing.upperTraceList |
breathing.lowerTraceList | breathing.lowerTraceList |
face | face |
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.metricsBuffersetMetricsBufferObserver()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:
| Before | After |
|---|---|
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
IDLESTARTINGRUNNINGSTOPPINGERROR
Enum Mapping
| Previous value | New value |
|---|---|
IDLE | IDLE |
COUNTDOWN | STARTING |
RUNNING | RUNNING |
PREPROCESSED | RUNNING |
DONE | RUNNING |
DISABLE | IDLE |
ERROR | ERROR |
Semantic Updates
RUNNINGmeans the pipeline is active and measurement is runningSTOPPINGis a transient shutdown state on the path back toIDLE
// 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,SmartSpectraResultViewSmartSpectraActivity,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 internalSmartSpectraVitalsProcessorkeepstimeLeft/hintTextfor instrumented tests; the SDK class no longer mirrors them. - Config flags removed from
SmartSpectraConfig(all wereinternal):showFps,showOutputFps,showControlsInScreeningView - Derived helpers
cardioMeasurementsEnabled,facialExpressionEnabled,edaMeasurementsEnabled(replicate app-side fromrequestedMetricsif 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()
}
}