JAAK FaceDetector SDK Android


El JAAK Face Detector SDK es una solución completa para verificación de identidad mediante detección facial y análisis de liveness (prueba de vida) en aplicaciones Android. El SDK se integra fácilmente en su aplicación y maneja toda la complejidad de la captura, análisis y verificación facial.

Características principales:

  • ✅ Detección facial en tiempo real con ML Kit de Google
  • ✅ Análisis de liveness (prueba de vida) para prevenir suplantación
  • ✅ Comparación facial (face matching) con documento de identidad
  • ✅ Detección de fraudes (ataques de presentación)
  • ✅ Captura optimizada de foto y video
  • ✅ Interfaz de usuario personalizable
  • ✅ Soporte completo de KYC (Know Your Customer)

¿Qué hace el SDK?

El SDK captura un video corto del rostro del usuario mientras realiza validaciones en tiempo real:

  1. Detecta el rostro del usuario usando la cámara frontal
  2. Guía al usuario para posicionar correctamente su rostro
  3. Captura video con detección facial continua
  4. Realiza análisis de liveness para verificar que es una persona real
  5. Compara el rostro con una foto de documento de identidad (opcional)
  6. Genera resultados con scores de confianza y métricas detalladas

Requisitos Previos

Requisitos Técnicos

ComponenteVersión RequeridaNotas
Android StudioIguana o superiorIDE recomendado
Gradle8.4+Sistema de compilación
Kotlin1.9.22+Lenguaje de programación
Android API mínima22 (Android 5.1)Dispositivos soportados
Android API objetivo33+ (Android 13+)Versión recomendada
Java Compatibility18Nivel de compatibilidad

Credenciales Necesarias

Para usar el SDK necesitará:

  1. Licencia del SDK (obligatorio)

    • Formato: String alfanumérico único
    • Ejemplo: "ABC-123-XYZ-789"
    • Solicitar a: [email protected]
  2. Google Play Integrity API (obligatorio)

  3. Permisos de Android (se configuran automáticamente)

    • CAMERA
    • INTERNET
    • ACCESS_NETWORK_STATE
    • ACCESS_COARSE_LOCATION
    • ACCESS_FINE_LOCATION

Instalación

Paso 1: Configurar Repositorio Maven

Agregue el repositorio de JAAK en su archivo settings.gradle:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
        maven {
            url 'https://us-maven.pkg.dev/jaak-saas/jaak-public-android-repo-release'
        }
    }
}

Paso 2: Agregar Dependencia del SDK

En su archivo build.gradle (módulo app):

dependencies {
    // JAAK Face Detector SDK
    implementation 'ai.jaak.android:facedetector:3.0.0'
    
    // Dependencias requeridas
    implementation 'com.google.dagger:hilt-android:2.46'
    kapt 'com.google.dagger:hilt-compiler:2.46'
}

Paso 3: Configurar Build.gradle del Proyecto

En su build.gradle (nivel proyecto):

buildscript {
    ext.kotlin_version = "1.9.22"
    ext.hilt_version = '2.46'
    dependencies {
        classpath 'com.android.tools.build:gradle:8.3.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

Paso 4: Aplicar Plugins

En su build.gradle (módulo app):

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

Configuración Inicial

Configuración de Google Play Integrity

  1. Acceda a Google Cloud Console
  2. Cree o seleccione un proyecto
  3. Habilite "Play Integrity API"
  4. Copie el Project Number (12 dígitos)
  5. Agregue en local.properties:
GOOGLE_INTEGRITY_PROJECT_NUMBER=123456789012

Inicializar el SDK

El SDK debe inicializarse UNA SOLA VEZ al inicio de su aplicación:

import ai.jaak.facedetector.FaceDetectorSDK
import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
        
        // Inicializar el SDK con su licencia
        FaceDetectorSDK.initialize(
            context = this,
            license = "SU-LICENCIA-AQUI"
        )
    }
}

No olvide registrar su Application en el AndroidManifest.xml:

<application
    android:name=".MyApplication"
    android:allowBackup="true"
    ...>

Cómo Usar el SDK

Arquitectura Básica

El SDK funciona mediante un patrón de callbacks. Su Activity o Fragment debe:

  1. Implementar la interfaz FaceDetectorListener
  2. Configurar los parámetros deseados
  3. Iniciar la verificación
  4. Recibir los resultados en los callbacks

Implementación en Activity

import ai.jaak.facedetector.FaceDetectorSDK
import ai.jaak.facedetector.interfaces.FaceDetectorListener
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity(), FaceDetectorListener {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Configurar el SDK
        setupSDK()
        
        // Iniciar verificación
        startVerification()
    }
    
    private fun setupSDK() {
        FaceDetectorSDK.apply {
            // Configurar ambiente
            setEnvironment(FaceDetectorSDK.Environment.PRODUCTION)
            
            // Configurar modo de servicio
            setEnableService(true) // true = verificación completa
            
            // Configurar captura automática
            setAutoCapture(true) // true = captura automática cuando detecta rostro
            
            // Configurar límites de video (opcional)
            setVideoDuration(10) // 10 segundos máximo
            setVideoSize(5) // 5 MB máximo
        }
    }
    
    private fun startVerification() {
        // Iniciar el proceso de verificación
        FaceDetectorSDK.startVerification(
            activity = this,
            listener = this,
            selfieReferenceUri = null // o Uri de foto de documento
        )
    }
    
    // Callbacks del SDK
    override fun onSuccessFaceDetector(response: String) {
        // ✅ Verificación exitosa
        // response contiene JSON con resultados completos
        Log.d("FaceDetector", "Success: $response")
        processSuccessResponse(response)
    }
    
    override fun onImageCaptured(imageUri: Uri) {
        // 📷 Imagen capturada (solo si enableService = false)
        Log.d("FaceDetector", "Image captured: $imageUri")
    }
    
    override fun onErrorFaceDetector(errorType: String, errorMessage: String) {
        // ❌ Error en la verificación
        Log.e("FaceDetector", "Error: $errorType - $errorMessage")
        handleError(errorType, errorMessage)
    }
    
    override fun onCancelFaceDetector() {
        // 🚫 Usuario canceló el proceso
        Log.d("FaceDetector", "User cancelled verification")
    }
}

