Getting started: Android (Validations SDK)

Introduction

The Truora Validations SDK allows you to integrate identity verification features directly into your native mobile applications. The SDK handles the complexity of capturing the user’s identity documents, facial recognition, and liveness detection to verify their identity against backend records.

Requirements

Platform Requirements

  • Android: 5.1 (API Level 21) or higher
  • The SDK must be launched from an AppCompatActivity

Permissions Required

  • Camera access (for document and face capture)
  • Internet access (for API communication)

Prerequisites & Authentication

The Key Provider Interface

To prevent hardcoding sensitive logic, you must implement the TruoraAPIKeyGetter interface in your code. This allows you to retrieve the API key from a secure location (like a compiled secret, a secure storage enclave, or an obfuscated string). This interface is how the sdk will get access to the api key for the validations, the api key must be of type sdk (temporary), any other key type will result in an error. Below we show three examples of how to implement the interface, but it can work however you want

Option 1: Encrypted Storage

Java


import com.truora.interfaces.TruoraAPIKeyGetter;
// You would need to import necessary Android classes like Context and relevant security classes.

public class EncryptedStorageApiKeyProvider implements TruoraAPIKeyGetter {

    // NOTE: In a real app, 'context' would be used to access EncryptedSharedPreferences.
    // The key must be retrieved and cached synchronously or fetched asynchronously before this method is called.
    private final String cachedApiKey; 

    public EncryptedStorageApiKeyProvider(String preFetchedKey) {
        // Assuming the key is pre-fetched asynchronously and passed to the constructor.
        this.cachedApiKey = preFetchedKey;
    }

    @Override
    public String getApiKeyFromSecureStorage() {
        // Check if the key was successfully retrieved and cached
        if (cachedApiKey == null || cachedApiKey.isEmpty()) {
            throw new RuntimeException("API key not found in secure storage or retrieval failed.");
        }
        return this.cachedApiKey;
    }
}

Kotlin

import com.truora.interfaces.TruoraAPIKeyGetter
import android.content.Context
// You would need androidx.security.crypto for EncryptedSharedPreferences

class EncryptedStorageApiKeyProvider(private val context: Context, private val preFetchedKey: String?) : TruoraAPIKeyGetter {
    
    // NOTE: For synchronous retrieval, this class assumes the API key was already 
    // fetched asynchronously and passed in the constructor, or it uses a blocking method.

    override fun getApiKeyFromSecureStorage(): String {
        // Use Kotlin's safety features to check for null/empty
        return preFetchedKey.takeIf { !it.isNullOrEmpty() } 
            ?: throw RuntimeException("API key not found in secure storage or retrieval failed.")
    }

    // Example conceptual function for synchronous secure retrieval if necessary
    // private fun retrieveKeyFromStorageSync(context: Context, key: String): String? { /* ... */ }
}

Option 2: BuildConfig value

In this option we get the key from a generated class defined in your module’s build.gradle file.

Java


import com.truora.interfaces.TruoraAPIKeyGetter;

/**
 * Retrieves the API key directly from the BuildConfig file.
 */
public class BuildConfigApiKeyProvider implements TruoraAPIKeyGetter {

    @Override
    public String getApiKeyFromSecureStorage() {
        // Option 2: From environment config (less secure, for development)
        String apiKey = BuildConfig.TRUORA_API_KEY;

        if (apiKey == null || apiKey.isEmpty()) {
            // Throw an error or return an empty string if the key is missing/null
            return ""; 
        }
        return apiKey;
    }
}

Kotlin

import com.truora.interfaces.TruoraAPIKeyGetter
// Assuming BuildConfig.TRUORA_API_KEY is available

/**
 * Retrieves the API key directly from the BuildConfig file.
 */
class BuildConfigApiKeyProvider : TruoraAPIKeyGetter {
    
