Skip to content

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 from LunaConfig to ShowCameraParams. If the parameter is not specified, it is set to true by default.
  • The videoQuality parameter has been moved from ShowCameraParams to LunaConfig and was renamed LunaVideoQuality.
    • Possible values: SD, HD.
    • Default video quality: SD (~640x480 pixels).
  • 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 now BorderDistanceStrategy.
  • LunaID.activateLicense(..) is now LunaID.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 { "" }
        }
    }
}