Saltar a contenido

Biometric ID - SDK Android

En este documento encontrará una explicación de cómo se realiza la integración con el SDK Android BiometricID

Última versión estable: 1.0.195

Pre requisitos

Para hacer uso del SDK se debe contar con

ApiKey válido: {{ API_KEY }}
Usuario Maven: {{ USUARIO_MAVEN }}
Password Maven: {{ PASSWORD_MAVEN }}
Correo de usuario creado desde el selfservice: {{ USUARIO_SELFSERVICE }}

Crear un Api Key

Para usar el sdk se necesita tener un api key válido, en caso de que tengas uno omite este paso

  1. Inicia sesión en el selfservice con tus credenciales

  2. Da click en la sección de Api Keys

  3. Da click agregar apikey

  4. Ingresa un nombre y selecciona el tipo sdk android por último escribe el application Id de tu proyecto

Instalación

Aplicación demo

Ponemos a su disposición una aplicación de ejemplo en un repositorio privado. Para clonarla, utilice el token de acceso (PAT) que le fue entregado; este corresponde al mismo {{ PASSWORD_MAVEN }} con el que accede a los artefactos de Maven:

git clone https://{{ PASSWORD_MAVEN }}@dev.azure.com/getlatam/biometricid-android-demo/_git/biometricid-android-demo

Token de acceso

El token se entrega de forma privada y permite tanto clonar este repositorio como descargar los artefactos de Maven desde el selfservice. No lo comparta ni lo publique en repositorios públicos.

Instalacion del SDK

1 . Vamos a settings.gradle y en la sección de repositories añadimos la sección de maven:

dependencyResolutionManagement {

repositories {
    google()
    mavenCentral()
    maven {
        url 'https://pkgs.dev.azure.com/getlatam/Racsa/_packaging/BiometricID/maven/v1'
        name 'BiometricID'
        credentials {
            username "{{ USUARIO_MAVEN }}"
            password "{{ PASSWORD_MAVEN }}"
        }
        authentication {
            basic(BasicAuthentication)
        }
    }
}

2 . En el build.gradle de la app en la sección de android colocamos el minSdk con un valor minimo de 22 en compileSdk y targetSdk 34, además debemos habilitar multiDex, viewBinding y añadimos las versiones kotlin

android {
    compileSdk 34
    defaultConfig {
        minSdk 22
        targetSdk 34

        ...

        multiDexEnabled true
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
        viewBinding true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.5.1'
    }
}

3 . En el build.gradle de la app en la sección de android añadimos la sección packagingOptions

android {
    ...
    packagingOptions {
        pickFirst 'lib/x86/libc++_shared.so'
        pickFirst 'lib/x86_64/libc++_shared.so'
        pickFirst 'lib/armeabi-v7a/libc++_shared.so'
        pickFirst 'lib/arm64-v8a/libc++_shared.so'
        pickFirst 'lib/armeabi-v7a/libopencv_java4.so'
        pickFirst 'lib/arm64-v8a/libopencv_java4.so'
        pickFirst 'lib/arm64-v8a/libncnn.so'
        pickFirst 'lib/armeabi-v7a/libncnn.so'
    }
}

4 . En el build.gradle de la app en la sección de dependencies añadimos la dependencia del sdk y unas dependencias adicionales

dependencies {
    //sdk
    implementation(group: 'com.getgroup', name: 'integraid-sdk', version: '1.0.195')

    //dependencias adicionales requeridas
    implementation(group: 'getgroup.integraid', name: 'integraid-java-client', version: '1.0.24191.1')
    implementation(group: 'com.getgroup', name: 'opencv', version: '4.6.0')

    //android x requeridas
    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'androidx.appcompat:appcompat:1.6.0'
    implementation 'com.google.android.material:material:1.8.0'
}

Instalación dagger

El sdk necesita instalar dagger para trabajar con inyección de dependencias

1 . En el build.gradle de la app en la sección dependencies añadimos

dependencies {
    //inyección de dependencias
    implementation "com.google.dagger:hilt-android:2.42"
    kapt "com.google.dagger:hilt-compiler:2.42"
    kapt "androidx.hilt:hilt-compiler:1.0.0"
}

2 . En el build.gradle de la app en la sección plugins añadimos

plugins {
    //inyeccion de dependencias
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'

    //plugins requeridos
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

3 . En el build.gradle de la app en la sección defaultConfig añadimos

android {
    defaultConfig {
        //inyeccion de dependencias
        javaCompileOptions.annotationProcessorOptions.arguments['dagger.hilt.disableCrossCompilationRootValidation'] = 'true'
    }
}

4 . En el build.gradle del proyecto añadimos

plugins {
    id 'com.android.application' version '8.2.0' apply false
    id 'com.android.library' version '8.2.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
}

5 . En el settings.gradle en la sección pluginManagement

pluginManagement{
    //inyeccion de dependencias
    resolutionStrategy {
        eachPlugin {
            if( requested.id.id == 'dagger.hilt.android.plugin') {
                useModule("com.google.dagger:hilt-android-gradle-plugin:2.39.1")
            }
        }
    }
}

Inyectar la configuración al sdk

1 . Creamos una clase llamada App que hereda de ApplicationBase y vamos a sobreescribir el metodo getSdkConfigModule

import com.getgroup.integraidsdk.ApplicationBase
import com.getgroup.integraidsdk.Domain.SdkConfig.SdkConfigModule
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class App : ApplicationBase() {

   //para sobreescribir la configuración
   override fun getSdkConfigModule(): SdkConfigModule {
       return AppConfigModule(this)
   }
}

2 . Creamos la clase AppConfigModule

import android.app.Application
import com.getgroup.integraidsdk.Domain.SdkConfig.SdkConfig
import com.getgroup.integraidsdk.Domain.SdkConfig.SdkConfigModule

class AppConfigModule(private val context: Application) : SdkConfigModule() {
   override fun provideSdkConfig(): SdkConfig {
       return SdkConfig(
            UrlBaseApi = "{URL_API}",
            ShowScore = false,
            IsDebug = false,
            TimeOutApi = 20 * 1000,
            TimeOutMostrarDocumento = 5 * 60 * 1000,
            ApiKey = "{API_KEY}",
            WriteLogsConsola = false,
            WriteLogsDispositivo = false,
            UrlTerminosCondiciones = "{URL_TERMINOS}",
            LivenessEscaneoFacial = 0.8,
            ScoreMinimoEscaneoFacial = 40f,
            LivenessScoreMinimoEscaneoHuella = 0.5f,
            Nist2qualityMinimoEscaneoHuella = 40,
            HuellaWidth = 800,
            HuellaHeight = 800,
            HuellaCompressionRatio = 0f,
       )
   }
}

3 . Vamos al manifest y asociamos la clase de la app como el contexto en la sección de application

<application
    android:name=".App"
    tools:replace="android:name,android:theme"
...

Compatibilidad con android con Android Studio Narwhal

En caso de presentarse errores de compilación se debe realizar la siguiente configuración:

  • Ir al menu File, settings, Build Tools, Gradle, en el gradle jdk se debe seleccionar la versión 17

  • En el archivo gradle.properties se debe añadir lo siguente:

org.gradle.jvmargs=--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
kapt.use.worker.api=false
kapt.include.compile.classpath=false
  • Limpiar cache: Ir al menu File, Ivalidate caches, seleccionar todas las opciones y click en el botón Invalidate and restart

Definiciones

Variables de configuración (SdkConfig)

Nombre Tipo Descripción
UrlBaseApi String Url del api https://apibiometricid.racsa.go.cr/bioapi
ShowScore bool Si esta en true cuando falle la verificación biometrica mostrará el score o puntaje
IsDebug bool Para definir si la app esta en debug
TimeOutApi int Asigna el tiempo máximo de espera en la conexión con el api, el valor recomendado es de 20000 milisegundos oséa 20 segundos
TimeOutMostrarDocumento long Asigna el tiempo máximo que permitirá mostrar la información del documento consultado en InformacionPersonalView
ApiKey string El token generado por el self service que tiene asociado el applicationId del proyecto, el apikey permite realizar peticiones al api
WriteLogsConsola bool Si está en true escribirá logs en la consola
WriteLogsDispositivo bool si está en true escribirá logs en el dispositivo, sin embargo se deberán declarar permisos de lectura y escritura en el manifest
UrlTerminosCondiciones string Asigna la url de terminos y condiciones que se mostrará al usuario cuando realice una consulta de documento
LivenessEscaneoFacial double Valor entre 0 y 1 entre más alto el número verificará que el escaneo se le tome a una persona para evitar fraudes, el valor recomendado es 0.8
LivenessEscaneoHuella float Valor entre 0 y 1 entre más alto el número verificará que el escaneo se le tome a una huella real para evitar fraudes
ScoreMinimoEscaneoFacial float Valor entre 0f y 100f, es el score o puntuación mínima local de rostro que realiza el sdk antes de enviarlo al api
ScoreMinimoEscaneoHuella float Valor entre 0f y 1f, es el score o puntuación mínima local de huellas que realiza el sdk antes de enviarlo al api
HuellaWidth int Ancho de la imagen de la huella que será enviada
HuellaHeight int Alto de la imagen de la huella que será enviada
HuellaCompressionRatio float valor entre 0f y 1f, que corresponde al nivel de compresión de la imagen de la huella
SkipHuellaExplicacion bool Si está en true (valor por defecto) omite la pantalla de instrucciones de huellas e inicia el escaneo directamente. Si está en false muestra la pantalla de instrucciones antes de escanear.

Clases

Nombre Descripción Propiedades
Documento Clase que muestra la información básica de un documento consultado Nombre: String, Apellido: String, SegundoApellido: String, Huellas: List<HuellaEscanearEnum?>, IsVerificacionFacial: Boolean, TipoDocumentoEnum:TipoDocumentoEnum, TransactionId:String
TipoDocumentoEnum Enumerador con el tipo de documento NACIONAL, EXTRANJERO
ResponseException clase que trae las excepciones internas y del api code: Int, error: String, response: Any?,, responseBody: String?, transactionId: String, extras: Any?
HuellaEscanearEnum Enumerador con los tipos de huella para escanear PULGAR_DERECHO = 1, PULGAR_IZQUIERDO = 6, INDICE_DERECHO= 2, INDICE_IZQUIERDO = 7, MEDIO_DERECHO = 3, MEDIO_IZQUIERDO = 8, ANULAR_DERECHO = 4, ANULAR_IZQUIERDO= 9, MENIQUE_DERECHO = 5, MENIQUE_IZQUIERDO = 10
VerificarFacialHuella clase para realizar la verificación biometrica (facial o huella) Token: String, Documento: String, NombreCompleto: String, Huella: HuellaEscanearEnum, PathHuellaWsq: String, PathFoto: String
InformacionPersonal clase de información personal de un documento consultado y que haya pasado la verificación biometrica TransactionId: String, ImagenDocumento: String, FechaExpiracion: String, NombreCompleto: String, Documento: String, ListFuentesExternas:List<FuenteExterna>
FuenteExterna clase de fuente externa disponible para consultar Id: Int, Nombre: String, Seleccionada: Boolean

Códigos de error

Cada variable se encuentra en la clase ApiConstants

Nombre Código Explicación
RESPONSE_OK 200 Todo fue correcto
RESPONSE_NOT_FOUND 404 No encontró el documento consultado
RESPONSE_ERROR_VALIDATIONS 400 Errores de validación, por ejemplo no se están enviando datos con el formato correcto
RESPONSE_INTERNAL_ERROR 500 Error interno del servidor
RESPONSE_UNAUTHORIZARED 401 Petición sin autorización, el token de consulta de documento expiro ó se esta enviando un Api Key sin autorización
RESPONSE_UNAUTHORIZARED_FORBIDDEN 403 Se esta enviando un Api Key inválido
RESPONSE_IMAGES_NOT_FOUND 512 Error local de la aplicación que ocurre cuando se perdió el cache de la verificación facial o huella, se debe realizar una nueva verificación

Permisos

El sdk ya tiene definidos los permisos y no es necesario declararlos en el manifest de tu proyecto sin embargo a continuación se muestran

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />

Diagrama de navegación

Cada interfaz (activity) debe usar un view que se encarga de la logica de negocio, a continuación el flujo de los activities por defecto con el nombre del view que utliza

Activities por defecto

Para invocar el flujo de navegación por defecto debes hacer un intent a ConsultarDocumentoActivity

val intent = Intent(this, ConsultarDocumentoActivity::class.java)
startActivity(intent)

Crea tus activities

Activity

Cada activity debe heredar de BaseActivity y arriba de la clase colocar @AndroidEntryPoint para que recibir las dependencias del sdk por ejemplo:

@AndroidEntryPoint
class CustomActivity : BaseActivity() {
}

xml

En el xml del activity se debe de usar el view deseado

<com.getgroup.integraidsdk.Ui.ViewDeseado
   android:id="@+id/identificador"
   android:layout_width="match_parent"
   android:layout_height="wrap_content" />

Evento

Cada view tiene una función init donde se le pasa un evento que te notificará para que puedas tomar acción, por ejemplo:

private val event = object : Evento{
   override fun onEvento(): Boolean {
       return super.onEvento()
   }
}

Consultar documento

Este view permite realizar la consulta de un documento especificando el tipo de documento y aceptando los términos y condiciones, existen 2 views que solo cambiará la manera de mostrar los tipos de documento

paquete view en forma seleccionable: com.getgroup.integraidsdk.Ui.ConsultarDocumento.ConsultarDocumentoView

paquete view en forma de lista: com.getgroup.integraidsdk.Ui.ConsultarDocumento.ConsultarDocumentoSpinnerListView

Método Descripción
init(event: ConsultarDocumentoViewEvent?, accessToken:string?, correoUsuario:String?) Se llama para inicializar el view, se le pasa el evento y el correo del usuario {USUARIO_SELFSERVICE} que está realizando la petición (El accessToken se envía null)
OnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) Se debe llamar en el onActivityResult del activity donde se usa el view, pasandole los mismos argumentos
mostrarError(ex: ResponseException?) Permite mostrar un error enviando una instancia de tipo ResponseException
mostrarError(text: String) Permite mostrar un error enviando una cadena de texto

ConsultarDocumentoViewEvent

Método Descripción
onLoader(isVisible: Boolean) Método que notifica que si se debe o no mostrar un loader
onResultTiposDocumento(result: Result<List<TipoDocumentoEnum>>) Método que traerá un resultado la lista de tipos de documento disponibles para consultar o un error. Para saber si todo fue correcto preguntar result.isSuccess, para obtener la lista de tipos de documento result.getOrNull(), para obtener el error val error = result.exceptionOrNull() as ResponseException. NOTA: se debe tener en cuenta que los tipos de documento que aparecerán en el View serán los que estén configurados en el tipo de suscripción al que pertenece el cliente del Selfservice, en caso de no tener algúno habilitado entonces comunicarse con el administrador.
onResult(result: Result<Documento?>) Método que traerá un resultado de Documento o un error. Para saber si todo fue correcto preguntar result.isSuccess, para obtener el documento result.getOrNull(), para obtener el error val error = result.exceptionOrNull() as ResponseException
onEndShowError(code: Int) Método que notifica una vez se haya terminado de mostrar un error desde el view mConsultarDocumentoView.mostrarError(error)

Documento encontrado

Este view muestra el documento encontrado y las opciones para la verificación facial o huella

paquete: com.getgroup.integraidsdk.Ui.DocumentoEncontrado.DocumentoEncontradoView

Método Descripción
init(context: Activity, model: Documento, event: DocumentoEncontradoEvent?, escanearHuellaMano:Bool) inicializa el view, se le debe enviar el activity, un objeto de tipo Documento, el evento de tipo DocumentoEncontradoEvent y se pasa un valor booleano para el parametro escanearHuellaMano en caso de que sea true el sdk pide que el usuario ponga su mano y solo escanea la huella que se haya solicitado, por defecto su valor es false entonces solo se escanea el dedo requerido
OnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) Se debe llamar en el onActivityResult del activity enviando los mismos argumentos