    override fun getApiKeyFromSecureStorage(): String {
        // Option 2: From environment config (less secure, for development)
        // Uses Kotlin's 'takeIf' and Elvis operator (?:) for a concise null/empty check
        return BuildConfig.TRUORA_API_KEY.takeIf { !it.isNullOrEmpty() } ?: ""
    }
}

Option 3: Retrieve from a backend

Recommended for production builds these are examples for backend retrieval:

Java

import com.truora.interfaces.TruoraAPIKeyGetter;

public class BackendServiceApiKeyProvider implements TruoraAPIKeyGetter {

    // Key must be declared volatile to ensure visibility across threads (network fetch vs. SDK call).
    private volatile String cachedApiKey = null;

    /**
     * Conceptual method: Key should be fetched in the Application class or an initialization phase
     * before the SDK is instantiated.
     */
    public void fetchApiKeyAsynchronously(String userToken) {
        // Implementation detail: Use a library like Retrofit or OkHttp to call your API.
        // ... (Network call logic on a background thread) ...
        // On successful response:
        // this.cachedApiKey = fetchedKey;
    }

    @Override
    public String getApiKeyFromSecureStorage() {
        // Option 3: Return the cached key.
        if (cachedApiKey == null || cachedApiKey.isEmpty()) {
            // Indicates failure to pre-fetch the key, halting SDK initialization.
            throw new RuntimeException("API key has not been retrieved from backend service or fetch failed.");
        }
        return cachedApiKey;
    }
}

Kotlin

import com.truora.interfaces.TruoraAPIKeyGetter

class BackendServiceApiKeyProvider : TruoraAPIKeyGetter {

    // Key must be declared volatile to ensure visibility across threads/Coroutines.
    @Volatile private var cachedApiKey: String? = null
    
    /**
     * Conceptual method: This function would use Coroutines to safely fetch the key
     * in a background thread and update 'cachedApiKey'.
     */
    fun fetchApiKeyAsynchronously(userToken: String) {
        // Implementation detail: Use Retrofit/Ktor within a Coroutine scope.
        // After successful fetch: cachedApiKey = fetchedKey
    }

    override fun getApiKeyFromSecureStorage(): String {
        // Option 3: Return the key that was retrieved and cached asynchronously beforehand
        // Uses the Elvis operator to throw a RuntimeException if the key is not ready
        return cachedApiKey ?: throw RuntimeException("API key has not been retrieved from backend service.")
    }
}

Installation

The Truora SDK is modular and published on Maven. You don’t need to include every module in your project, only the validation modules that you need.

For example, if you are just interested in the face validation module add the following to your app-level build.gradle.kts (or build.gradle):

dependencies {    
    // Face Validation Module
    implementation("com.truora:validations.face:1.0.0")
    // Document Validation Module
    implementation("com.truora:validations.document:1.0.0") 

    // Required AndroidX dependencies
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
}

Android Permissions

Add the camera permission to your AndroidManifest.xml. The SDK requires camera access to capture the selfie.

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

Configuration Options

Before implementing validations, let’s understand the available configuration options.

Using custom validation config and UI config is optional when building the validation:

  • If no uiConfig is provided then the validation view will show the standard Truora branding, logo and colors
  • If no validationConfig is provided then default values will be applied like:
    • No waiting for validation results before finishing the validation process for the user
    • autocapture of face/doc when it’s detected will be on

More indepth information on the validation configuration options, such as similarity threshold, can be found in the official validations api documentation

User ID

The userId parameter is crucial for initializing the Truora SDK. It provides a unique identifier for the user undergoing the validation process.

  • Purpose: The userId allows Truora to associate validation attempts and reference data (like previously captured reference faces) with a specific user in your system. It is mandatory for linking the validation results to your user base.

  • Best Practice: This ID should be an immutable, non-sensitive identifier from your database (e.g., a UUID or internal user ID) that uniquely identifies the person. Do not use sensitive PII like an email address or national ID number as the userId.