Parámetros de Entrada

1. setEnvironment(environment: Environment)

Configura el ambiente de trabajo del SDK.

Tipo: FaceDetectorSDK.Environment

Valores posibles:

  • FaceDetectorSDK.Environment.PRODUCTION - Ambiente de producción (por defecto)
  • FaceDetectorSDK.Environment.SANDBOX - Ambiente de pruebas

Descripción:

  • Cambia las URLs internas del SDK para conectarse al servidor correspondiente
  • Las URLs están configuradas internamente por JAAK
  • Use SANDBOX para desarrollo y pruebas
  • Use PRODUCTION para su aplicación en producción

Ejemplo:

FaceDetectorSDK.setEnvironment(FaceDetectorSDK.Environment.SANDBOX)

¿Cuándo usar?

  • Configure antes de llamar a startVerification()
  • Configure una sola vez al inicio de su aplicación
  • Cambie a PRODUCTION antes de publicar en Google Play

2. setEnableService(enabled: Boolean)

Configura el modo de operación del SDK.

Tipo: Boolean

Valores posibles:

  • true - Modo servicio completo (verificación KYC completa con JAAK)
  • false - Modo solo captura (solo toma la foto y la devuelve)

Valor por defecto: false

Descripción:

Cuando enabled = true (Servicio Completo):

  • El SDK realiza todo el proceso de verificación con los servidores de JAAK
  • Analiza liveness (prueba de vida)
  • Realiza face matching si proporcionó selfieReferenceUri
  • Calcula scores de confianza
  • Detecta fraudes
  • Retorna resultados completos en onSuccessFaceDetector()

Cuando enabled = false (Solo Captura):

  • El SDK solo captura la imagen del rostro
  • NO se comunica con servidores de JAAK
  • NO realiza análisis de liveness
  • NO calcula scores
  • Retorna la imagen en onImageCaptured(imageUri)
  • El cliente debe procesar la imagen en su propio servidor

Ejemplo:

// Modo servicio completo
FaceDetectorSDK.setEnableService(true)

// Modo solo captura
FaceDetectorSDK.setEnableService(false)

¿Cuándo usar cada modo?

ModoUsar cuando...
trueDesea verificación KYC completa con JAAK
trueNecesita análisis de liveness automático
trueQuiere resultados inmediatos con scores
falseTiene su propio servidor de procesamiento
falseSolo necesita capturar la imagen del rostro
falseRealizará análisis personalizado

3. setAutoCapture(enabled: Boolean)

Configura el modo de captura de imagen/video.

Tipo: Boolean

Valores posibles:

  • true - Captura automática cuando detecta rostro correctamente posicionado
  • false - Usuario debe presionar botón de captura manualmente

Valor por defecto: true

Descripción:

  • Con true: El SDK captura automáticamente cuando detecta que el rostro está correctamente posicionado, bien iluminado y mirando a la cámara
  • Con false: Muestra un botón de captura que el usuario debe presionar

Ejemplo:

FaceDetectorSDK.setAutoCapture(true) // Recomendado para mejor UX

Recomendación: Use true para una experiencia más fluida y automática.


4. setVideoDuration(seconds: Int)

Configura la duración máxima del video capturado.

Tipo: Int

Rango válido: 1 - 30 segundos

Valor por defecto: 10 segundos

Descripción:

  • Define cuánto tiempo máximo durará la grabación del video
  • El video puede terminar antes si la detección facial es exitosa
  • Videos más largos permiten mejor análisis pero generan archivos más grandes

Ejemplo:

FaceDetectorSDK.setVideoDuration(15) // 15 segundos máximo

Recomendación:

  • Use 8-12 segundos para balance entre calidad y tamaño
  • Para liveness básico: 5-8 segundos es suficiente
  • Para análisis detallado: 12-15 segundos

5. setVideoSize(megabytes: Int)

Configura el tamaño máximo del archivo de video.

Tipo: Int

Rango válido: 1 - 20 MB

Valor por defecto: 5 MB

Descripción:

  • Límite superior del tamaño del archivo de video
  • El SDK comprime automáticamente el video si excede este límite
  • Archivos más grandes tienen mejor calidad pero toman más tiempo en procesarse

Ejemplo:

FaceDetectorSDK.setVideoSize(8) // Máximo 8 MB

Recomendación:

  • Para redes móviles lentas: 3-5 MB
  • Para WiFi o redes rápidas: 5-10 MB
  • Para máxima calidad: 10-15 MB

6. startVerification(activity, listener, selfieReferenceUri)

Inicia el proceso de verificación facial.

Parámetros:

ParámetroTipoObligatorioDescripción
activityAppCompatActivity✅ SíActivity desde donde se inicia el SDK
listenerFaceDetectorListener✅ SíInterfaz para recibir callbacks
selfieReferenceUriUri?❌ NoURI de foto de documento para face matching

Descripción del parámetro selfieReferenceUri:

Este parámetro opcional permite realizar comparación facial (face matching) entre:

  • El video capturado del usuario (selfie en vivo)
  • Una foto de su documento de identidad (INE, pasaporte, etc.)

Valores posibles:

  • null - Solo realiza análisis de liveness, sin comparación facial
  • Uri - URI válido de una imagen (foto del documento)

Formato aceptado para la imagen:

  • Formatos: JPG, JPEG, PNG
  • Resolución recomendada: 1024x768 o superior
  • Tamaño máximo: 10 MB
  • La imagen debe mostrar claramente el rostro de la persona

¿Cómo obtener el URI?

// Desde archivo local
val imageFile = File(context.filesDir, "documento.jpg")
val uri = FileProvider.getUriForFile(
    context,
    "${context.packageName}.provider",
    imageFile
)