DocumentoEncontradoEvent

Método Descripción
onEscaneoFacial(): Boolean Cuando se le da click a la verificación facial llega aquí, si se retorna el metodo super().onEscaneoFacial() entonces continua, en caso de retornar false no permite continuar con el escaneo facial
onEscaneoHuella(huella: HuellaEscanearEnum?): Boolean Al dar click para escanear una huella llega aquí y entrega la huella que va a escanear, si return super().onEscaneoHuella() continua si return false entonces no realiza el escaneo
onVerificarEscaneoFacial(model: VerificarFacialHuella): Boolean una vez realiza el escaneo facial o por huella si pasa el score mínimo local que se le ha configurado al sdk llega aquí, entrega un objeto para realizar la verificación biométrica, para realizar la verificación con una activity propia se retorna false
onShowMensajes(mensaje: String) cada vez que el view necesite mostrar un mensaje llega aquí una cadena que se puede mostrar con un Toast por ejemplo

Verificación biométrica (facial o huella)

Es un view que se comunica con el api enviando la foto o huella y retorna la información personal con las fuentes externas disponibles

paquete: com.getgroup.integraidsdk.Ui.VerificacionFacialHuella.VerificacionFacialHuellaView

Método Descripción
init(event: VerificacionFacialHuellaEvent?) para inicializar el view, se le debe enviar un evento de tipo VerificacionFacialHuellaEvent
verificar(model: VerificarFacialHuella) para realizar la verificación biometrica (por huella o facial), se le debe enviar el objeto que se extrae de DocumentoEncontradoView