Language

The language parameter allows you to define the localization of the text and instructions displayed within the validation view.

  • Customization: By setting the language, you ensure a user-friendly experience by presenting instructions and feedback in the user’s preferred language.

  • Available Options: The SDK supports several languages. You should provide the language code as a two-letter string (e.g., “es” for Spanish, “en” for English, “pt” for Portuguese). If no language is provided, the SDK will typically default to English or attempt to use the device’s system language, though providing an explicit language is recommended.

Example in Java Android Initialization:

// Initialize SDK with an AppCompatActivity context, most often is the same caller "this"
// Do this before the onResume of the lifecycle of the component
ValidationHandler validationHandler = ValidationHandler.create(this);

After initializing once the handler its time to configure the SDK

// Build SDK by passing an object with the Key Provider interface  
TruoraSDK.Validations.Builder(this,"user_unique_id") //Unique user id  
.withLanguage(TruoraLanguage.SPANISH) //Sets language to Spanish  
	.withUIConfig({ /* ... */ })
    .withValidation({ /* ... */ })
	.build(validationHandler);

Example in Kotlin Android Initialization:

// Initialize SDK with an AppCompatActivity context, most often is the same caller "this"
// Do this before the onResume of the lifecycle of the component
val validationHandler = ValidationHandler.create(this)

After initializing once the handler its time to configure the SDK

// Build SDK by passing an object with the Key Provider interface  
TruoraSDK.Validations.Builder(this,"user_unique_id") // Unique user id
.withLanguage(TruoraLanguage.SPANISH) //Sets language to Spanish  
	.withUIConfig{ /* ... */ }
    .withValidation{ /* ... */ }
	.build(validationHandler)

Face configuration

To create a face validation you just need to add it when building the validation object withValidation, you can just return the validation config inside the lambda function if you do not want to use any custom configuration

To specify that the validation type you want to use is Face, then explicitly type the lambda function as accepting a Face config validation type

Java Example

This is only the face configuration example in Java, the entire integration example is in the following section.

// 1. Configure using Lambdas
TruoraSDK.Validations.Builder builder = TruoraSDK.Validations.Builder(this, "user_unique_id");
.withValidation ((FaceValidationConfigFunction) config -> {
    config
        .useAutocapture(true)
        .setSimilarityThreshold(0.95f)
        .setTimeout(120) // 2 minutes timeout
        .waitForResults(false)
        .build()
});

// 2. Bind
builder.build(validationHandler);

Kotlin Example

This is only the face configuration example in Kotlin, the entire integration example is in the following section.

// 1. Configure using Lambdas
val builder = TruoraSDK.Validations.Builder(this, "user_unique_id")
.withValidation { config ->
    config
        .useAutocapture(true)
        .setSimilarityThreshold(0.95f)
        .setTimeout(120) // 2 minutes timeout
        .waitForResults(false)
        .build()
}

// 2. Bind
builder.build(validationHandler)

Configuration Parameters:

Parameter Type Default Description
useAutocapture bool true Automatically capture when face is properly detected
similarityThreshold double 0.8 Minimum similarity score required (0.0 - 1.0)
timeout int 60 Maximum time in seconds for the validation
waitForResults bool false Wait for API response before closing the view
referenceFace ReferenceFace null Optional reference image for comparison

Document configuration

To create a face validation you just need to added it when building the validation object withValidation, you can just return the validation config inside the lambda function if you do not want to use any custom configuration

To specify that the validation type you want to use is Document, then explicitly type the lambda function as accepting a Document config validation type

Java Example

This is only the document configuration example in Java, the entire integration example is in the following section.

// 1. Configure using Lambdas
TruoraSDK.Validations.Builder builder = TruoraSDK.Validations.Builder(this, "user_unique_id");
.withValidation ((DocumentValidationConfig) config -> {
    config
        .useAutocapture(true)
        .setTimeout(120) // 2 minutes timeout
        .waitForResults(false)
        .setCountry(Country.CO) // Country code (e.g., co, mx, pe)
        .setDocumentType(DocumentType.NATIONAL_ID) // Type of document
        .build()
});