// Desde galería (después de que usuario seleccione)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (resultCode == RESULT_OK && data != null) {
        val imageUri = data.data // Este es el URI que puede usar
    }
}

// Desde captura de cámara
val photoUri: Uri = // URI de foto previamente capturada

Ejemplo completo:

// Sin comparación facial (solo liveness)
FaceDetectorSDK.startVerification(
    activity = this,
    listener = this,
    selfieReferenceUri = null
)

// Con comparación facial
val documentPhotoUri = getDocumentPhotoUri() // Su método para obtener URI
FaceDetectorSDK.startVerification(
    activity = this,
    listener = this,
    selfieReferenceUri = documentPhotoUri
)

¿Cuándo usar selfieReferenceUri?

EscenarioUsar selfieReferenceUri
Solo verificar que es persona real❌ No (null)
Verificar identidad completa (KYC)✅ Sí (URI de documento)
Validar documento de identidad✅ Sí (URI de documento)
Control de acceso simple❌ No (null)
Onboarding de usuarios✅ Sí (URI de documento)

Parámetros de Salida

Interface: FaceDetectorListener

Su Activity o Fragment debe implementar esta interfaz para recibir los resultados del SDK.

interface FaceDetectorListener {
    fun onSuccessFaceDetector(response: String)
    fun onImageCaptured(imageUri: Uri)
    fun onErrorFaceDetector(errorType: String, errorMessage: String)
    fun onCancelFaceDetector()
}

1. onSuccessFaceDetector(response: String)

Se invoca cuando la verificación facial se completa exitosamente.

¿Cuándo se invoca?

  • Solo cuando setEnableService(true) está configurado
  • Después de que el SDK complete todo el análisis
  • Cuando el servidor de JAAK retorna respuesta exitosa

Parámetro response:

  • Tipo: String (formato JSON)
  • Contenido: Objeto JSON completo con todos los resultados de verificación

Estructura del JSON de respuesta:

{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "liveness": {
    "status": "success",
    "confidence_score": 0.97,
    "is_live": true,
    "attempts": 1,
    "indicators": {
      "motion_detected": true,
      "depth_analysis": true,
      "texture_analysis": true,
      "reflection_check": true
    }
  },
  "face": {
    "status": "success",
    "comparison_score": 0.95,
    "is_match": true,
    "confidence_level": "high",
    "face_quality": {
      "sharpness": 0.92,
      "brightness": 0.88,
      "face_size": 0.94
    }
  },
  "oto": {
    "status": "success",
    "fraud_detected": false,
    "fraud_indicators": [],
    "device_integrity": true,
    "risk_score": 0.05
  },
  "summary": {
    "overall_status": "approved",
    "verification_passed": true,
    "risk_level": "low",
    "recommendation": "approve",
    "timestamp": "2025-11-13T10:30:45Z"
  }
}

Descripción de campos:

session_id

  • Tipo: String (UUID)
  • Descripción: Identificador único de esta sesión de verificación
  • Uso: Para rastrear y auditar verificaciones
  • Ejemplo: "550e8400-e29b-41d4-a716-446655440000"

Objeto liveness (Análisis de prueba de vida)

CampoTipoDescripciónValores posibles
statusStringEstado del análisis de liveness"success", "failed", "error"
confidence_scoreFloatScore de confianza (0.0 - 1.0)0.0 (sin confianza) a 1.0 (máxima confianza)
is_liveBoolean¿Es una persona real?true, false
attemptsIntegerNúmero de intentos realizados1 a 3 (máximo)
indicators.motion_detectedBoolean¿Se detectó movimiento?true, false
indicators.depth_analysisBoolean¿Pasó análisis de profundidad?true, false
indicators.texture_analysisBoolean¿Pasó análisis de textura?true, false
indicators.reflection_checkBoolean¿Pasó verificación de reflejos?true, false

Interpretación del liveness:

  • confidence_score >= 0.90 → Alta confianza de que es persona real
  • confidence_score 0.70-0.89 → Confianza media, revisar manualmente
  • confidence_score < 0.70 → Baja confianza, probable fraude
  • is_live = true → El SDK confirma que es persona real
  • is_live = false → Posible foto/video/máscara (intento de fraude)

Objeto face (Comparación facial)

CampoTipoDescripciónValores posibles
statusStringEstado de la comparación"success", "failed", "not_performed"
comparison_scoreFloatScore de similitud (0.0 - 1.0)0.0 (no coincide) a 1.0 (coincidencia perfecta)
is_matchBoolean¿Los rostros coinciden?true, false
confidence_levelStringNivel de confianza textual"high", "medium", "low"
face_quality.sharpnessFloatNitidez de la imagen (0.0 - 1.0)Más alto = mejor calidad
face_quality.brightnessFloatNivel de iluminación (0.0 - 1.0)0.7-0.9 es óptimo
face_quality.face_sizeFloatTamaño relativo del rostro (0.0 - 1.0)Más alto = rostro más grande/cercano

Interpretación del face matching:

  • comparison_score >= 0.85 → Alta probabilidad de que sea la misma persona
  • comparison_score 0.70-0.84 → Similitud moderada, revisar contexto
  • comparison_score < 0.70 → Baja similitud, probablemente personas diferentes
  • is_match = true → El SDK confirma que es la misma persona
  • status = "not_performed" → No se proporcionó selfieReferenceUri

Objeto oto (One-Time Operation - Detección de fraude)

CampoTipoDescripciónValores posibles
statusStringEstado del análisis de fraude"success", "warning", "failed"
fraud_detectedBoolean¿Se detectó fraude?true, false
fraud_indicatorsArray[String]Lista de indicadores de fraude detectados["screen_replay", "mask", "photo"]
device_integrityBoolean¿El dispositivo es confiable?true, false
risk_scoreFloatScore de riesgo (0.0 - 1.0)0.0 (sin riesgo) a 1.0 (alto riesgo)

