API changes made in LUNA ID for Android v.1.16.0 in comparison to earlier versions#
This document outlines the changes introduced in LUNA ID for Android v1.16.0 compared to previous versions. Carefully review these updates to ensure a smooth migration and continued functionality in your final application.
Configuration updates#
Removed parameters#
The statusBarColorHex
parameter was removed from ShowCameraParams
because the screen format now uses Edge-to-Edge.
Transferred parameters#
- The
checkSecurity
parameter has been moved fromLunaConfig
toShowCameraParams
. If the parameter is not specified, it is set totrue
by default. - The
videoQuality
parameter has been moved fromShowCameraParams
toLunaConfig
and was renamedLunaVideoQuality
.- Possible values:
SD
,HD
. - Default video quality:
SD
(~640x480 pixels).
- Possible values:
- The
customFrameResolution
parameter has been replaced with:preferredAnalysisFrameWidth
preferredAnalysisFrameHeight
Note: The prefix
preferred
indicates that the user specifies their preferred resolution, which may not always be supported by the device's camera. If unsupported, the system adjusts to the nearest available resolution.
The default frame resolution for analysis is 480x320.
New parameter#
aspectRatioStrategy
-
An enum class (
LunaAspectRatioStrategy
) used to explicitly set the screen aspect ratio.Possible values:
RATIO_4_3_FALLBACK_AUTO_STRATEGY
(default)RATIO_16_9_FALLBACK_AUTO_STRATEGY
Naming changes#
InitBorderDistanceStrategy
is nowBorderDistanceStrategy
.LunaID.activateLicense(..)
is nowLunaID.initEngine(..)
.
Changes in best shot retrieval (multipartBestShotsEnabled)#
The method of retrieving the list of best shots has been updated when multipartBestShotsEnabled
is active.
Before#
The list of best shots was located in the Event.BestShotFound
data class:
data class BestShotFound(
val bestShot: BestShot,
val bestShots: List<BestShot>?,
val videoPath: String?,
val interactionFrames: List<InteractionFrame>?
) : Event()
After#
The list of best shots has been moved to a separate Event
called BestShotsFound
:
data class BestShotsFound(
val bestShots: List<BestShot>?
) : Event()
The new structure of BestShotFound
is as follows:
data class BestShotFound(
val bestShot: BestShot,
val videoPath: String?,
val interactionFrames: List<InteractionFrame>?
) : Event()
To retrieve the list of best shots, use the bestShots
Flow:
LunaID.bestShots.filterNotNull().onEach { bestShotsList ->
Log.e(TAG, "bestShots: ${bestShotsList.bestShots}")
}.
Changes in result retrieval#
Previously, the result could be obtained through the LunaID.finishStates()
Flow
, which returned Event.StateFinished
.
Now, the result can be retrieved via the LunaID.bestShot
Flow
:
val bestShot = MutableStateFlow<Event.BestShotFound?>(null)
This Flow
returns an object of the class Event.BestShotFound
:
data class BestShotFound(
val bestShot: BestShot,
val videoPath: String?,
val interactionFrames: List<InteractionFrame>?
) : Event()
Usage example:
LunaID.bestShot
.filterNotNull()
.onEach { bestShotFound ->
Log.e("BestShotFound", bestShotFound.toString())
}
.launchIn(viewModelScope)
Changes in error retrieval#
You can now obtain errors through errorFlow
:
val errorFlow: Flow<LunaID.Effect.Error>
Usage example:
LunaID.errorFlow
.sample(1000)
.onEach { effect ->
when (effect.error) {
DetectionError.PrimaryFaceLostCritical -> TODO("Handle critical primary face loss")
DetectionError.PrimaryFaceLost -> TODO("Handle primary face loss")
DetectionError.FaceLost -> TODO("Handle face not detected")
DetectionError.TooManyFaces -> TODO("Handle multiple faces detected")
DetectionError.FaceOutOfFrame -> TODO("Handle face out of frame")
DetectionError.FaceDetectSmall -> TODO("Handle small face detection")
DetectionError.BadHeadPose -> TODO("Handle incorrect head pose")
DetectionError.BadQuality -> TODO("Handle poor image quality")
DetectionError.BlurredFace -> TODO("Handle blurred face")
DetectionError.TooDark -> TODO("Handle underexposed image")
DetectionError.TooMuchLight -> TODO("Handle overexposed image")
DetectionError.GlassesOn -> TODO("Handle glasses on face")
DetectionError.OccludedFace -> TODO("Handle partially occluded face")
DetectionError.BadEyesStatus -> TODO("Handle closed or obstructed eyes")
}
}
.launchIn(this.lifecycleScope)
Event subscription updates#
In LUNA ID for Android v.1.16.0, the single Flow handling multiple event types has been replaced with separate Flows for each event category. This modular approach enhances clarity and simplifies event handling.
Event categories:
Category | Description |
---|---|
errorFlow |
Captures errors from LUNA ID. |
currentInteractionType |
Represents the current type of interaction (for example, blinking, head rotation). |
bestShot |
Contains the result of LUNA ID processing (best shot detection). |
videoRecordingResult |
Provides outcomes of video recording operations. |
engineInitStatus |
Indicates the status of engine activation. |
faceDetectionChannel |
Emits face detection events. |
eventChannel |
Captures all other events not included in the above Flows (for example, liveness checks, interaction timeouts). In future updates, this Channel will be further divided into more specific categories. |
bestShots |
Lists all best shots when multipartBestShotsEnabled is active. |
XML Fragment implementation#
Below is an example of how to implement an event subscription using an XML fragment:
class OverlayFragment : Fragment() {
private val viewModel: OverlayViewModel by viewModels()
private var _binding: FragmentOverlayBinding? = null
private val binding get() = _binding!!
companion object {
private const val TAG = "OverlayFragment"
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentOverlayBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Subscribe to current interaction events
viewModel.currentInteraction
.onEach { interaction ->
Log.d(TAG, "onViewCreated: collected interaction $interaction")
_binding?.overlayInteraction?.text = interaction
}
.flowOn(Dispatchers.Main)
.launchIn(lifecycleScope)
// Subscribe to error state events
viewModel.errorState.onEach { error ->
binding.overlayError.text = error
}.launchIn(this.lifecycleScope)
// Handle other LunaID events
LunaID.eventChannel.receiveAsFlow()
.onEach { event ->
when (event) {
is LunaID.Event.SecurityCheck.Success -> {
Log.d(TAG, "onViewCreated() collect security SUCCESS")
}
is LunaID.Event.SecurityCheck.Failure -> {
Log.d(TAG, "onViewCreated() collect security FAILURE")
}
is LunaID.Event.FaceFound -> {
Log.d(TAG, "onViewCreated() face found")
}
is LunaID.Event.InteractionEnded -> {
Log.d(TAG, "onViewCreated() interaction ended")
}
is LunaID.Event.InteractionFailed -> {
Log.d(TAG, "onViewCreated() interaction failed")
}
is LunaID.Event.InteractionTimeout -> {
Log.d(TAG, "onViewCreated() interaction timeout")
Toast.makeText(this.activity, "Interaction timeout", Toast.LENGTH_LONG).show()
activity?.finish()
}
is LunaID.Event.LivenessCheckError -> {
Log.d(TAG, "onViewCreated() liveness check error ${event.cause}")
}
is LunaID.Event.LivenessCheckFailed -> {
Log.d(TAG, "onViewCreated() Liveness Check Failed")
activity?.finish()
Toast.makeText(this.activity, "liveness check error", Toast.LENGTH_LONG).show()
}
is LunaID.Event.LivenessCheckStarted -> {
Log.d(TAG, "onViewCreated() liveness check started")
}
is LunaID.Event.Started -> {
Log.d(TAG, "onViewCreated() started")
}
is LunaID.Event.UnknownError -> {
Log.d(TAG, "onViewCreated() unknown error ${event.cause}")
}
else -> {
Log.d(TAG, "onViewCreated() collected unknown event")
}
}
}
.launchIn(this.lifecycleScope)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Compose implementation#
Here’s an example of implementing an event subscription using Jetpack Compose:
class OverlayComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr), MeasureBorderDistances {
private var innerBoxPosition by mutableStateOf(Offset.Zero)
@Composable
override fun Content() {
val viewModel: OverlayViewModel =
ViewModelProvider(context as ViewModelStoreOwner)[OverlayViewModel::class.java]
val interactionState = viewModel.currentInteraction.onStart { delay(1000) }.collectAsState("")
val errorState = viewModel.errorState.onStart { delay(1000) }.collectAsState("")
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
if (true) {
Box(
modifier = Modifier
.size(256.dp)
.border(BorderStroke(4.dp, Color.White))
.onGloballyPositioned { coordinates ->
innerBoxPosition = coordinates.localToWindow(Offset.Zero)
}
)
}
}
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
Spacer(modifier = Modifier.weight(4f))
// Display error messages
Text(
modifier = Modifier.fillMaxWidth(),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
text = errorState.value,
color = MaterialTheme.colorScheme.error,
)
Spacer(modifier = Modifier.size(8.dp))
// Display interaction messages
Text(
modifier = Modifier.fillMaxWidth(),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
text = interactionState.value,
color = Color.Yellow,
)
Spacer(modifier = Modifier.weight(1f))
}
}
override fun measureBorderDistances(): BorderDistancesInPx {
Log.d("OverlayComposeView", "x=${innerBoxPosition.x} y=${innerBoxPosition.y}")
val fromLeft = innerBoxPosition.x.toInt()
val fromTop = innerBoxPosition.y.toInt()
val fromRight = fromLeft
val fromBottom = fromTop
Log.d(
"OverlayComposeView",
"fromLeft=$fromLeft fromTop=$fromTop fromRight=$fromRight fromBottom=$fromBottom"
)
return BorderDistancesInPx(
fromLeft = fromLeft,
fromTop = fromTop,
fromRight = fromRight,
fromBottom = fromBottom
)
}
}
ViewModel for both UI variants#
The following ViewModel
can be used for both Compose and XML implementations:
class OverlayViewModel(application: Application) : AndroidViewModel(application) {
val currentInteraction = LunaID.currentInteractionType
.filterNotNull()
.map { Interaction.message(application.applicationContext, it) }
.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(1000), "")
private val _errorState = MutableStateFlow("")
val errorState = _errorState.asStateFlow()
var job: Job? = null
init {
LunaID.errorFlow
.onEach { event ->
val text = application.applicationContext.getString(event.error.messageResId()!!)
updateTextAndClearLater(text)
}
.launchIn(viewModelScope)
}
suspend fun updateTextAndClearLater(text: String) {
Log.d("OverlayViewModel", "updateTextAndClearLater: with text $text")
job?.cancel()
_errorState.update { text }
job = viewModelScope.launch {
delay(1000)
_errorState.update { "" }
}
}
}