// 2. Bind
builder.build(validationHandler);

Kotlin Example

This is only the document configuration example in Kotlin, the entire integration example is in the following section.

// 1. Configure using Lambdas
val builder = TruoraSDK.Validations.Builder(this, "user_unique_id")
.withValidation { config ->
    config
        .useAutocapture(true)
        .setTimeout(120) // 2 minutes timeout
        .waitForResults(false)
        .setCountry(Country.CO) // Country code (e.g., co, mx, pe)
        .setDocumentType(DocumentType.NATIONAL_ID) // Type of document
        .build()
}

// 2. Bind
builder.build(validationHandler)

Configuration Parameters:

Parameter Type Default Description
country Country null Country code for document validation (if not set, user selects)
documentType DocumentType null Type of document (if not set, user selects)
useAutocapture bool true Automatically capture when document is detected
timeout int 90 Maximum time in seconds for the validation
waitForResults bool false Wait for API response before closing the view

Available Countries and Document Types:

Country Code Supported Document Types
Argentina Country.ar nationalId
Brazil Country.br cnh, generalRegistration
Chile Country.cl nationalId, foreignId, driverLicense, passport
Colombia Country.co nationalId, foreignId, rut, ppt, passport, identityCard, temporaryNationalId
Costa Rica Country.cr nationalId, foreignId
El Salvador Country.sv nationalId, foreignId, passport
Mexico Country.mx nationalId, foreignId, passport
Peru Country.pe nationalId, foreignId
Venezuela Country.ve nationalId
All Country.all passport

Understanding SDK Results

Before implementing validations, it’s important to understand what results you’ll receive and how to handle them.

The SDK returns a TruoraValidationResult which can be:

  • completed: Validation process finished (contains ValidationResult)
  • canceled: Validation process canceled by the user (could contain ValidationResult)
  • error: SDK error occurred (contains TruoraError)

Validation Result Object

The plugin returns ValidationResult objects with the following structure:

data class ValidationResult(
  val id: String?, // Unique ID for this validation
  val status: String?, // Status of the validation
  val type: String?, // Type of the validation
  val detail: ValidationDetailResponse?
)

Validation Statuses

Status Description When it occurs
ValidationStatus.success Validation passed User successfully passed the validation
ValidationStatus.failure Validation failed User did not pass the validation criteria
ValidationStatus.pending Awaiting processing Validation is being process

TruoraValidationError Types

When the SDK returns failed, the error can be one of three types:

Exception Type Description
TruoraValidationError.SDK Internal SDK error (configuration, permissions, user actions)
TruoraValidationError.ValidationApi Error from the Truora Validation API
TruoraValidationError.Network Network connectivity error

Common SDK Error Types (SDKErrorType):

Error Type Code Description
cameraPermissionError 20011 Camera permission was denied
invalidApiKey 20017 API key is invalid or expired
invalidConfiguration 20024 SDK configuration is invalid
networkError 20025 Network connection failed
uploadFailed 20026 Failed to upload captured media

Handling Example Java

This is only the results handling example in Java, the entire integration example is in the following section.

private void handleResult(TruoraValidationResult result) {
    if (result instanceof TruoraValidationCompleted<?>) {
        TruoraValidationCompleted<?> completed = (TruoraValidationCompleted<?>) result;
        ValidationResult validation = (ValidationResult) completed.getValue();
        
        Log.i(TAG, "Validation completed successfully: " + validation.getId()
                    + "\n- Status: " + validation.getStatus()
                    + "\n- Type: " + validation.getType()
        );

        if (validation.getStatus() == ValidationStatus.SUCCESS) {
            // User passed the validation
            navigateToSuccessScreen();
        } else if (validation.getStatus() == ValidationStatus.FAILURE) {
            // User did not pass the validation criteria
            showRetryDialog();
        }
    } else if (result instanceof TruoraValidationError<?>) {
        TruoraValidationError<?> error = (TruoraValidationError<?>) result;
        handleSDKError(error);
    } else if (result instanceof TruoraValidationCanceled<?>) {
        // User cancelled - just go back
        goToPrevScreen();
    }
}

