Complete Manual for document detection in Android applications

Estimated time: 50-70 minutes for complete configuration


What will you learn from this manual?

The manual will teach you how to implement and use the Detector document SDK of JAAK for automated capture of identity documents in native Android applications. No advanced technical skills required - Just follow the steps.


Before Start - Checks list

Be sure to have these elements ready:

  • Android Studio Iguana installed
  • Android device with API 26 + (Android 8.0 +)
  • At least 2GB free RAM (for ML processing)
  • Access to Maren Repository JAAK
  • Basic knowledge of Kotlin / Java
  • Test ID

Contents Index

SectionWhat will you do?Time
Step 1Configure Project and Units15 min
Step 2Basic implementation of SDK20 min
Step 3Configure permissions and components10 min
Step 4Management of responses and 4 images20 min
Step 5Test document detection10 min

Step 1: Configure Project and Units

Objective

Configure the development environment and add the necessary SDK.

Technical requirements

RequirementVersionMandatory?
Android StudioIguanaYes
Fat8.4+Yes
minSdk26Yes
targetSdkVersion33Yes
Kotlin1.9.22+Yes
Free RAM2GB +Yes (for ML)

1.1 Building (Project) configuration

buildscript {
    ext.kotlin_version = "1.9.22"
    ext.hilt_version = '2.46'
    repositories {
        google()
        mavenCentral()
        maven {
            url 'https://us-maven.pkg.dev/jaak-platform/jaak-android'
        }
        maven {
            url 'https://jitpack.io'
        }
    }
    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"
        classpath 'com.google.gms:google-services:4.4.2'
    }
}

1.2 Settings.gradle configuration

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

1.3 Build.gradle configuration (Module app)

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

android {
    compileSdk 35

    defaultConfig {
        minSdk 26
        targetSdk 33
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_18
        targetCompatibility JavaVersion.VERSION_18
    }

    buildFeatures {
        viewBinding = true
    }
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
    implementation("androidx.core:core-ktx:1.15.0")
    implementation("androidx.appcompat:appcompat:1.7.0")
    implementation("androidx.constraintlayout:constraintlayout:2.2.0")
    implementation("com.google.dagger:hilt-android:$hilt_version")
    kapt("com.google.dagger:hilt-android-compiler:$hilt_version")

    // Document Detector SDK
    implementation("com.jaak.documentdetectorsdk:jaakdocumentdetector-sdk:3.0.0")

    // TensorFlow Lite Support
    implementation 'org.tensorflow:tensorflow-lite-support:0.4.4'
}

Step 2: Basic implementation of SDK

Objective

Create the basic implementation of the Detector document SDK with automatic detection.

2.1 Create Application Class

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class DocumentDetectorApp : Application()

2.2 Main activity

import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.jaak.documentdetectorsdk.sdk.DocumentDetectorSDK
import com.jaak.documentdetectorsdk.ui.adapter.DocumentDetectorListener
import com.jaak.documentdetectorsdk.utils.Utils
import com.example.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity(), DocumentDetectorListener {

    private lateinit var binding: ActivityMainBinding
    private lateinit var documentDetectorSDK: DocumentDetectorSDK

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setupDocumentDetector()
        setupButtons()
    }

    private fun setupDocumentDetector() {
        documentDetectorSDK = DocumentDetectorSDK(this, this)
    }

    private fun setupButtons() {
        binding.btnStartDetection.setOnClickListener {
            documentDetectorSDK.startDocumentDetector(1)
        }
    }

    override fun onSuccessDocumentDetector(
        typeProcess: Int,
        frontOriginalUri: Uri?,
        frontCropUri: Uri?,
        backOriginalUri: Uri?,
        backCropUri: Uri?
    ) {
        handleSuccess(typeProcess, frontOriginalUri, frontCropUri, backOriginalUri, backCropUri)
    }

    override fun onErrorDocumentDetector(text: String) {
        handleError(text)
    }

    private fun handleSuccess(
        typeProcess: Int,
        frontOriginal: Uri?,
        frontCrop: Uri?,
        backOriginal: Uri?,
        backCrop: Uri?
    ) {
        val message = when (typeProcess) {
            1 -> "document captured from camera"
            else -> "Completion"
        }

        binding.tvStatus.text = message

        // Procure 4 images if available
        frontOriginal?.let { uri ->
            val base64 = Utils.uriToBase64(contentResolver, uri)
            processImage(base64, "front_original")
        }

        frontCrop?.let { uri ->
            val base64 = Utils.uriToBase64(contentResolver, uri)
            processImage(base64, "front_crop")
        }

        backOriginal?.let { uri ->
            val base64 = Utils.uriToBase64(contentResolver, uri)
            processImage(base64, "back_original")
        }

        backCrop?.let { uri ->
            val base64 = Utils.uriToBase64(contentResolver, uri)
            processImage(base64, "back_crop")
        }
    }

    private fun handleError(text: String) {
        binding.tvStatus.text = "Error: $text"
    }

    private fun processImage(base64: String?, type: String) {
        base64?.let {
            android.util.Log.d("DocumentDetector", "$type: ${it.length} caracteres")
        }
    }
}