Interpretación del análisis de fraude:

  • fraud_detected = false → No se detectaron intentos de fraude
  • fraud_detected = true → Se detectó intento de engañar al sistema
  • risk_score < 0.20 → Bajo riesgo, transacción confiable
  • risk_score 0.20-0.50 → Riesgo medio, aplicar verificaciones adicionales
  • risk_score > 0.50 → Alto riesgo, probablemente fraudulento

Indicadores de fraude comunes:

  • "screen_replay" - Video/foto mostrado en pantalla
  • "mask" - Máscara o rostro impreso
  • "photo" - Foto estática en lugar de persona real
  • "deepfake" - Video manipulado con IA
  • "multiple_faces" - Más de un rostro detectado

Objeto summary (Resumen general)

CampoTipoDescripciónValores posibles
overall_statusStringEstado general de verificación"approved", "rejected", "review"
verification_passedBoolean¿Pasó todas las verificaciones?true, false
risk_levelStringNivel de riesgo global"low", "medium", "high"
recommendationStringRecomendación del sistema"approve", "reject", "manual_review"
timestampStringFecha/hora de la verificaciónISO 8601 format

Decisiones según el summary:

overall_statusverification_passedAcción recomendada
"approved"true✅ Aprobar usuario/transacción
"rejected"false❌ Rechazar, posible fraude
"review"false⚠️ Enviar a revisión manual

Ejemplo de procesamiento:

override fun onSuccessFaceDetector(response: String) {
    try {
        val jsonResponse = JSONObject(response)
        
        // Obtener resumen general
        val summary = jsonResponse.getJSONObject("summary")
        val overallStatus = summary.getString("overall_status")
        val verificationPassed = summary.getBoolean("verification_passed")
        val riskLevel = summary.getString("risk_level")
        
        // Obtener scores específicos
        val liveness = jsonResponse.getJSONObject("liveness")
        val livenessScore = liveness.getDouble("confidence_score")
        val isLive = liveness.getBoolean("is_live")
        
        // Obtener face matching (si se realizó)
        val face = jsonResponse.getJSONObject("face")
        val faceStatus = face.getString("status")
        if (faceStatus == "success") {
            val comparisonScore = face.getDouble("comparison_score")
            val isMatch = face.getBoolean("is_match")
            Log.d("FaceDetector", "Face match score: $comparisonScore")
        }
        
        // Obtener análisis de fraude
        val oto = jsonResponse.getJSONObject("oto")
        val fraudDetected = oto.getBoolean("fraud_detected")
        val riskScore = oto.getDouble("risk_score")
        
        // Tomar decisión
        when (overallStatus) {
            "approved" -> {
                if (verificationPassed && !fraudDetected) {
                    // ✅ Usuario verificado exitosamente
                    approveUser()
                    showSuccess("Verificación exitosa")
                }
            }
            "rejected" -> {
                // ❌ Verificación fallida
                rejectUser()
                showError("Verificación fallida: posible fraude")
            }
            "review" -> {
                // ⚠️ Requiere revisión manual
                sendToManualReview()
                showWarning("Verificación requiere revisión manual")
            }
        }
        
        // Guardar session_id para auditoría
        val sessionId = jsonResponse.getString("session_id")
        saveToDatabase(sessionId, response)
        
    } catch (e: JSONException) {
        Log.e("FaceDetector", "Error parsing response", e)
    }
}

2. onImageCaptured(imageUri: Uri)

Se invoca cuando se captura exitosamente una imagen del rostro.

¿Cuándo se invoca?

  • Solo cuando setEnableService(false) está configurado
  • Después de que el SDK captura la imagen del rostro
  • NO se comunica con servidores de JAAK

Parámetro imageUri:

  • Tipo: Uri
  • Contenido: URI local de la imagen capturada
  • Formato: JPG
  • Ubicación: Almacenamiento interno de la app

¿Qué hacer con la imagen?

override fun onImageCaptured(imageUri: Uri) {
    // Opción 1: Convertir a Base64 para enviar a su servidor
    val base64Image = convertImageToBase64(imageUri)
    sendToYourServer(base64Image)
    
    // Opción 2: Subir directamente como multipart
    uploadImageToServer(imageUri)
    
    // Opción 3: Guardar en galería
    saveToGallery(imageUri)
    
    // Opción 4: Mostrar en ImageView
    binding.imageView.setImageURI(imageUri)
}

private fun convertImageToBase64(uri: Uri): String {
    val inputStream = contentResolver.openInputStream(uri)
    val bytes = inputStream?.readBytes()
    inputStream?.close()
    return Base64.encodeToString(bytes, Base64.NO_WRAP)
}

Casos de uso:

  • Tiene su propio servidor de procesamiento de imágenes
  • Desea realizar análisis personalizado
  • Necesita almacenar la imagen para procesamiento posterior
  • Implementa su propio sistema de verificación

3. onErrorFaceDetector(errorType: String, errorMessage: String)

Se invoca cuando ocurre un error durante el proceso de verificación.

Parámetros:

errorType - Tipo de error

Código de ErrorDescripción¿Qué significa?
"license_invalid"Licencia inválidaLa licencia proporcionada no es válida o expiró
"license_expired"Licencia expiradaLa licencia venció, contactar a JAAK
"network_error"Error de redSin conexión a internet o timeout
"camera_permission_denied"Permiso de cámara denegadoUsuario no otorgó permiso de cámara
"location_permission_denied"Permiso de ubicación denegadoUsuario no otorgó permiso de ubicación (requerido por ML Kit)
"camera_not_available"Cámara no disponibleDispositivo no tiene cámara o está en uso
"face_not_detected"Rostro no detectadoNo se pudo detectar rostro en el video
"liveness_failed"Liveness fallidoNo pasó la prueba de vida (posible fraude)
"face_comparison_failed"Comparación facial fallidaLos rostros no coinciden
"fraud_detected"Fraude detectadoSe detectó intento de fraude
"max_attempts_exceeded"Máximo de intentosUsuario agotó los 3 intentos permitidos
"session_expired"Sesión expiradaLa sesión de verificación expiró
"server_error"Error del servidorError interno del servidor de JAAK
"invalid_reference_image"Imagen de referencia inválidaEl selfieReferenceUri no es válido
"file_too_large"Archivo muy grandeEl archivo excede los límites configurados
"unsupported_format"Formato no soportadoFormato de imagen no compatible
"initialization_failed"Inicialización fallidaEl SDK no pudo inicializarse correctamente
"unknown_error"Error desconocidoError no categorizado