private void handleSDKError(TruoraValidationError<?> truoraError) {
    TruoraException exception = truoraError.getException();
    if (exception instanceof TruoraException.SDK) {
         // SDK-level errors (includes network errors)
         switch (exception.getCode()) {
             case 20011: // SDKErrorType.CAMERA_PERMISSION_ERROR
                 // Camera permission denied
                 showPermissionDialog();
                 break;
             case 20025: // SDKErrorType.NETWORK_ERROR
                 showErrorDialog("Connection error: " + exception.getMessage());
                 break;
             default:
                 showErrorDialog(exception.getMessage());
                 break;
          }
     } else if (exception instanceof TruoraException.ValidationApi) {
         // API-level errors (HTTP failures from the Truora API)
         showErrorDialog("API error: " + exception.getMessage());
     } 
}

Handling Example Kotlin

This is only the results handling example in Kotlin, the entire integration example is in the following section.

private fun handleResult(result: TruoraValidationResult<*>) {
    when (result) {
        is TruoraValidationCompleted<*> -> {
            val validation = result.value as ValidationResult 
            Log.i(TAG, "Validation completed successfully: ${validation.id}" +
                    "\n- Status: ${validation.status}" +
                    "\n- Type: ${validation.type}"
            )

            if (validation.status == ValidationStatus.SUCCESS) {
                // User passed the validation
                navigateToSuccessScreen()
            } else if (validation.status == ValidationStatus.FAILURE) {
                // User did not pass the validation criteria
                showRetryDialog()
            }
        }
        is TruoraValidationError<*> -> {
            handleSDKError(result)
        }
        is TruoraValidationCanceled<*> -> {
            // User cancelled - just go back
            goToPrevScreen()
        }
    }
}

private fun handleSDKError(truoraError: TruoraValidationError<*>) {
    val exception = truoraError.exception

    when (exception) {
        is TruoraException.SDK -> {
            // SDK-level errors (includes network errors)
            when (exception.getCode()) {
                20011 -> { // SDKErrorType.CAMERA_PERMISSION_ERROR
                    // Camera permission denied
                    showPermissionDialog()
                }
                else -> {
                    showErrorDialog(exception.message)
                }
            }
        }
        is TruoraException.ValidationApi -> {
            // API-level errors (HTTP failures from the Truora API)
            showErrorDialog("API error: ${exception.message}")
        }
 is TruoraException.Network -> {
            // Network connection errors 
            showErrorDialog("Connection error: ${exception.message}")
        }

    }
}

Java Implementation

Now that you understand configurations and result handling, let’s implement validations.

Basic Face Validation

Here’s a complete example of implementing face validation in Java:

package com.truora.sampleapp;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import com.truora.core.TruoraSDK;
import com.truora.core.shared.client.ValidationHandler;
import com.truora.errors.TruoraException;
import com.truora.interfaces.FaceValidationConfigFunction;
import com.truora.interfaces.TruoraAPIKeyGetter;
import com.truora.shared.models.TruoraValidationCanceled;
import com.truora.shared.models.TruoraValidationCompleted;
import com.truora.shared.models.TruoraValidationError;
import com.truora.shared.models.ValidationResult;

public class ValidationActivity extends AppCompatActivity implements TruoraAPIKeyGetter {
    private static final String TAG = "ValidationActivity";

    // Step 1: Create handler to register activity launcher
    private final ValidationHandler validationHandler = ValidationHandler.create(this);