VerificacionFacialHuellaEvent

Método Descripción
onLoader(isVisible: Boolean) cada vez que el view necesite mostrar o no un loader
onError(mensaje: String, error: ResponseException?) aquí llegarán los errores como texto o como un objeto ResponseException
onResult(result: Result) metodo que traera un objeto de tipo InformacionPersonal, para obtener el resultado val informacionPersonal = result.getOrNull() as InformacionPersonal

Nota

No se recomienda enviar el modelo a otro activity con un intent usando extras, debido a que la longitud de la imagen en base64 puede generar una excepción

Ejemplo enviar y recibir información personal
//Enviar información como una variable estática

val informacionPersonal = result.getOrNull() as InformacionPersonal
InformacionPersonalActivity.EXTRAS.MODEL = informacionPersonal
val intent = Intent(baseContext, InformacionPersonalActivity::class.java)
startActivity(intent)

//Recibir información de la variable estática

@AndroidEntryPoint
class InformacionPersonalActivity : BaseActivity() {
object EXTRAS {
    var MODEL: InformacionPersonal? = null
}
private var model: InformacionPersonal? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    model = EXTRAS.MODEL
    if (model == null) {
        finish()
        showToast("No se recibió la información correctamente")
        return
    }
    binding.mInformacionPersonalView.init(event, model!!)
}

Información personal

Muestra la información personal de un documento consultado (Nombres, Apellidos, Foto del documento, fecha de expiración y la opción de zoom en el documento)

paquete: com.getgroup.integraidsdk.Ui.InformacionPersonal.InformacionPersonalView

Método Descripción
init(event: InformacionPersonalEvent?, model: InformacionPersonal) inicializa el view, se debe pasar un evento de tipo InformacionPersonalEvent y el objeto InformacionPersonal extraído de VerificacionFacialHuellaView.
Iniciara un temporizador con el tiempo que se haya configurado el sdk
destroy() detiene el temporizador y destruye el view

InformacionPersonalEvent

Método Descripción
onMostrarFuentesExternas(model: InformacionPersonal): Boolean cuando hay fuentes externas disponibles, el usuario puede dar click para verlas y primero pasará por aquí, para mostrarlas en un activity propio retornar false

Fuentes externas

Muestra la lista de fuentes externas disponibles de un usuario, cuando el usuario de expanda la fuente externa realiza la consulta y se cargan los resultados con la posibilidad de ocultarlos

paquete: com.getgroup.integraidsdk.Ui.FuentesExternas.View.FuentesExternasView

Método Descripción
init(event: FuentesExternasEvent?, model:InformacionPersonal) inicializa el view, se le pasa un evento de tipo FuentesExternasEvent y el objeto InformacionPersonal que se extrae de InformacionPersonalView

FuentesExternasEvent

Método Descripción
onQuery(model:FuenteExterna) Evento que notifica la fuente externa que se esta consultando
onResultErrorQuery(message: String, error: ResponseException?, fuenteExterna: FuenteExterna) Evento que notifica excepciones durante la consulta de una fuente externa
onResultQuery(result: FuenteExternaResponseOk, fuenteExterna: FuenteExterna) Evento que notifica trae el resultado de la consulta y la fuente externa asociada

Casos de uso

En caso de querer utilizar la lógica sin interactuar con los views se puede hacer utilizar los casos de uso, para lograrlo se debe crear un ViewModel y solicitar el caso de uso desde el constructor

Ejemplo de ViewModel

//Se crea un evento para notificar del resultado
interface CustomEvent {
    fun onTiposDocumentoRecived(result: List<TipoDocumentoEnum>)
    fun onDocumentoRecived(result: Documento)
    fun onErrorRecived(exception: Throwable)
}
//Se crea el ViewModel para interactuar con los casos de uso
@HiltViewModel
class CustomViewModel @Inject constructor(
    @Named(DI.CONSULTAR_DOCUMENTO_USECASE) val useCase: IConsultarDocumentoUseCase,
) : BaseViewModel() {

    private val TAG = "CustomViewModel"
    private var event: CustomEvent? = null

    fun init(event: CustomEvent){
        this.event = event
    }

    fun consultarTiposDocumento(context: Context) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val result = useCase.ConsultarTiposDocumento(accessToken = null)
                if (!result.isSuccess) {
                    val exception = result.exceptionOrNull()?:Exception("Ocurrio un error")
                    //Manejar la exception
                    LogsUtils.log(context, TAG, exception)
                    return@launch
                }
                val tiposDocumento = result.getOrNull() as List<TipoDocumentoEnum>
                //Manejar el resultado actualizando la UI en caso de ser necesario
                when (context) {
                    is Activity -> context.runOnUiThread {
                        event?.onTiposDocumentoRecived(tiposDocumento)
                    }
                    else -> {
                        event?.onTiposDocumentoRecived(tiposDocumento)
                    }
                }
            } catch (ex: Exception) {
                LogsUtils.log(context, TAG, ex)
            }
        }
    }

    fun consultarDocumento(context: Context, tipoDocumento: TipoDocumentoEnum, numeroDocumento:String) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val result = useCase.Consultar(tipo=tipoDocumento, documento = numeroDocumento, correoUsuario = BuildConfig.USER_EMAIL, accessToken = null)
                if (!result.isSuccess) {
                    val exception = result.exceptionOrNull()?:Exception("Ocurrio un error")
                    //Manejar la exception
                    LogsUtils.log(context, TAG, exception)
                    when (context) {
                        is Activity -> context.runOnUiThread {
                            event?.onErrorRecived(exception)
                        }
                        else -> {
                            event?.onErrorRecived(exception)
                        }
                    }
                    return@launch
                }
                val documento = result.getOrNull() as Documento
                //Manejar el resultado actualizando la UI en caso de ser necesario
                when (context) {
                    is Activity -> context.runOnUiThread {
                        event?.onDocumentoRecived(documento)
                    }
                    else -> {
                        event?.onDocumentoRecived(documento)
                    }
                }
            } catch (ex: Exception) {
                LogsUtils.log(context, TAG, ex)
            }
        }
    }
}