2.3 Layout XML (activity _ main.xml)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp">

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="JAAK Document Detector"
        android:textSize="24sp"
        android:textStyle="bold"
        android:textAlignment="center"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginTop="20dp" />

    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Press the button to start"
        android:textAlignment="center"
        android:padding="15dp"
        android:background="@drawable/rounded_background"
        app:layout_constraintTop_toBottomOf="@id/tvTitle"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginTop="30dp" />

    <Button
        android:id="@+id/btnStartDetection"
        android:layout_width="280dp"
        android:layout_height="60dp"
        android:text="Start Detection"
        android:textSize="16sp"
        app:layout_constraintTop_toBottomOf="@id/tvStatus"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginTop="40dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

Step 3: Configure Permissions and Components

Objective

Configure necessary permissions and system components correctly.

3.1 AndroidManifes.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Permisos necesarios -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        tools:replace="android:maxSdkVersion"
        android:maxSdkVersion="28" />

    <application
        android:name=".DocumentDetectorApp"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
        tools:replace="android:theme,android:name">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

3.2 Performance of Permises at Runtime

import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build

class MainActivity : AppCompatActivity(), DocumentDetectorListener {

    companion object {
        private const val PERMISSIONS_REQUEST_CODE = 100
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        checkAndRequestPermissions()
        setupDocumentDetector()
        setupButtons()
    }

    private fun checkAndRequestPermissions() {
        val permissionsNeeded = mutableListOf<String>()

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            != PackageManager.PERMISSION_GRANTED) {
            permissionsNeeded.add(Manifest.permission.CAMERA)
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES)
                != PackageManager.PERMISSION_GRANTED) {
                permissionsNeeded.add(Manifest.permission.READ_MEDIA_IMAGES)
            }
        } else {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
                permissionsNeeded.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            }
        }

        if (permissionsNeeded.isNotEmpty()) {
            ActivityCompat.requestPermissions(
                this,
                permissionsNeeded.toTypedArray(),
                PERMISSIONS_REQUEST_CODE
            )
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {
            PERMISSIONS_REQUEST_CODE -> {
                if (grantResults.isNotEmpty() &&
                    grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
                    binding.tvStatus.text = "Permisos concedidos"
                } else {
                    binding.tvStatus.text = "Permisos denegados"
                }
            }
        }
    }
}

Track 4: Reg Management and 4 Images System

Objective

Implement complete management of the new 4-image system SDK v3.0.0.

4.1 Full 4 Images processing

override fun onSuccessDocumentDetector(
    typeProcess: Int,
    frontOriginalUri: Uri?,
    frontCropUri: Uri?,
    backOriginalUri: Uri?,
    backCropUri: Uri?
) {
    try {
        val documentImages = DocumentImages()

        // Procure original front image
        frontOriginalUri?.let { uri ->
            documentImages.frontOriginal = Utils.uriToBase64(contentResolver, uri)
            android.util.Log.d("DocumentDetector", "Front Original: ${documentImages.frontOriginal?.length} chars")
        }

        // Procure cut front image
        frontCropUri?.let { uri ->
            documentImages.frontCrop = Utils.uriToBase64(contentResolver, uri)
            android.util.Log.d("DocumentDetector", "Front Crop: ${documentImages.frontCrop?.length} chars")
        }

        // Reprocessing original back image (if available)
        backOriginalUri?.let { uri ->
            documentImages.backOriginal = Utils.uriToBase64(contentResolver, uri)
            android.util.Log.d("DocumentDetector", "Back Original: ${documentImages.backOriginal?.length} chars")
        }

        // Treat cut back image (if available)
        backCropUri?.let { uri ->
            documentImages.backCrop = Utils.uriToBase64(contentResolver, uri)
            android.util.Log.d("DocumentDetector", "Back Crop: ${documentImages.backCrop?.length} chars")
        }

        // Determine document type
        val documentType = if (backOriginalUri != null || backCropUri != null) {
            "double_sided"
        } else {
            "single_sided"
        }

        // To process complete document
        processCompleteDocument(documentImages, documentType, typeProcess)

        binding.tvStatus.text = "document successfully processed"

    } catch (e: Exception) {
        android.util.Log.e("DocumentDetector", "Error processing images: ${.message}")
        binding.tvStatus.text = "Error processing images"
    }
}