    private TextView statusText;
    private Button startButton;
    private ProgressBar progressBar;
    private boolean isValidating = false;

    @Override
    public String getApiKeyFromSecureStorage() {
        // Retrieve from secure storage by customer development side
        return "YOUR_API_KEY";
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_validation);

        statusText = findViewById(R.id.status_text);
        startButton = findViewById(R.id.start_button);
        progressBar = findViewById(R.id.progress_bar);

        statusText.setText("Ready to start validation");

        startButton.setOnClickListener(v -> {
            if (!isValidating) {
                startFaceValidation();
            }
        });
    }

    private void startFaceValidation() {
        isValidating = true;
        statusText.setText("Starting face validation...");
        startButton.setEnabled(false);
        progressBar.setVisibility(View.VISIBLE);

        try {
            TruoraSDK.Validations.Builder builder = TruoraSDK.Validations
                    .Builder(this, "android-test-user");

            // Optional: Customize UI
            builder.withUIConfig(uiBuilder -> {
                uiBuilder.setPrimaryColor("#435AE0");
                uiBuilder.setSurfaceColor("#FFFFFF");
                uiBuilder.setErrorColor("#FF5454");
                uiBuilder.setLogo("https://your-cdn.com/logo.png");
                return uiBuilder.build();
            });

            // Configure face validation
            builder.withValidation((FaceValidationConfigFunction) config -> {
                config.useAutocapture(true)
                      .setSimilarityThreshold(0.8f)
                      .waitForResults(true);
                return config.build();
            });

            builder.build(validationHandler);
            validationHandler.start(this::handleValidationResult);

        } catch (TruoraException | IllegalArgumentException e) {
            Log.e(TAG, "Failed to initialize SDK", e);
            statusText.setText("Initialization failed: " + e.getMessage());
            resetUI();
        }
    }

    private void handleValidationResult(Object result) {
        runOnUiThread(() -> {
            resetUI();

            if (result instanceof TruoraValidationCompleted<?> success) {
                ValidationResult validation = (ValidationResult) success.getValue();
                String status = validation.getStatus();

                switch (status) {
                    case "success":
                        statusText.setText("Success! ID: " + validation.getId());
                        break;
                    case "failure":
                        statusText.setText("Validation failed. Please try again.");
                        break;
                    case "pending":
                        statusText.setText("Processing...");
                        break;
                    default:
                        statusText.setText("Status: " + status);
                        break;
                }

            } else if (result instanceof TruoraValidationError<?> error) {
                // Detailed error handling
                TruoraException exception = error.getException();

                if (exception instanceof TruoraException.SDK) {
                    switch (exception.getCode()) {
                        case 20011: // cameraPermissionError
                            statusText.setText("Camera permission required");
                            break;
                        case 20025: // networkError
                            statusText.setText("Connection error: " + exception.getMessage());
                            break;
                        default:
                            statusText.setText("Error: " + exception.getMessage());
                            break;
                    }
                } else if (exception instanceof TruoraException.ValidationApi) {
                    statusText.setText("API error: " + exception.getMessage());
                }

            } else if (result instanceof TruoraValidationCanceled<?> canceled) {
                ValidationResult partial = (ValidationResult) canceled.getValidationResult();
                if (partial != null) {
                    statusText.setText("Canceled. Partial ID: " + partial.getId());
                } else {
                    statusText.setText("Validation canceled by user");
                }
            }
        });
    }

    private void resetUI() {
        isValidating = false;
        startButton.setEnabled(true);
        progressBar.setVisibility(View.GONE);
    }
}

Document Validation