Posteriormente desde el Activity se obtiene la instancia del ViewModel y se realiza el llamado al metodo correspondiente

@AndroidEntryPoint
class ConsultarDocumentoCasoUsoActivity : BaseActivity() {

    private lateinit var binding: ActivityConsultarDocumentoCasosUsoBinding
    private val viewModel: CustomViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityConsultarDocumentoCasosUsoBinding.inflate(layoutInflater)
        setContentView(binding.root)
        viewModel.init(event)
        binding.btnConsultarTiposDocumento.setOnClickListener {
            viewModel.consultarTiposDocumento(baseContext)
        }
        binding.btnConsultarExtranjero.setOnClickListener {
            viewModel.consultarDocumento(baseContext, TipoDocumentoEnum.EXTRANJERO, binding.txtDocumento.text.toString())
        }
        binding.btnConsultarNacional.setOnClickListener {
            viewModel.consultarDocumento(baseContext, TipoDocumentoEnum.NACIONAL, binding.txtDocumento.text.toString())
        }
    }

    private val event = object : CustomEvent {
        override fun onTiposDocumentoRecived(result: List<TipoDocumentoEnum>) {
            binding.txtResultado.setTextColor(getColor(com.getgroup.demo.R.color.green))
            binding.txtResultado.text = JsonConvertUtils.serialize(result)
        }

        override fun onDocumentoRecived(result: Documento) {
            binding.txtResultado.setTextColor(getColor(com.getgroup.demo.R.color.green))
            binding.txtResultado.text = JsonConvertUtils.serialize(result)
            //continuar el flujo por medio de los views
            /*val intent =
                Intent(baseContext, DocumentoEncontradoManualActivity::class.java).apply {
                    putExtra(
                        DocumentoEncontradoManualActivity.EXTRAS.EXTRA_JSON,
                        JsonConvertUtils.serialize(result)
                    )
                }
            startActivity(intent)*/
        }

        override fun onErrorRecived(exception: Throwable) {
            binding.txtResultado.setTextColor(getColor(com.getgroup.demo.R.color.red))
            if (exception is ResponseException) {
                val responseException = exception as ResponseException
                //Se entregan más detalles de la excepcion
            }
            binding.txtResultado.text = JsonConvertUtils.serialize(exception)
        }

    }
}

Consultar Documento UseCase

Dependencia

    @Named(DI.CONSULTAR_DOCUMENTO_USECASE) private val useCase: IConsultarDocumentoUseCase

Funciones

  • Consultar tipos de documentos
    suspend fun ConsultarTiposDocumento(
        accessToken: String? //Token de acceso se envia null
    ): Result<List<TipoDocumentoEnum>>
  • Consultar documento
suspend fun Consultar(
        tipo: TipoDocumentoEnum, //TipoDocumentoEnum.NACIONAL | TipoDocumentoEnum.EXTRANJERO
        documento: String, //Número de documento
        accessToken: String?, //Token de acceso se envia null
        correoUsuario: String?, //Enviar el correo de usuario que está realizando la solicitud
    ): Result<Documento>

Escaneo Facial UseCase

Dependencia

    @Named(DI.ESCANEO_FACIAL_USECASE) val useCase: IEscaneoFacialUseCase

Funciones

  • Escanear
    fun EscaneoFacial(
        context: Activity,//Activity que realiza la solicitud
        listener: FaceCapturedListener?, //Evento para recibir la respuesta del escaneo
        usarCamaraFrontal: Boolean, //true | false
    )

    interface FaceCapturedListener {
        fun OnError(errorMessage: String, details: FaceCapturedDetail?)// Fallo el escaneo o no se cumplio el score requerido
        fun OnResult(keyImage: String, details: FaceCapturedDetail)// Escaneo correcto
    }

    class FaceCapturedDetail(
        val score: Float,// Score resultante del escaneo
        val scoreLimit: Float,// Limite configurado sdkConfig.ScoreMinimoEscaneoFacial
    )

Ejemplo

useCase.EscaneoFacial(this, mFaceCaptureListener, true)