data class DocumentImages(
    var frontOriginal: String? = null,
    var frontCrop: String? = null,
    var backOriginal: String? = null,
    var backCrop: String? = null
)

private fun processCompleteDocument(
    images: DocumentImages,
    documentType: String,
    typeProcess: Int
) {
    android.util.Log.d("DocumentDetector", "_")
    android.util.Log.d("DocumentDetector", "Tipo: $documentType")
    android.util.Log.d("DocumentDetector", "Process: $typeProcess")

    // Validate that we have at least the front image
    if (images.frontOriginal.isNullOrEmpty() && images.frontCrop.isNullOrEmpty()) {
        throw IllegalStateException("No valid front images found")
    }

    // Create payload for backend
    val payload = createDocumentPayload(images, documentType)

    // Send to backend or process locally
    sendToBackend(payload)

    android.util.Log.d("DocumentDetector", "Reprocessing document")
}

private fun createDocumentPayload(images: DocumentImages, documentType: String): Map<String, Any> {
    return mapOf(
        "documentType" to documentType,
        "timestamp" to System.currentTimeMillis(),
        "version" to "3.0.0",
        "images" to mapOf(
            "frontOriginal" to (images.frontOriginal ?: ""),
            "frontCrop" to (images.frontCrop ?: ""),
            "backOriginal" to (images.backOriginal ?: ""),
            "backCrop" to (images.backCrop ?: "")
        ),
        "metadata" to mapOf(
            "hasBack" to !images.backOriginal.isNullOrEmpty(),
            "hasCrop" to !images.frontCrop.isNullOrEmpty(),
            "platform" to "android"
        )
    )
}

private fun sendToBackend(payload: Map<String, Any>) {
    android.util.Log.d("DocumentDetector", "Enviando a backend: ${payload.keys}")
    // Implement call to your API
}

4.2 Advanced Errors Management

override fun onErrorDocumentDetector(text: String) {
    android.util.Log.e("DocumentDetector", "Error: $text")

    when {
        text.contains("TensorFlow", ignoreCase = true) -> {
            handleMLError(text)
        }
        text.contains("camera", ignoreCase = true) -> {
            handleCameraError(text)
        }
        text.contains("permission", ignoreCase = true) -> {
            handlePermissionError(text)
        }
        text.contains("memory", ignoreCase = true) -> {
            handleMemoryError(text)
        }
        else -> {
            handleGenericError(text)
        }
    }
}

private fun handleMLError(error: String) {
    binding.tvStatus.text = "ML processing error"
    showErrorDialog(
        "Processing Error",
        "The artificial intelligence model could not be loaded.\n\nVerify that the device has enough free memory."
    )
}

private fun handleCameraError(error: String) {
    binding.tvStatus.text = "Camera error"
    showErrorDialog(
        "Camera error",
        "The camera could not be accessed.\n\nVerify the app's permissions."
    )
}

private fun handlePermissionError(error: String) {
    binding.tvStatus.text = "Permisos denegados"
    showErrorDialog(
        "Permisos Requeridos",
        "The application requires camera and storage permits to work properly."
    )
}

private fun handleMemoryError(error: String) {
    binding.tvStatus.text = "Memoria insuficiente"
    showErrorDialog(
        "Memoria Insuficiente",
        "The device does not have enough free memory.\n\nClose other applications and try again."
    )
}

private fun handleGenericError(error: String) {
    binding.tvStatus.text = "Error: $error"
}

private fun showErrorDialog(title: String, message: String) {
    androidx.appcompat.app.AlertDialog.Builder(this)
        .setTitle(title)
        .setMessage(message)
        .setPositiveButton("Entendido") { dialog, _ ->
            dialog.dismiss()
        }
        .show()
}

Step 5: Test document detection

Objective

Check that all functionalities of SDK They work properly.

checklist