errorMessage - Mensaje descriptivo

Mensaje legible para humanos que describe el error con más detalle.

Ejemplo: "No se pudo detectar el rostro en el video. Por favor, asegúrese de estar en un lugar bien iluminado."

Ejemplo de manejo completo de errores:

override fun onErrorFaceDetector(errorType: String, errorMessage: String) {
    Log.e("FaceDetector", "Error: $errorType - $errorMessage")
    
    when (errorType) {
        "license_invalid", "license_expired" -> {
            // Error crítico: contactar a JAAK
            showCriticalError(
                "Error de licencia",
                "Por favor contacte a [email protected]"
            )
        }
        
        "camera_permission_denied" -> {
            // Solicitar permiso nuevamente
            requestCameraPermission()
            showError(
                "Permiso requerido",
                "Necesitamos acceso a la cámara para verificar tu identidad"
            )
        }
        
        "location_permission_denied" -> {
            // Solicitar permiso de ubicación (requerido por ML Kit)
            requestLocationPermission()
            showError(
                "Permiso requerido",
                "Necesitamos acceso a la ubicación para la detección facial"
            )
        }
        
        "network_error" -> {
            // Problema de conectividad
            showRetryableError(
                "Sin conexión",
                "Verifica tu conexión a internet e intenta nuevamente"
            )
        }
        
        "face_not_detected" -> {
            // Usuario no posicionó bien su rostro
            showRetryableError(
                "Rostro no detectado",
                "Asegúrate de estar bien iluminado y mirando a la cámara"
            )
        }
        
        "liveness_failed" -> {
            // Falló la prueba de vida
            showRetryableError(
                "Verificación fallida",
                "No pudimos verificar que eres una persona real. Por favor, intenta nuevamente"
            )
        }
        
        "face_comparison_failed" -> {
            // Los rostros no coinciden
            showError(
                "Rostros no coinciden",
                "El rostro capturado no coincide con el documento proporcionado"
            )
        }
        
        "fraud_detected" -> {
            // Intento de fraude detectado
            showCriticalError(
                "Verificación rechazada",
                "No fue posible completar la verificación"
            )
            // Registrar para auditoría
            logSecurityEvent("fraud_attempt", errorMessage)
        }
        
        "max_attempts_exceeded" -> {
            // Agotó los 3 intentos
            showError(
                "Máximo de intentos",
                "Has superado el número máximo de intentos. Por favor, intenta más tarde"
            )
            // Bloquear temporalmente
            blockUserTemporarily()
        }
        
        "server_error" -> {
            // Error del servidor
            showRetryableError(
                "Error temporal",
                "Ocurrió un problema temporal. Por favor, intenta en unos minutos"
            )
        }
        
        "invalid_reference_image" -> {
            // Imagen de documento inválida
            showError(
                "Imagen no válida",
                "La foto del documento no es válida. Por favor, sube una nueva foto"
            )
            // Permitir subir nueva foto
            requestNewDocumentPhoto()
        }
        
        else -> {
            // Error genérico
            showError(
                "Error inesperado",
                "Ocurrió un error. Por favor, intenta nuevamente"
            )
        }
    }
}

// Métodos auxiliares
private fun showError(title: String, message: String) {
    AlertDialog.Builder(this)
        .setTitle(title)
        .setMessage(message)
        .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
        .show()
}

private fun showRetryableError(title: String, message: String) {
    AlertDialog.Builder(this)
        .setTitle(title)
        .setMessage(message)
        .setPositiveButton("Reintentar") { dialog, _ ->
            dialog.dismiss()
            startVerification()
        }
        .setNegativeButton("Cancelar") { dialog, _ -> dialog.dismiss() }
        .show()
}

private fun showCriticalError(title: String, message: String) {
    AlertDialog.Builder(this)
        .setTitle(title)
        .setMessage(message)
        .setPositiveButton("Entendido") { dialog, _ ->
            dialog.dismiss()
            finish() // Cerrar activity
        }
        .setCancelable(false)
        .show()
}

4. onCancelFaceDetector()

Se invoca cuando el usuario cancela manualmente el proceso de verificación.

¿Cuándo se invoca?

  • Usuario presiona el botón "Atrás" durante la captura
  • Usuario cierra la interfaz del SDK
  • Usuario navega fuera de la pantalla de verificación

Parámetros: Ninguno

¿Qué hacer?

override fun onCancelFaceDetector() {
    Log.d("FaceDetector", "Usuario canceló la verificación")
    
    // Opción 1: Regresar a pantalla anterior
    finish()
    
    // Opción 2: Mostrar mensaje y permitir reintentar
    showCancelMessage()
    
    // Opción 3: Registrar evento de cancelación
    logAnalyticsEvent("verification_cancelled")
}

private fun showCancelMessage() {
    AlertDialog.Builder(this)
        .setTitle("Verificación cancelada")
        .setMessage("¿Deseas intentar nuevamente?")
        .setPositiveButton("Sí") { dialog, _ ->
            dialog.dismiss()
            startVerification()
        }
        .setNegativeButton("No") { dialog, _ ->
            dialog.dismiss()
            finish()
        }
        .show()
}

Ejemplos de Uso

Ejemplo 1: Verificación Completa con JAAK (Modo Servicio)

@AndroidEntryPoint
class VerificationActivity : AppCompatActivity(), FaceDetectorListener {
    