For document validation, use the DocumentValidationConfig:

    private void startDocumentValidation() {
        isValidating = true;
        statusText.setText("Starting document validation...");
        startButton.setEnabled(false);
        progressBar.setVisibility(View.VISIBLE);

        try {
            TruoraSDK.Validations.Builder builder = TruoraSDK.Validations
                    .Builder(this, "android-test-user");

            // Optional: Customize UI
            builder.withUIConfig(uiBuilder -> {
                uiBuilder.setPrimaryColor("#435AE0");
                uiBuilder.setSurfaceColor("#FFFFFF");
                uiBuilder.setErrorColor("#FF5454");
                uiBuilder.setLogo("https://your-cdn.com/logo.png");
                return uiBuilder.build();
            });

            // Configure document validation
            builder.withValidation((DocumentValidationConfigFunction) config -> {
                config.useAutocapture(true)
                      .waitForResults(true);
                return config.build();
            });

            builder.build(validationHandler);
            validationHandler.start(this::handleValidationResult);

        } catch (TruoraException | IllegalArgumentException e) {
            Log.e(TAG, "Failed to initialize SDK", e);
            statusText.setText("Initialization failed: " + e.getMessage());
            resetUI();
        }
    }

Kotlin Implementation

Basic Face Validation

Now that you understand configurations and result handling, let’s implement validations in Kotlin.

package com.truora.sampleapp

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.truora.core.TruoraSDK
import com.truora.core.shared.client.ValidationHandler
import com.truora.errors.TruoraException
import com.truora.interfaces.TruoraAPIKeyGetter
import com.truora.shared.models.TruoraValidationCanceled
import com.truora.shared.models.TruoraValidationCompleted
import com.truora.shared.models.TruoraValidationError
import com.truora.shared.models.ValidationResult

class ValidationActivity : AppCompatActivity(), TruoraAPIKeyGetter {

    companion object {
        private const val TAG = "ValidationActivity"
    }

    // Step 1: Create handler to register activity launcher
    private val validationHandler = ValidationHandler.create(this)

    // Using 'lateinit var' for views initialized in onCreate
    private lateinit var statusText: TextView
    private lateinit var startButton: Button
    private lateinit var progressBar: ProgressBar
    private var isValidating = false

    override fun getApiKeyFromSecureStorage(): String {
        // Retrieve from secure storage by customer development side
        return "YOUR_API_KEY"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_validation)

        // Initialize views
        statusText = findViewById(R.id.status_text)
        startButton = findViewById(R.id.start_button)
        progressBar = findViewById(R.id.progress_bar)

        statusText.text = "Ready to start validation"

