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:
- Detecta el rostro del usuario usando la cámara frontal
- Guía al usuario para posicionar correctamente su rostro
- Captura video con detección facial continua
- Realiza análisis de liveness para verificar que es una persona real
- Compara el rostro con una foto de documento de identidad (opcional)
- Genera resultados con scores de confianza y métricas detalladas
Requisitos Previos
Requisitos Técnicos
| Componente | Versión Requerida | Notas |
|---|---|---|
| Android Studio | Iguana o superior | IDE recomendado |
| Gradle | 8.4+ | Sistema de compilación |
| Kotlin | 1.9.22+ | Lenguaje de programación |
| Android API mínima | 22 (Android 5.1) | Dispositivos soportados |
| Android API objetivo | 33+ (Android 13+) | Versión recomendada |
| Java Compatibility | 18 | Nivel de compatibilidad |
Credenciales Necesarias
Para usar el SDK necesitará:
-
Licencia del SDK (obligatorio)
- Formato: String alfanumérico único
- Ejemplo:
"ABC-123-XYZ-789" - Solicitar a: [email protected]
-
Google Play Integrity API (obligatorio)
- Project Number de Google Cloud (número de 12 dígitos)
- Instrucciones de configuración
-
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
- Acceda a Google Cloud Console
- Cree o seleccione un proyecto
- Habilite "Play Integrity API"
- Copie el Project Number (12 dígitos)
- Agregue en
local.properties:
GOOGLE_INTEGRITY_PROJECT_NUMBER=123456789012Inicializar 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:
- Implementar la interfaz
FaceDetectorListener - Configurar los parámetros deseados
- Iniciar la verificación
- 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)
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
SANDBOXpara desarrollo y pruebas - Use
PRODUCTIONpara 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
PRODUCTIONantes de publicar en Google Play
2. setEnableService(enabled: Boolean)
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?
| Modo | Usar cuando... |
|---|---|
true | Desea verificación KYC completa con JAAK |
true | Necesita análisis de liveness automático |
true | Quiere resultados inmediatos con scores |
false | Tiene su propio servidor de procesamiento |
false | Solo necesita capturar la imagen del rostro |
false | Realizará análisis personalizado |
3. setAutoCapture(enabled: Boolean)
setAutoCapture(enabled: Boolean)Configura el modo de captura de imagen/video.
Tipo: Boolean
Valores posibles:
true- Captura automática cuando detecta rostro correctamente posicionadofalse- 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 UXRecomendación: Use true para una experiencia más fluida y automática.
4. setVideoDuration(seconds: Int)
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áximoRecomendació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)
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 MBRecomendació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)
startVerification(activity, listener, selfieReferenceUri)Inicia el proceso de verificación facial.
Parámetros:
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
activity | AppCompatActivity | ✅ Sí | Activity desde donde se inicia el SDK |
listener | FaceDetectorListener | ✅ Sí | Interfaz para recibir callbacks |
selfieReferenceUri | Uri? | ❌ No | URI 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 facialUri- 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 capturadaEjemplo 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?
| Escenario | Usar 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
FaceDetectorListenerSu 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)
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)
liveness (Análisis de prueba de vida)| Campo | Tipo | Descripción | Valores posibles |
|---|---|---|---|
status | String | Estado del análisis de liveness | "success", "failed", "error" |
confidence_score | Float | Score de confianza (0.0 - 1.0) | 0.0 (sin confianza) a 1.0 (máxima confianza) |
is_live | Boolean | ¿Es una persona real? | true, false |
attempts | Integer | Número de intentos realizados | 1 a 3 (máximo) |
indicators.motion_detected | Boolean | ¿Se detectó movimiento? | true, false |
indicators.depth_analysis | Boolean | ¿Pasó análisis de profundidad? | true, false |
indicators.texture_analysis | Boolean | ¿Pasó análisis de textura? | true, false |
indicators.reflection_check | Boolean | ¿Pasó verificación de reflejos? | true, false |
Interpretación del liveness:
confidence_score >= 0.90→ Alta confianza de que es persona realconfidence_score 0.70-0.89→ Confianza media, revisar manualmenteconfidence_score < 0.70→ Baja confianza, probable fraudeis_live = true→ El SDK confirma que es persona realis_live = false→ Posible foto/video/máscara (intento de fraude)
Objeto face (Comparación facial)
face (Comparación facial)| Campo | Tipo | Descripción | Valores posibles |
|---|---|---|---|
status | String | Estado de la comparación | "success", "failed", "not_performed" |
comparison_score | Float | Score de similitud (0.0 - 1.0) | 0.0 (no coincide) a 1.0 (coincidencia perfecta) |
is_match | Boolean | ¿Los rostros coinciden? | true, false |
confidence_level | String | Nivel de confianza textual | "high", "medium", "low" |
face_quality.sharpness | Float | Nitidez de la imagen (0.0 - 1.0) | Más alto = mejor calidad |
face_quality.brightness | Float | Nivel de iluminación (0.0 - 1.0) | 0.7-0.9 es óptimo |
face_quality.face_size | Float | Tamañ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 personacomparison_score 0.70-0.84→ Similitud moderada, revisar contextocomparison_score < 0.70→ Baja similitud, probablemente personas diferentesis_match = true→ El SDK confirma que es la misma personastatus = "not_performed"→ No se proporcionóselfieReferenceUri
Objeto oto (One-Time Operation - Detección de fraude)
oto (One-Time Operation - Detección de fraude)| Campo | Tipo | Descripción | Valores posibles |
|---|---|---|---|
status | String | Estado del análisis de fraude | "success", "warning", "failed" |
fraud_detected | Boolean | ¿Se detectó fraude? | true, false |
fraud_indicators | Array[String] | Lista de indicadores de fraude detectados | ["screen_replay", "mask", "photo"] |
device_integrity | Boolean | ¿El dispositivo es confiable? | true, false |
risk_score | Float | Score 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 fraudefraud_detected = true→ Se detectó intento de engañar al sistemarisk_score < 0.20→ Bajo riesgo, transacción confiablerisk_score 0.20-0.50→ Riesgo medio, aplicar verificaciones adicionalesrisk_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)
summary (Resumen general)| Campo | Tipo | Descripción | Valores posibles |
|---|---|---|---|
overall_status | String | Estado general de verificación | "approved", "rejected", "review" |
verification_passed | Boolean | ¿Pasó todas las verificaciones? | true, false |
risk_level | String | Nivel de riesgo global | "low", "medium", "high" |
recommendation | String | Recomendación del sistema | "approve", "reject", "manual_review" |
timestamp | String | Fecha/hora de la verificación | ISO 8601 format |
Decisiones según el summary:
| overall_status | verification_passed | Acció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)
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)
onErrorFaceDetector(errorType: String, errorMessage: String)Se invoca cuando ocurre un error durante el proceso de verificación.
Parámetros:
errorType - Tipo de error
errorType - Tipo de error| Código de Error | Descripción | ¿Qué significa? |
|---|---|---|
"license_invalid" | Licencia inválida | La licencia proporcionada no es válida o expiró |
"license_expired" | Licencia expirada | La licencia venció, contactar a JAAK |
"network_error" | Error de red | Sin conexión a internet o timeout |
"camera_permission_denied" | Permiso de cámara denegado | Usuario no otorgó permiso de cámara |
"location_permission_denied" | Permiso de ubicación denegado | Usuario no otorgó permiso de ubicación (requerido por ML Kit) |
"camera_not_available" | Cámara no disponible | Dispositivo no tiene cámara o está en uso |
"face_not_detected" | Rostro no detectado | No se pudo detectar rostro en el video |
"liveness_failed" | Liveness fallido | No pasó la prueba de vida (posible fraude) |
"face_comparison_failed" | Comparación facial fallida | Los rostros no coinciden |
"fraud_detected" | Fraude detectado | Se detectó intento de fraude |
"max_attempts_exceeded" | Máximo de intentos | Usuario agotó los 3 intentos permitidos |
"session_expired" | Sesión expirada | La sesión de verificación expiró |
"server_error" | Error del servidor | Error interno del servidor de JAAK |
"invalid_reference_image" | Imagen de referencia inválida | El selfieReferenceUri no es válido |
"file_too_large" | Archivo muy grande | El archivo excede los límites configurados |
"unsupported_format" | Formato no soportado | Formato de imagen no compatible |
"initialization_failed" | Inicialización fallida | El SDK no pudo inicializarse correctamente |
"unknown_error" | Error desconocido | Error no categorizado |
errorMessage - Mensaje descriptivo
errorMessage - Mensaje descriptivoMensaje 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()
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á pasando3. 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 facialPreguntas Frecuentes (FAQ)
¿Cuál es la diferencia entre enableService = true y false?
enableService = true y false?| enableService | ¿Qué hace? | Callback usado |
|---|---|---|
true | Verificación completa con JAAK: liveness + face matching + fraud detection | onSuccessFaceDetector(response) |
false | Solo captura imagen, sin análisis | onImageCaptured(imageUri) |
¿Necesito proporcionar selfieReferenceUri siempre?
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:
- Verifique esta documentación primero
- Revise los logs de error completos
- Envíe email a soporte con:
- Descripción del problema
- Logs de error
- Versión del SDK
- Versión de Android del dispositivo
Updated about 4 hours ago