    private lateinit var binding: ActivityVerificationBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityVerificationBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        setupUI()
        configureSDK()
    }
    
    private fun setupUI() {
        binding.btnStartVerification.setOnClickListener {
            startVerificationProcess()
        }
    }
    
    private fun configureSDK() {
        FaceDetectorSDK.apply {
            setEnvironment(FaceDetectorSDK.Environment.PRODUCTION)
            setEnableService(true) // Usar servicio completo de JAAK
            setAutoCapture(true)
            setVideoDuration(10)
            setVideoSize(5)
        }
    }
    
    private fun startVerificationProcess() {
        // Mostrar loading
        binding.progressBar.visibility = View.VISIBLE
        binding.btnStartVerification.isEnabled = false
        
        // Obtener foto del documento (si ya la tienes)
        val documentPhotoUri = getDocumentPhotoUri()
        
        // Iniciar verificación
        FaceDetectorSDK.startVerification(
            activity = this,
            listener = this,
            selfieReferenceUri = documentPhotoUri
        )
    }
    
    override fun onSuccessFaceDetector(response: String) {
        binding.progressBar.visibility = View.GONE
        
        try {
            val json = JSONObject(response)
            val summary = json.getJSONObject("summary")
            
            val overallStatus = summary.getString("overall_status")
            val verificationPassed = summary.getBoolean("verification_passed")
            
            if (overallStatus == "approved" && verificationPassed) {
                // ✅ Verificación exitosa
                showSuccessScreen(response)
                saveVerificationResult(response)
            } else {
                // ❌ Verificación fallida
                showFailureScreen(summary.getString("recommendation"))
            }
        } catch (e: JSONException) {
            Log.e(TAG, "Error parsing response", e)
            showError("Error procesando respuesta")
        }
    }
    
    override fun onImageCaptured(imageUri: Uri) {
        // No se usa en modo servicio completo
    }
    
    override fun onErrorFaceDetector(errorType: String, errorMessage: String) {
        binding.progressBar.visibility = View.GONE
        binding.btnStartVerification.isEnabled = true
        
        handleError(errorType, errorMessage)
    }
    
    override fun onCancelFaceDetector() {
        binding.progressBar.visibility = View.GONE
        binding.btnStartVerification.isEnabled = true
        
        Toast.makeText(this, "Verificación cancelada", Toast.LENGTH_SHORT).show()
    }
    
    private fun showSuccessScreen(response: String) {
        val intent = Intent(this, SuccessActivity::class.java)
        intent.putExtra("verification_data", response)
        startActivity(intent)
        finish()
    }
    
    private fun showFailureScreen(recommendation: String) {
        AlertDialog.Builder(this)
            .setTitle("Verificación fallida")
            .setMessage("No se pudo completar la verificación: $recommendation")
            .setPositiveButton("Reintentar") { dialog, _ ->
                dialog.dismiss()
                startVerificationProcess()
            }
            .setNegativeButton("Cancelar") { dialog, _ ->
                dialog.dismiss()
                finish()
            }
            .show()
    }
}

Ejemplo 2: Solo Captura de Imagen (Sin Servicio JAAK)

@AndroidEntryPoint
class CaptureActivity : AppCompatActivity(), FaceDetectorListener {
    
    private lateinit var binding: ActivityCaptureBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityCaptureBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        configureSDK()
        
        binding.btnCapture.setOnClickListener {
            startCapture()
        }
    }
    
    private fun configureSDK() {
        FaceDetectorSDK.apply {
            setEnvironment(FaceDetectorSDK.Environment.PRODUCTION)
            setEnableService(false) // Solo captura, sin servicio JAAK
            setAutoCapture(true)
        }
    }
    
    private fun startCapture() {
        binding.progressBar.visibility = View.VISIBLE
        
        FaceDetectorSDK.startVerification(
            activity = this,
            listener = this,
            selfieReferenceUri = null
        )
    }
    
    override fun onImageCaptured(imageUri: Uri) {
        binding.progressBar.visibility = View.GONE
        
        // Mostrar imagen capturada
        binding.imagePreview.setImageURI(imageUri)
        binding.imagePreview.visibility = View.VISIBLE
        
        // Procesar imagen en su propio servidor
        lifecycleScope.launch(Dispatchers.IO) {
            try {
                // Convertir a Base64
                val base64Image = convertToBase64(imageUri)
                
                // Enviar a su servidor
                val response = sendToMyServer(base64Image)
                
                withContext(Dispatchers.Main) {
                    handleServerResponse(response)
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    showError("Error procesando imagen: ${e.message}")
                }
            }
        }
    }
    
    override fun onSuccessFaceDetector(response: String) {
        // No se usa cuando enableService = false
    }
    
    override fun onErrorFaceDetector(errorType: String, errorMessage: String) {
        binding.progressBar.visibility = View.GONE
        showError("$errorType: $errorMessage")
    }
    
    override fun onCancelFaceDetector() {
        binding.progressBar.visibility = View.GONE
        finish()
    }
    
    private suspend fun convertToBase64(uri: Uri): String {
        return withContext(Dispatchers.IO) {
            val inputStream = contentResolver.openInputStream(uri)
            val bytes = inputStream?.readBytes()
            inputStream?.close()
            Base64.encodeToString(bytes, Base64.NO_WRAP)
        }
    }
    
    private suspend fun sendToMyServer(base64Image: String): String {
        // Implementar llamada a su API
        val url = "https://your-api.com/verify-face"
        // ... código de API call
        return "response_from_server"
    }
    
    private fun handleServerResponse(response: String) {
        // Procesar respuesta de su servidor
        Toast.makeText(this, "Imagen procesada exitosamente", Toast.LENGTH_SHORT).show()
    }
}

Ejemplo 3: Integración con ViewModel (MVVM)

// ViewModel
@HiltViewModel
class VerificationViewModel @Inject constructor(
    private val repository: VerificationRepository
) : ViewModel() {
    
    private val _verificationState = MutableLiveData<VerificationState>()
    val verificationState: LiveData<VerificationState> = _verificationState
    
    fun processVerificationResult(response: String) {
        viewModelScope.launch {
            try {
                _verificationState.value = VerificationState.Loading
                
                val json = JSONObject(response)
                val summary = json.getJSONObject("summary")
                
                if (summary.getBoolean("verification_passed")) {
                    // Guardar en base de datos o servidor
                    repository.saveVerification(response)
                    _verificationState.value = VerificationState.Success(response)
                } else {
                    _verificationState.value = VerificationState.Failed(
                        summary.getString("recommendation")
                    )
                }
            } catch (e: Exception) {
                _verificationState.value = VerificationState.Error(e.message ?: "Error")
            }
        }
    }
}