        // Using Kotlin lambda for click listener
        startButton.setOnClickListener {
            if (!isValidating) {
                startFaceValidation()
            }
        }
    }

    private fun startFaceValidation() {
        isValidating = true
        statusText.text = "Starting face validation..."
        startButton.isEnabled = false
        progressBar.visibility = View.VISIBLE

        try {
            // Building the SDK configuration using chained calls and the 'apply' scope function
            TruoraSDK.Validations
                .Builder(this, "android-test-user")
                .apply {
                    // Optional: Customize UI. Using trailing lambda syntax.
                    withUIConfig { uiBuilder ->
                        uiBuilder.setPrimaryColor("#435AE0")
                        uiBuilder.setSurfaceColor("#FFFFFF")
                        uiBuilder.setErrorColor("#FF5454")
                        uiBuilder.setLogo("https://your-cdn.com/logo.png")
                        uiBuilder.build()
                    }
                    
                    // Configure face validation. Using trailing lambda syntax.
                    withValidation { config ->
                        config.useAutocapture(true)
                              .setSimilarityThreshold(0.8f)
                              .waitForResults(true)
                        config.build()
                    }
                }
                .build(validationHandler)
            
            // Using a Kotlin method reference (::) for the callback
            validationHandler.start(::handleValidationResult)

        } catch (e: TruoraException) {
            Log.e(TAG, "Failed to initialize SDK", e)
            statusText.text = "Initialization failed: ${e.message}"
            resetUI()
        } catch (e: IllegalArgumentException) {
            // Combined catch block for multiple exceptions
            Log.e(TAG, "Failed to initialize SDK", e)
            statusText.text = "Initialization failed: ${e.message}"
            resetUI()
        }
    }

    private fun handleValidationResult(result: Any) {
        // runOnUiThread is often omitted in modern Kotlin if using coroutines, 
        // but kept here for direct translation parity.
        runOnUiThread {
            resetUI()

            // Using 'when' expression for type-checking and smart-casting
            when (result) {
                is TruoraValidationCompleted<*> -> {
                    val validation = result.value as? ValidationResult ?: return@runOnUiThread
                    val status = validation.status

                    statusText.text = when (status) {
                        "success" -> "Success! ID: ${validation.id}"
                        "failure" -> "Validation failed. Please try again."
                        "pending" -> "Processing..."
                        else -> "Status: $status"
                    }
                }
                is TruoraValidationError<*> -> {
                    // Detailed error handling
                    val exception = result.exception

                    when (exception) {
                        is TruoraException.SDK -> {
                            statusText.text = when (exception.code) {
                                20011 -> "Camera permission required" // cameraPermissionError
                                20025 -> "Connection error: ${exception.message}" // networkError
                                else -> "Error: ${exception.message}"
                            }
                        }
                        is TruoraException.ValidationApi -> {
                            statusText.text = "API error: ${exception.message}"
                        }
                        else -> {
                            statusText.text = "Unknown error occurred."
                        }
                    }
                }
                is TruoraValidationCanceled<*> -> {
                    val partial = result.validationResult as? ValidationResult
                    
                    statusText.text = if (partial != null) {
                        "Canceled. Partial ID: ${partial.id}"
                    } else {
                        "Validation canceled by user"
                    }
                }
            }
        }
    }

    private fun resetUI() {
        isValidating = false
        startButton.isEnabled = true
        progressBar.visibility = View.GONE
    }
}

Document Validation

For document validation, use the DocumentValidationConfig:

 private fun startFaceValidation() {
        isValidating = true
        statusText.text = "Starting document validation..."
        startButton.isEnabled = false
        progressBar.visibility = View.VISIBLE

        try {
            // Building the SDK configuration using chained calls and the 'apply' scope function
            TruoraSDK.Validations
                .Builder(this, "android-test-user")
                .apply {
                    // Optional: Customize UI. Using trailing lambda syntax.
                    withUIConfig { uiBuilder ->
                        uiBuilder.setPrimaryColor("#435AE0")
                        uiBuilder.setSurfaceColor("#FFFFFF")
                        uiBuilder.setErrorColor("#FF5454")
                        uiBuilder.setLogo("https://your-cdn.com/logo.png")
                        uiBuilder.build()
                    }
                    
                    // Configure doc validation. Using trailing lambda syntax.
                    withValidation { config ->
                        config.useAutocapture(true)
                              .waitForResults(true)
                        config.build()
                    }
                }
                .build(validationHandler)
            
            // Using a Kotlin method reference (::) for the callback
            validationHandler.start(::handleValidationResult)

        } catch (e: TruoraException) {
            Log.e(TAG, "Failed to initialize SDK", e)
            statusText.text = "Initialization failed: ${e.message}"
            resetUI()
        } catch (e: IllegalArgumentException) {
            // Combined catch block for multiple exceptions
            Log.e(TAG, "Failed to initialize SDK", e)
            statusText.text = "Initialization failed: ${e.message}"
            resetUI()
        }
    }

Troubleshooting

Common Issues

  1. Camera not working on Android
    Solution: Verify that camera permissions are added to AndroidManifest.xml and that the user has granted permission at runtime.

  2. “API Key Invalid” error
    Solution: Verify your API key is correct and has the proper grants (generator or sdk type). Check that it hasn’t expired.

  3. Validation timeout
    Solution: Increase the timeout value in the config, or ensure the device has a stable internet connection.