ElementStateDescription
Automatic detection☐AI detects document automatically
System 4 images☐Original and face / back crop
Basal conversion64☐All images become
Overview 2x2☐Sample 4 images in grid
Double face documents☐Detects and processes both sides

Test process

Step A: Test Simple Document

  1. Press "Start Detection"
  2. Position document within the framework
  3. To expect automatic detection (Green frame)
  4. Check capture automatic
  5. Review preview on-screen images

Step B: Test Double face document

  1. Capture front document
  2. Following instructions To turn
  3. Capture Reverse automatically
  4. Check 4 images at preview 2x2
  5. Confirm processing success

Step C: Check ML quality

  1. Test with bad light - you have to set up
  2. Test with tipped document - you have to detect and cut
  3. Test with different types Documents
  4. Check logs from TensFlow Lite

Debugging and Troublehooting

Check Device Resources

private fun checkDeviceCapability(): Boolean {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    val memoryInfo = ActivityManager.MemoryInfo()
    activityManager.getMemoryInfo(memoryInfo)

    val availableMemoryMB = memoryInfo.availMem / (1024 * 1024)
    android.util.Log.d("DocumentDetector", "Memoria disponible: ${availableMemoryMB}MB")

    return availableMemoryMB > 500
}

private fun enableDebugLogs() {
    android.util.Log.d("DocumentDetector", "=== DEBUG DOCUMENT DETECTOR ===")
    android.util.Log.d("DocumentDetector", "SDK version: 3.0.0")
    android.util.Log.d("DocumentDetector", "Memoria disponible: ${checkDeviceCapability()}")
    android.util.Log.d("DocumentDetector", "TensorFlow Lite disponible: ${checkTensorFlowLite()}")
}

private fun checkTensorFlowLite(): Boolean {
    return try {
        Class.forName("org.tensorflow.lite.Interpreter")
        true
    } catch (e: ClassNotFoundException) {
        false
    }
}

Common problems

ProblemResolution
"TensFlow model error"Check free memory and reset app
"Non-available camera"Check permits and that are not in use
"Launcher isn't registered"Call Setup at onCreate (), not immediately
"Processing failed"Check RAM available and close other apps

Full reference

Methods SDK

MethodDescriptionExample
DocumentDetectorSDK (context, list)Building SDKDocumentDetectorSDK(this, this)
startDocumentDetector (Int)Begins detectiondocumentDetectorSDK.startDocumentDetector(1)

Startable Parameters DocumentDetector

ModeDescriptionWhen to use
1Direct camera detectionImmediate document collection

4 Images System

ImageDescriptionWhen it's generated
frontFull face imageAlways
frontCropFront cut by AIWhen detecting document
backOriginalFull back imageOnly if document requires back
backCropReverse cut by AIWhen you get back

Fits

MethodDescriptionExample
Utils.uriToBase64 ()Convert URI to Base64Utils.uriToBase64(contentResolver, uri)

Problem solving

Problem: "memory error"

// Check before initializing
private fun initializeSDKSafely() {
    if (checkDeviceCapability()) {
        setupDocumentDetector()
    } else {
        showMemoryWarning()
    }
}

private fun showMemoryWarning() {
    binding.tvStatus.text = "Insufficient memory device"
}

Problem: "Documents aren't detectable"

// Check compatibility TensFlow Lite
private fun checkMLCompatibility() {
    try {
        val interpreter = org.tensorflow.lite.Interpreter(ByteBuffer.allocate(0))
        android.util.Log.d("DocumentDetector", "Compatible")
    } catch (e: Exception) {
        android.util.Log.e("DocumentDetector", "TF Lite no compatible: ${e.message}")
    }
}

Problem: "Preview doesn't display 4 images"

The SDK v3.0.0 automatically includes a preview with Layout 2x2 showing:

  • Left upper: Original Front
  • Right upper: Front Crop
  • Lower left: Original Back
  • lower right: Back Crop

No additional configuration required.


Do you need some help?

Support information

  • Problem description: What document are you trying to capture vs. what's going on?
  • Android Studio Logs: Mistakes Screenshots
  • Unit information: RAM, CPU, Android version
  • Building Setup Unit versions
  • Type of document: INE, passport, license, etc.

Support Contact

Email: support Always include: Log ML, configuration, version SDK 3.0.0


You have successfully implemented the Detector document SDK v3.0.0 JAAK Your app can now detect and process document automatically using artificial intelligence, generating 4 optimization images for processes KYC.