// States
sealed class VerificationState {
    object Loading : VerificationState()
    data class Success(val data: String) : VerificationState()
    data class Failed(val reason: String) : VerificationState()
    data class Error(val message: String) : VerificationState()
}

// Activity
@AndroidEntryPoint
class VerificationActivity : AppCompatActivity(), FaceDetectorListener {
    
    private val viewModel: VerificationViewModel by viewModels()
    private lateinit var binding: ActivityVerificationBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityVerificationBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        observeVerificationState()
        configureSDK()
        
        binding.btnStart.setOnClickListener {
            startVerification()
        }
    }
    
    private fun observeVerificationState() {
        viewModel.verificationState.observe(this) { state ->
            when (state) {
                is VerificationState.Loading -> showLoading()
                is VerificationState.Success -> showSuccess(state.data)
                is VerificationState.Failed -> showFailure(state.reason)
                is VerificationState.Error -> showError(state.message)
            }
        }
    }
    
    private fun configureSDK() {
        FaceDetectorSDK.apply {
            setEnvironment(FaceDetectorSDK.Environment.PRODUCTION)
            setEnableService(true)
            setAutoCapture(true)
            setVideoDuration(10)
            setVideoSize(5)
        }
    }
    
    private fun startVerification() {
        FaceDetectorSDK.startVerification(
            activity = this,
            listener = this,
            selfieReferenceUri = null
        )
    }
    
    override fun onSuccessFaceDetector(response: String) {
        viewModel.processVerificationResult(response)
    }
    
    override fun onImageCaptured(imageUri: Uri) {
        // No usado en este ejemplo
    }
    
    override fun onErrorFaceDetector(errorType: String, errorMessage: String) {
        binding.progressBar.visibility = View.GONE
        showError("$errorType: $errorMessage")
    }
    
    override fun onCancelFaceDetector() {
        binding.progressBar.visibility = View.GONE
        finish()
    }
    
    private fun showLoading() {
        binding.progressBar.visibility = View.VISIBLE
    }
    
    private fun showSuccess(data: String) {
        binding.progressBar.visibility = View.GONE
        // Navegar a pantalla de éxito
    }
    
    private fun showFailure(reason: String) {
        binding.progressBar.visibility = View.GONE
        // Mostrar mensaje de fallo
    }
    
    private fun showError(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    }
}

Manejo de Errores

Estrategia de Manejo de Errores

1. Errores Recuperables (Reintentar)

private val retryableErrors = setOf(
    "network_error",
    "face_not_detected",
    "liveness_failed",
    "camera_not_available"
)

override fun onErrorFaceDetector(errorType: String, errorMessage: String) {
    if (errorType in retryableErrors && attemptCount < MAX_ATTEMPTS) {
        showRetryDialog(errorMessage)
    } else {
        showFinalError(errorType, errorMessage)
    }
}

private fun showRetryDialog(message: String) {
    AlertDialog.Builder(this)
        .setTitle("Error temporal")
        .setMessage(message)
        .setPositiveButton("Reintentar") { _, _ ->
            attemptCount++
            startVerification()
        }
        .setNegativeButton("Cancelar") { _, _ -> finish() }
        .show()
}

2. Errores Críticos (No Recuperables)

private val criticalErrors = setOf(
    "license_invalid",
    "license_expired",
    "fraud_detected",
    "max_attempts_exceeded"
)

override fun onErrorFaceDetector(errorType: String, errorMessage: String) {
    if (errorType in criticalErrors) {
        handleCriticalError(errorType, errorMessage)
        // Registrar para auditoría
        logSecurityEvent(errorType, errorMessage)
    }
}

private fun handleCriticalError(type: String, message: String) {
    AlertDialog.Builder(this)
        .setTitle("Error crítico")
        .setMessage(getFriendlyMessage(type))
        .setPositiveButton("Entendido") { _, _ ->
            finish()
        }
        .setCancelable(false)
        .show()
}

private fun getFriendlyMessage(errorType: String): String {
    return when (errorType) {
        "license_invalid" -> "Error de configuración. Por favor contacte a soporte."
        "license_expired" -> "Su licencia ha expirado. Contacte a [email protected]"
        "fraud_detected" -> "No fue posible completar la verificación."
        "max_attempts_exceeded" -> "Ha excedido el número máximo de intentos."
        else -> "Ocurrió un error. Por favor intente más tarde."
    }
}

3. Errores de Permisos

override fun onErrorFaceDetector(errorType: String, errorMessage: String) {
    when (errorType) {
        "camera_permission_denied" -> {
            if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
                showPermissionRationale()
            } else {
                showPermissionSettings()
            }
        }
        "location_permission_denied" -> {
            if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) {
                showLocationPermissionRationale()
            } else {
                showPermissionSettings()
            }
        }
    }
}

private fun showPermissionRationale() {
    AlertDialog.Builder(this)
        .setTitle("Permiso necesario")
        .setMessage("Necesitamos acceso a la cámara para verificar tu identidad.")
        .setPositiveButton("Permitir") { _, _ ->
            requestCameraPermission()
        }
        .setNegativeButton("Cancelar") { _, _ -> finish() }
        .show()
}

private fun showPermissionSettings() {
    AlertDialog.Builder(this)
        .setTitle("Permiso denegado")
        .setMessage("Por favor habilita el permiso de cámara en Configuración.")
        .setPositiveButton("Ir a Configuración") { _, _ ->
            openAppSettings()
        }
        .setNegativeButton("Cancelar") { _, _ -> finish() }
        .show()
}