private var mFaceCaptureListener = object : FaceCapturedListener{
        override fun OnError(
            errorMessage: String,
            details: FaceCapturedDetail?
        ) {
            val message = if(!MainUtils.isNullOrEmpty(errorMessage)) errorMessage else "Intenta de nuevo por favor"
            showMensajes(
                message
            )
        }

        override fun OnResult(
            keyImage: String,
            details: FaceCapturedDetail
        ) {

            val nombreCompleto =
                modelDocumento.Nombre + " " + modelDocumento.Apellido + " " + modelDocumento.SegundoApellido
            val verificarFacialHuellaModel = VerificarFacialHuella(
                modelDocumento.Token,
                modelDocumento.Documento,
                nombreCompleto,
                null,
                "",
                keyImage
            )

            val doAction =
                event?.onVerificarEscaneoFacial(verificarFacialHuellaModel) ?: true

            //continuar con la verificación
            useCaseVerificacionFacialHuella.VerificarIntent(
                    _uiState.value.activity!!,
                    verificarFacialHuellaModel,
                    _uiState.value.showScore
            )
        }

Escaneo Huellas UseCase

Dependencia

    @Named(DI.ESCANEO_HUELLA_USECASE) val useCase: IEscaneoHuellaUseCase

Funciones

  • Escanear huella

    fun EscanearHuella(
        context: Activity,//Activity que realiza la solicitud
        documento: String,//Número de documento
        huella: HuellaEscanearEnum?,//Tipo de huella solicitada
        listener: FingerCapturedListener?,//Evento para recibir la respuesta del escaneo
    ): Result<Boolean>
- Escanear huella pero colocando varios dedos

    fun EscanearHuellaMano(
        context: Activity,//Activity que realiza la solicitud
        documento: String,//Número de documento
        huella: HuellaEscanearEnum?,//Tipo de huella solicitada
        listener: FingerCapturedListener?,//Evento para recibir la respuesta del escaneo
    ): Result<Boolean>
interface FingerCapturedListener {
    fun OnError(errorMessage: String, details: FingerCapturedDetail?) // Fallo el escaneo o no se cumplio el score o calidad requerida
    fun OnResult(keyImage: String, details: FingerCapturedDetail) // Escaneo correcto
}

class FingerCapturedDetail(
    val score: Float,// score resultante
    val scoreLimit: Float,// sdkConfig.LivenessScoreMinimoEscaneoHuella
    val quality: Int,// quality resultante
    val qualityLimit: Int// sdkConfig.QualityMinimoEscaneoHuella
)

Ejemplo

useCase.EscanearHuellaMano(this, modelDocumento.Documento, modelDocumento.Huellas[0], mFingerCaptureListener, true)

private var mFingerCaptureListener = object : FingerCapturedListener {
        override fun OnError(
            errorMessage: String,
            details: FingerCapturedDetail?
        ) {
            val message = if(!MainUtils.isNullOrEmpty(errorMessage)) errorMessage else "Intenta de nuevo por favor"
            showMensajes(
                message
            )
        }

        override fun OnResult(
            keyImage: String,
            details: FingerCapturedDetail
        ) {
            val nombreCompleto =
                modelDocumento.Nombre + " " + modelDocumento.Apellido + " " + modelDocumento.SegundoApellido
            val verificarFacialHuellaModel = VerificarFacialHuella(
                modelDocumento.Token,
                modelDocumento.Documento,
                nombreCompleto,
                _uiState.value.huellaEscanear,
                keyImage,
                ""
            )
            //Continuar con la verificación
            useCaseVerificacionFacialHuella.VerificarIntent(
                    _uiState.value.activity!!,
                    verificarFacialHuellaModel,
                    _uiState.value.showScore
            )
        }
    }

IImagesUseCase

Para realizar la verificación facial o huellas no envia directamente la imagen sino una llave (key) en caso de querer obtener la imagen en base64 se hace por medio de la dependencia IImagesUseCase

interface IImagesUseCase {
    fun set(base64: String?): String //Se envia el base64 y retorna la key
    fun get(key: String): String//Para obtener la imagen en base64 por medio de la key
}

Dependencia

@Named(DI.IMAGES_USECASE) val useCase: IImagesUseCase

Verificacion Facial o Huella UseCase

Dependencia

    @Named(DI.ESCANEO_HUELLA_USECASE) private val useCase: IVerificacionFacialHuellaUseCase

Funciones

  • Verificar
    class VerificarFacialHuella(
        var Token: String="",//Token
        var Documento: String="",//Número de documento
        var NombreCompleto: String="",//Nombre
        var Huella: HuellaEscanearEnum?=null,//Tipo de huella
        var PathHuellaWsq: String?=null,//Llave de la huella en wsq, se obtiene con LocalDataSource
        var PathFoto: String?=null,//Llave de la foto, se obtiene con LocalDataSource
    )

    suspend fun Verificar(
        model: VerificarFacialHuella//
    ): Result<InformacionPersonal>

Consultar fuentes externas UseCase

Dependencia

    @Named(DI.FUENTE_EXTERNA_USECASE) private val useCase: IFuenteExternaUseCase

Funciones

  • Consultar
    class ConsultarFuenteExterna (
        val FuenteExternaTipo:ExternalSourceEnumeration,//Tipo de fuente externa CCSS | CST
        val AccessTokenFuentesExternas:String//Enviar token obtenido del modelo InformacionPersonal después de pasar la verificación
    )

    suspend fun Consultar(
        request: ConsultarFuenteExterna//Enviar instancia de ConsultarFuenteExterna
    ): Result<FuenteExternaResponseOk>

Otras funciones

Cambiar tipografías

Existen 2 tipografía en los views bold y regular, para reemplazarlas solo se debe crear una carpeta font dentro de la carpeta res y pegarlas con los nombres font_bold.ttf y font_regular.ttf

Cambiar textos

En el archivo de strings del proyecto se pueden cambiar los valores de estos strings

    <string name="doc_nacional">Nacional</string>
    <string name="doc_extranjero">Extranjero Residente</string>
    <string name="numero_identificacion">Número de identificación</string>
    <string name="consultar">Consultar</string>
    <string name="regresar">Regresar</string>
    <string name="wait">Espera un momento por favor...</string>
    <string name="verificacion_facial">Reconocimiento Facial</string>
    <string name="verificacion_huella">Verificación por huella</string>
    <string name="seleccione_tipo_verificacion">Selecciona el tipo de verificación para</string>
    <string name="intentar_de_nuevo">Intentar de nuevo</string>
    <string name="verificar">Verificar</string>
    <string name="necesitas_conceder_permisos">Necesitas conceder permisos</string>
    <string name="consultar_mas_informacion">Consultar más información</string>
    <string name="aceptar">Aceptar</string>
    <string name="informacion_eliminada">La información ha sido eliminada del dispositivo</string>
    <string name="antes_continuar">Antes de continuar:</string>
    <string name="antes_continuar_lea">Antes de continuar:\n\nPor favor, lea atentamente lo siguiente.\nAl continuar, usted confirma que está de acuerdo con las siguientes declaraciones:</string>
    <string name="terminos_condiciones">Ver términos y condiciones</string>
    <string name="acepto_terminos">He leído y estoy de acuerdo con estos términos y condiciones</string>
    <string name="confirmar_ciudadano">Por favor confirmar que el ciudadano:</string>
    <string name="no_documento">No. Documento</string>
    <string name="esta_confirmando">Esta confirmando que esta de acuerdo con las siguientes declaraciones</string>
    <string name="declaraciones">Acepta dar su consentimiento para usar su fotografía (Selfie) y Huellas dactilares para verificar su identidad.\n\nHa leido y aceptado la politica de privacidad\n\nHa leido y acepta las condiciones de uso</string>
    <string name="autorizo">Autorizo</string>
    <string name="ok_">Ok</string>
    <string name="exp">EXP:</string>
    <string name="No">No:</string>
    <string name="limpiar">Limpiar</string>
    <string name="camara_frontal">Cámara frontal</string>
    <string name="camara_trasera">Cámara trasera</string>
    <string name="no_hay_fuentes">No hay fuentes para consultar</string>
    <string name="transaccion_copiada">¡Transacción copiada!</string>
    <string name="transaccion">Transacción</string>
    <string name="identificacion">Identificación</string>
    <string name="confirmar">Confirmar</string>
    <string name="iniciar_nueva_consulta">Iniciar nueva consulta</string>
    <string name="doble_click_zoom">Doble click para zoom</string>
    <string name="cuatro_dedos">4 dedos de la mano</string>
    <string name="los_pulgares">Los Pulgares</string>
    <string name="el_pulgar_de_mano">El Pulgar de la mano</string>
    <string name="derecha">Derecha</string>
    <string name="izquierda">Izquierda</string>
    <string name="ubique">Ubique</string>
    <string name="acerque_su_mano">Acerque su mano</string>
    <string name="aleje_su_mano">Aleje su mano</string>
    <string name="enfoque_mano">Mueva su mano para enfocar</string>
    <string name="mantenga_mano_quita">Mantenga su mano quieta</string>

    <!--sdk facial-->
    <string name="camera_close_message">Acercate a la cámara</string>
    <string name="camera_far_message">Alejate un poco de la cámara</string>
    <string name="camera_up_message">Sube la cámara hacia ti</string>
    <string name="camera_down_message">Baja la cámara hacia ti</string>
    <string name="open_eyes_message">Abre los ojos y mira a la cámara</string>
    <string name="multiple_faces_detected">Multiples rostros detectados</string>
    <string name="detection_failed_message">Detección de rostro fallida</string>
    <string name="no_faces_detected_message">No se reconoce un rostro</string>
    <string name="look_straight_message">Mira recto</string>
    <string name="mask_detected_message">Máscara detectada</string>
    <string name="sunglasses_detected_message">Gafas detectadas</string>
    <string name="eyes_closed_message">Abre los ojos</string>
    <string name="hold_still_message">Quedate quieto</string>
    <string name="spoof_detected_message">Falsificación detetada</string>
    <string name="ok_str">Capturando...</string>
    <string name="please_wait">Por favor espera...</string>
    <string name="network_activity_error">Algo va mal, por favor intenta de nuevo</string>
    <string name="label_occulusion">Oclusión</string>
    <string name="label_eye_close">Ojo cerrado</string>
    <string name="label_livehness">Liveness</string>
    <string name="hold_still_and_center">Centra tu rostro y quédate quieto</string>

    <string name="te_queda">"Te queda: "</string>

    <string name="error_documento_no_encontrado">El documento no esta disponible para consulta</string>
    <string name="error_generico">Ocurrio un error, comuniquese con el administrador</string>
    <string name="error_conexion">Error en la conexión, intente de nuevo ó comuniquese con el administrador</string>
    <string name="error_no_autorizado">Su sesión ha expirado</string>
    <string name="error_apikey_revocado">No estas autorizado para esta consulta [Apikey revocado]</string>
    <string name="error_no_autorizado_forbidden">No estas autorizado para esta consulta [Apikey invalido]</string>
    <string name="error_req_identificacion">Ingrese el número de identificación</string>
    <string name="error_req_tipo_identificacion">Seleccione el tipo de documento</string>
    <string name="error_timeout">La solicitud excedió el tiempo máximo de espera</string>
    <string name="error_unauthorizes_verificacion">El token expiro por favor realice la consulta del documento nuevamente</string>
    <string name="error_intentos_maximos">Se alcanzaron la cantidad de intentos máximos</string>
    <string name="error_no_coincidencias">No se encontraron coincidencias ó no alcanzó el rango de verificación mínimo</string>
    <string name="error_imagenes_cache">Ocurrio un error con la memoria del disposito, se necesita realizar la verificación facial o de huellas nuevamente</string>
    <string name="error_req_terminos">Debes aceptar los terminos y condiciones</string>
    <string name="error_score">No se alcanzo el score mínimo, intentalo de nuevo</string>
    <string name="error_req_firma">Necesitas firmar para autorizar</string>
    <string name="error_seguridad">Error de seguridad</string>
    <string name="error_desconocido">Error desconocido</string>
    <string name="error_copiar_transaccion">Ocurrio un error al copiar la transacción</string>
    <string name="error_cargar_pagina">Ocurrio un error al cargar la pagina</string>
    <string name="error_tomar_captura">Ocurrio un error al tomar la captura</string>
    <string name="error_liveness">No se identificó una persona</string>
    <string name="error_response">Error en el servidor, por favor intenta de nuevo</string>
    <string name="error_fuente_no_seleccionada">Debes seleccionar al menos una fuente</string>
    <string name="error_servicio">Ocurrio un error con el servicio</string>
    <string name="cargar">Cargar</string>

    <string name="error_fuente_externa_not_found">La persona no fue encontrada</string>
    <string name="si">Si</string>
    <string name="no">No</string>

    <string name="error_token_expirado_fuentes_externas">El token ha expirado, necesitas realizar una nueva consulta</string>
    <string name="identidad_verificada">Identidad Verificada</string>
    <string name="id">ID:</string>
    <string name="info_adicional">Información adicional para:</string>

    <string name="CCSS_descripcion">Caja Costarricense de Seguro Social</string>
    <string name="CST_descripcion">Condición Tributaria</string>

    <string name="pulgar_derecho">Dedo Pulgar</string>
    <string name="indice_derecho">Dedo Indice</string>
    <string name="medio_derecho">Dedo Medio</string>
    <string name="anular_derecho">Dedo Anular</string>
    <string name="menique_derecho">Dedo Meñique</string>

    <string name="pulgar_izquierdo">Dedo Pulgar</string>
    <string name="indice_izquierdo">Dedo Indice</string>
    <string name="medio_izquierdo">Dedo Medio</string>
    <string name="anular_izquierdo">Dedo Anular</string>
    <string name="menique_izquierdo">Dedo Meñique</string>

    <!--pantalla de instrucciones de huellas-->
    <string name="instructions">. Asegúrate de estar en un área bien iluminada.\n. Coloca los dedos de tu mano frente a la cámara como se te indique.\n. Sigue las instrucciones.</string>
    <string name="siguiente">Siguiente</string>
    <string name="cancelar_captura">Cancelar</string>
    <string name="label_left_thumb">Pulgar izquierdo</string>
    <string name="label_right_thumb">Pulgar derecho</string>
    <string name="label_left_slap_num_fingers">mano izquierda %1$d dedos</string>
    <string name="label_right_slap_num_fingers">mano derecha %1$d dedos</string>
    <string name="label_left_index_n_middle_fingers">dedo índice y dedo medio izquierdos</string>
    <string name="label_right_index_n_middle_fingers">dedo índice y dedo medio derechos</string>
    <string name="label_left_ring_n_little_fingers">dedo anular y meñique izquierdos</string>
    <string name="label_right_ring_n_little_fingers">dedo anular y meñique derechos</string>
    <string name="label_more_than_one_finger_detected">Se detectó más de 1 dedo</string>
    <string name="label_more_than_n_fingers_detected">Se detectó más de %1$d dedos</string>
    <string name="label_init_sdk_failed">Error al inicializar el SDK. Código: (%1$d)</string>

Pantalla de instrucciones de huellas

Antes del escaneo de huellas, se puede mostrar una pantalla de instrucciones al usuario. El SDK permite saltar esta pantalla, personalizar sus colores, el fondo y los textos.

Saltar la pantalla de instrucciones

Configura el parámetro SkipHuellaExplicacion en SdkConfig:

  • true (por defecto): omite la pantalla y va directamente al escaneo.
  • false: muestra la pantalla de instrucciones antes de escanear.
return SdkConfig(
    ...
    SkipHuellaExplicacion = false, // muestra la pantalla de instrucciones
)

Personalizar colores

Añade los siguientes colores en tu archivo res/values/colors.xml:

<resources>
    <!-- Color de la barra superior -->
    <color name="finger_preview_toolbar_color">#05becb</color>
    <!-- Color del ícono/texto en la barra superior -->
    <color name="finger_preview_toolbar_icon_color">#FFFFFF</color>
    <!-- Color del texto de instrucciones -->
    <color name="finger_preview_instructions_text_color">#5e5e5e</color>
    <!-- Color de fondo del botón "Siguiente" -->
    <color name="finger_preview_next_button_color">#05becb</color>
    <!-- Color del texto del botón "Siguiente" -->
    <color name="finger_preview_next_button_text_color">#FFFFFF</color>
    <!-- Color de fondo del botón "Cancelar" -->
    <color name="finger_preview_cancel_button_color">#FFFFFF</color>
    <!-- Color del texto del botón "Cancelar" -->
    <color name="finger_preview_cancel_button_text_color">#05becb</color>
</resources>

Personalizar el fondo

Crea un drawable background_plain.xml en res/drawable/ para reemplazar el fondo de la pantalla. Por ejemplo, para poner un fondo blanco con una imagen de marca:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <color android:color="@android:color/white" />
    </item>
    <item android:drawable="@drawable/img_background" />
</layer-list>

Novedades / Historial de versiones

Resumen de las mejoras incluidas en cada versión de la aplicación Biometric ID RACSA.

Versión 1.63 — 25/05/2026

Mejora en la velocidad de captura

Se optimizaron los algoritmos de detección y procesamiento de imagen, reduciendo el tiempo de respuesta entre la colocación del dedo y la confirmación de captura exitosa.

Opción para captura de forma vertical

Se activa la posibilidad de captura de los dedos desde el frente del dispositivo, aumentando el área de captura en la pantalla. Se permite la captura únicamente en modo vertical (Portrait).

Optimización por dispositivo

Se mejora la experiencia de captura de huellas en los dispositivos Samsung Galaxy S23 y Tablet Lenovo Tab K11.

Versión 1.60 — 30/04/2026

Mejora en la velocidad de captura

Aumento en la rapidez de la autocaptura de huellas. Se mantienen las guías visuales de posicionamiento del dedo en pantalla durante todo el proceso.

Validación cantidad de dígitos tanto para Cédula como para DIMEX

La consulta de Nacionales exige 9 caracteres (cédula) y la de Extranjeros 12 caracteres (DIMEX) antes de iniciar la transacción, evitando errores por datos incompletos.

Tiempo de inactividad de sesión ampliado

El inicio de sesión permanece activo hasta 8 horas de inactividad antes de solicitar nuevamente las credenciales por seguridad.