private fun openAppSettings() {
    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
    val uri = Uri.fromParts("package", packageName, null)
    intent.data = uri
    startActivity(intent)
}

Mejores Prácticas

1. Seguridad

// ✅ BUENO: Cifrar datos sensibles
fun saveVerificationResult(response: String) {
    val encrypted = encryptData(response)
    securePreferences.save("verification", encrypted)
}

// ✅ BUENO: Validar resultados en servidor
fun validateVerification(sessionId: String) {
    apiService.validateSession(sessionId) { isValid ->
        if (isValid) approveUser()
    }
}

// ❌ MALO: Guardar sin cifrar
sharedPreferences.edit()
    .putString("verification", response)
    .apply()

2. Experiencia de Usuario

// ✅ BUENO: Mostrar progreso
override fun onSuccessFaceDetector(response: String) {
    showProgressDialog("Procesando resultados...")
    processResponse(response)
}

// ✅ BUENO: Dar feedback inmediato
FaceDetectorSDK.startVerification(...)
showLoading("Iniciando cámara...")

// ✅ BUENO: Mensajes claros
showError("Rostro no detectado", 
    "Por favor asegúrate de estar bien iluminado")

// ❌ MALO: Sin feedback
FaceDetectorSDK.startVerification(...)
// Usuario no sabe qué está pasando

3. Performance

// ✅ BUENO: Procesar en background
override fun onSuccessFaceDetector(response: String) {
    lifecycleScope.launch(Dispatchers.IO) {
        val processed = processLargeResponse(response)
        withContext(Dispatchers.Main) {
            updateUI(processed)
        }
    }
}

// ✅ BUENO: Configurar límites apropiados
FaceDetectorSDK.apply {
    setVideoDuration(8) // Suficiente para liveness
    setVideoSize(5) // Balance entre calidad y tamaño
}

// ❌ MALO: Procesar en main thread
override fun onSuccessFaceDetector(response: String) {
    val processed = heavyProcessing(response) // Bloquea UI
    updateUI(processed)
}

4. Manejo de Lifecycle

// ✅ BUENO: Limpiar recursos
override fun onDestroy() {
    super.onDestroy()
    // El SDK maneja su propio cleanup automáticamente
    // Solo limpie sus propios recursos
    disposables.clear()
}

// ✅ BUENO: Manejar rotación
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // El SDK maneja rotación automáticamente
    // Solo restaure su propio estado si es necesario
    savedInstanceState?.let {
        attemptCount = it.getInt("attempt_count", 0)
    }
}

5. Logging y Monitoreo

// ✅ BUENO: Log detallado en desarrollo
if (BuildConfig.DEBUG) {
    Log.d(TAG, "Starting verification with environment: ${getCurrentEnvironment()}")
    Log.d(TAG, "Configuration: enableService=$enableService, autoCapture=$autoCapture")
}

// ✅ BUENO: Registrar eventos importantes
override fun onSuccessFaceDetector(response: String) {
    analytics.logEvent("verification_success", Bundle().apply {
        putString("session_id", getSessionId(response))
        putLong("duration", verificationDuration)
    })
}

// ✅ BUENO: Monitorear errores
override fun onErrorFaceDetector(errorType: String, errorMessage: String) {
    crashlytics.recordException(
        Exception("Verification failed: $errorType")
    )
}

6. Testing

// ✅ BUENO: Usar ambiente SANDBOX para pruebas
@Before
fun setup() {
    FaceDetectorSDK.setEnvironment(FaceDetectorSDK.Environment.SANDBOX)
}

// ✅ BUENO: Probar casos de error
@Test
fun testVerificationFailure() {
    // Simular error y verificar manejo
}

// ✅ BUENO: Probar en dispositivos reales
// No confiar solo en emulador para detección facial

Preguntas Frecuentes (FAQ)

¿Cuál es la diferencia entre enableService = true y false?

enableService¿Qué hace?Callback usado
trueVerificación completa con JAAK: liveness + face matching + fraud detectiononSuccessFaceDetector(response)
falseSolo captura imagen, sin análisisonImageCaptured(imageUri)

¿Necesito proporcionar selfieReferenceUri siempre?

No, es opcional:

  • Con selfieReferenceUri: Se realiza face matching (comparación facial)
  • Sin selfieReferenceUri (null): Solo se realiza análisis de liveness

¿Cuántos intentos tiene el usuario?

El usuario tiene máximo 3 intentos por sesión. Después de 3 fallos, se retorna error "max_attempts_exceeded".

¿Puedo personalizar la UI del SDK?

El SDK incluye su propia UI optimizada. La personalización está limitada pero puede:

  • Configurar colores principales en su tema de app
  • Los textos respetan los idiomas del sistema (ES/EN)

¿Qué pasa con los datos capturados?

  • Modo servicio (enableService=true): Los datos se envían a servidores de JAAK para análisis y se eliminan después de procesar
  • Modo captura (enableService=false): Los archivos se guardan temporalmente en su app, usted es responsable de eliminarlos

¿El SDK funciona offline?

No, el SDK requiere internet para:

  • Validar licencia (una vez al inicio)
  • Descargar modelos de ML Kit (una vez, se cachean)
  • Enviar videos para análisis (modo servicio)

¿Qué formato tiene la licencia?

La licencia es un String alfanumérico único, por ejemplo: "ABC-123-XYZ-789" Solicite su licencia a: [email protected]


Contacto y Soporte

Soporte Técnico

  • Email: [email protected]
  • Documentación: [Contactar para acceso]
  • Horario: Lunes a Viernes, 9:00 - 18:00 (UTC-6)

Solicitar Licencia

Para obtener una licencia del SDK, envíe un email a [email protected] con:

  • Nombre de su empresa
  • Propósito de uso del SDK
  • Package name de su aplicación Android

Reportar Problemas

Si encuentra algún problema:

  1. Verifique esta documentación primero
  2. Revise los logs de error completos
  3. Envíe email a soporte con:
    • Descripción del problema
    • Logs de error
    • Versión del SDK
    • Versión de Android del dispositivo