This codelab demonstrates how to implement the RELID Initialization flow using the REL-ID Android SDK (AAR). The RELID SDK provides secure identity verification and session management for mobile applications.

What You'll Learn

What You'll Need

Get the Code from GitHub

The code to get started is stored in a GitHub repository.

You can clone the repository using the following command:

git clone https://github.com/uniken-public/codelab-android.git

Navigate to the relid-initialize folder in the repository you cloned earlier

Before implementing your own RELID initialization, let's examine the sample app structure to understand the recommended architecture:

Component

Purpose

Sample App Reference

Connection Profile

Configuration data

app/src/main/res/raw/agent_info.json

Profile Parser

Utility to parse connection data

app/src/main/java/*/uniken/utils/ConnectionProfileParser.kt

Callback Manager

Handles SDK callbacks

app/src/main/java/*/uniken/services/RDNACallbackManager.kt

RELID Service

Main SDK interface

app/src/main/java/*/uniken/services/RDNAService.kt

Event Provider

Global event handling

app/src/main/java/*/uniken/providers/SDKEventProvider.kt

UI Components

Composable screens

app/src/main/java/*/tutorial/screens/TutorialHomeScreen.kt

Recommended Directory Structure

Create the following directory structure in your Android project:

YourApp/
├── app/
│   ├── build.gradle.kts            # Dependencies and configuration
│   ├── src/main/
│   │   ├── AndroidManifest.xml     # App permissions and config
│   │   ├── java/com/yourapp/
│   │   │   ├── MainActivity.kt     # App entry point
│   │   │   ├── tutorial/
│   │   │   │   ├── navigation/
│   │   │   │   │   └── AppNavigation.kt
│   │   │   │   ├── screens/        # Composable screens
│   │   │   │   │   ├── TutorialHomeScreen.kt
│   │   │   │   │   ├── TutorialSuccessScreen.kt
│   │   │   │   │   └── TutorialErrorScreen.kt
│   │   │   │   └── viewmodels/     # ViewModels with StateFlow
│   │   │   │       └── TutorialHomeViewModel.kt
│   │   │   └── uniken/             # REL-ID SDK integration
│   │   │       ├── providers/
│   │   │       │   └── SDKEventProvider.kt
│   │   │       ├── services/
│   │   │       │   ├── RDNAService.kt
│   │   │       │   └── RDNACallbackManager.kt
│   │   │       ├── models/
│   │   │       │   └── RDNAModels.kt
│   │   │       └── utils/
│   │   │           ├── ConnectionProfileParser.kt
│   │   │           └── ProgressHelper.kt
│   │   └── res/
│   │       ├── raw/                # Resources
│   │       │   └── agent_info.json
│   │       └── values/
│   │           ├── colors.xml
│   │           └── strings.xml
│   └── libs/                       # AAR files
│       └── REL-ID_API_SDK_vX.X.X_release.aar
├── build.gradle.kts                # Project-level Gradle config
└── settings.gradle.kts             # Project settings

Prerequisites

Ensure you have Android Studio installed:

# Check Android Studio and SDK
android --version  # or check in Android Studio > Settings > Languages & Frameworks > Android SDK

You need Android Studio Narwhal (2025.1.1) or later with:

Add SDK Dependency

Step 1: Add AAR File

Copy the REL-ID SDK AAR file to your project:

# Create libs directory if it doesn't exist
mkdir -p app/libs

# Copy AAR file (get from your Uniken administrator)
cp REL-ID_API_SDK_vX.X.X_release.aar app/libs/

Step 2: Configure Maven Repository

Add the Uniken Maven repository to settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()

        // Uniken Maven repository for REL-ID engine dependency
        maven {
            url = uri("https://pkgs.dev.azure.com/managevm/REL-ID-Thirdparty-Repo/_packaging/library/maven/v1")
        }
    }
}

Step 3: Configure build.gradle.kts

Add dependencies to app/build.gradle.kts:

dependencies {
    // REL-ID SDK
    implementation(files("libs/REL-ID_API_SDK_vX.X.X_release.aar"))
    implementation("com.uniken.relid:engine:X.X.Xen") {
        isTransitive = true
    }

    // Core Android
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)

    // Compose
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.material3)

    // ViewModel Compose
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")

    // Navigation Compose
    implementation("androidx.navigation:navigation-compose:2.7.6")

    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

    // JSON parsing
    implementation("com.google.code.gson:gson:2.10.1")
}

Step 4: Sync Gradle

Click "Sync Now" in the banner or:

./gradlew sync

Platform Configuration

Add required permissions to AndroidManifest.xml:

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

Follow the Android SDK setup guide for:

agent_info.json

Create your connection profile JSON file in app/src/main/res/raw/agent_info.json:

{
  "RelIds": [
    {
      "Name": "YourRELIDAgentName",
      "RelId": "your-rel-id-string-here"
    }
  ],
  "Profiles": [
    {
      "Name": "YourRELIDAgentName",
      "Host": "your-gateway-host.com",
      "Port": "443"
    }
  ]
}

Security Considerations

Define the data model and parser for type safety:

// app/src/main/java/*/uniken/models/ConnectionProfile.kt
package com.yourapp.uniken.models

data class ConnectionProfile(
    val relId: String,
    val host: String,
    val port: String
)
// app/src/main/java/*/uniken/utils/ConnectionProfileParser.kt
package com.yourapp.uniken.utils

import android.content.Context
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.yourapp.R
import com.yourapp.uniken.models.ConnectionProfile
import java.io.InputStreamReader

/**
 * Parses agent_info.json connection profile from raw resources
 */
object ConnectionProfileParser {

    /**
     * Loads and parses the connection profile from res/raw/agent_info.json
     *
     * @param context Application or Activity context
     * @return ConnectionProfile or null if parsing fails
     */
    fun loadFromRawResource(context: Context): ConnectionProfile? {
        return try {
            val inputStream = context.resources.openRawResource(R.raw.agent_info)
            val reader = InputStreamReader(inputStream)
            val json = Gson().fromJson(reader, JsonObject::class.java)
            reader.close()

            // Parse RelIds array
            val relIdsArray = json.getAsJsonArray("RelIds")
            val firstRelId = relIdsArray?.get(0)?.asJsonObject
            val relId = firstRelId?.get("RelId")?.asString ?: return null

            // Parse Profiles array
            val profilesArray = json.getAsJsonArray("Profiles")
            val firstProfile = profilesArray?.get(0)?.asJsonObject
            val host = firstProfile?.get("Host")?.asString ?: return null
            val port = firstProfile?.get("Port")?.asString ?: return null

            ConnectionProfile(relId, host, port)
        } catch (e: Exception) {
            android.util.Log.e("ConnectionProfileParser", "Failed to load connection profile", e)
            null
        }
    }
}

The parser performs several critical functions:

The callback manager handles all RELID SDK callbacks using Kotlin Coroutines and SharedFlow:

// app/src/main/java/*/uniken/services/RDNACallbackManager.kt
package com.yourapp.uniken.services

import android.app.Activity
import android.content.Context
import com.uniken.rdna.RDNA
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch

/**
 * RDNACallbackManager - Implements ALL RDNACallbacks interface methods
 *
 * CRITICAL: Implements all 58 methods from RDNACallbacks interface.
 * - Active callbacks (used in relid-initialize): onInitializeProgress, onInitializeError, onInitialized
 * - Remaining 55 methods: Stubbed for interface compliance
 *
 * Uses SharedFlow for reactive event handling with Kotlin coroutines.
 */
class RDNACallbackManager(
    private val context: Context,
    private val currentActivity: Activity?
) : RDNA.RDNACallbacks {

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

    // ===== ACTIVE EVENTS FOR RELID-INITIALIZE =====

    /**
     * Initialization progress events - Using SDK type directly
     */
    private val _initializeProgressEvent = MutableSharedFlow<RDNA.RDNAInitProgressStatus>()
    val initializeProgressEvent: SharedFlow<RDNA.RDNAInitProgressStatus> = _initializeProgressEvent.asSharedFlow()

    /**
     * Initialization error events - Using SDK type directly
     */
    private val _initializeErrorEvent = MutableSharedFlow<RDNA.RDNAError>()
    val initializeErrorEvent: SharedFlow<RDNA.RDNAError> = _initializeErrorEvent.asSharedFlow()

    /**
     * Initialization success events - Using SDK type directly
     */
    private val _initializedEvent = MutableSharedFlow<RDNA.RDNAChallengeResponse>()
    val initializedEvent: SharedFlow<RDNA.RDNAChallengeResponse> = _initializedEvent.asSharedFlow()

    // ===== RDNA CALLBACKS IMPLEMENTATION =====

    // === Context/Activity Methods (Required) ===

    override fun getDeviceContext(): Context {
        return context
    }

    override fun getCurrentActivity(): Activity? {
        return currentActivity
    }

    override fun getDeviceToken(): String {
        return "" // Not used in relid-initialize
    }

    // === Initialization Callbacks (ACTIVE) ===

    override fun onInitializeProgress(progress: RDNA.RDNAInitProgressStatus) {
        scope.launch {
            _initializeProgressEvent.emit(progress)
        }
    }

    override fun onInitializeError(error: RDNA.RDNAError) {
        scope.launch {
            _initializeErrorEvent.emit(error)
        }
    }

    override fun onInitialized(response: RDNA.RDNAChallengeResponse) {
        scope.launch {
            _initializedEvent.emit(response)
        }
    }

    // === Remaining 57 methods stubbed for interface compliance ===
    // See sample app for complete implementation

    override fun onGetNotifications(status: RDNA.RDNAStatusGetNotifications): Int = 0
    override fun onUpdateNotification(status: RDNA.RDNAStatusUpdateNotification): Int = 0
    override fun onGetNotificationsHistory(status: RDNA.RDNAStatusGetNotificationHistory): Int = 0
    override fun onTerminate(status: RDNA.RDNAStatusTerminate): Int = 0
    override fun onTerminateWithThreats(threats: Array<out RDNA.RDNAThreat>) {}
    override fun onPauseRuntime(status: RDNA.RDNAStatusPause): Int = 0
    override fun onResumeRuntime(status: RDNA.RDNAStatusResume): Int = 0
    override fun onConfigReceived(status: RDNA.RDNAStatusGetConfig): Int = 0
    override fun onGetAllChallengeStatus(status: RDNA.RDNAStatusGetAllChallenges): Int = 0
    override fun onGetPostLoginChallenges(status: RDNA.RDNAStatusGetPostLoginChallenges): Int = 0
    override fun onHandleCustomChallenge(userId: String?, challenge: String?, status: RDNA.RDNARequestStatus?, error: RDNA.RDNAError?) {}
    override fun onLogOff(status: RDNA.RDNAStatusLogOff): Int = 0
    override fun onUserLoggedOff(response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun getCredentials(userId: String?): RDNA.RDNAIWACreds? = null
    override fun onGetRegistredDeviceDetails(status: RDNA.RDNAStatusGetRegisteredDeviceDetails): Int = 0
    override fun onUpdateDeviceDetails(status: RDNA.RDNAStatusUpdateDeviceDetails): Int = 0
    override fun onSessionTimeout(sessionId: String?): Int = 0
    override fun onSessionTimeOutNotification(userId: String?, sessionId: String?, timeOut: Int, warningTime: Int, generalInfo: RDNA.RDNAGeneralInfo?) {}
    override fun onSessionExtensionResponse(sessionId: String?, generalInfo: RDNA.RDNAGeneralInfo?, status: RDNA.RDNARequestStatus?, error: RDNA.RDNAError?) {}
    override fun onSdkLogPrintRequest(level: RDNA.RDNALoggingLevel?, message: String?): Int = 0
    override fun getUser(userIds: Array<out String>?, challenge: String?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun getActivationCode(userId: String?, challenge: String?, attempts: Int, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun getPassword(userId: String?, mode: RDNA.RDNAChallengeOpMode?, attempts: Int, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun getDeviceName(userId: String?, challenge: String?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun getEmailOTP(userId: String?, challenge: String?, attempts: Int, mode: RDNA.RDNAChallengeOpMode?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun getSMSOTP(userId: String?, challenge: String?, attempts: Int, mode: RDNA.RDNAChallengeOpMode?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun addNewDeviceOptions(userId: String?, options: Array<out String>?, details: HashMap<String, String>?) {}
    override fun activateUserOptions(userId: String?, options: Array<out String>?, details: HashMap<String, String>?) {}
    override fun onUserLoggedIn(userId: String?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun getLoginId() {}
    override fun onLoginIdUpdateStatus(loginId: String?, status: RDNA.RDNARequestStatus?, error: RDNA.RDNAError?) {}
    override fun onForgotLoginIDStatus(status: RDNA.RDNARequestStatus?, error: RDNA.RDNAError?) {}
    override fun onTOTPRegistrationStatus(error: RDNA.RDNAError?) {}
    override fun onTOTPGenerated(userId: String?, secretKey: String?, timeRemaining: Int, error: RDNA.RDNAError?) {}
    override fun getTOTPPassword(userId: String?, attempt: Int, timeRemaining: Int, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun hideLoader() {}
    override fun showLoader() {}
    override fun onCredentialsAvailableForUpdate(userId: String?, credentials: Array<out String>?, error: RDNA.RDNAError?) {}
    override fun onUpdateCredentialResponse(userId: String?, credential: String?, status: RDNA.RDNARequestStatus?, error: RDNA.RDNAError?) {}
    override fun onUserConsentThreats(threats: Array<out RDNA.RDNAThreat>?) {}
    override fun getSecretAnswer(userId: String?, mode: RDNA.RDNAChallengeOpMode?, attempts: Int, question: RDNA.RDNASecretQuestionAnswer?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun onSelectSecretQuestionAnswer(userId: String?, mode: RDNA.RDNAChallengeOpMode?, questions: Array<out Array<String>>?, maxSelection: Int, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun onDeviceAuthManagementStatus(userId: String?, enabled: Boolean, capabilities: RDNA.RDNALDACapabilities?, status: RDNA.RDNARequestStatus?, error: RDNA.RDNAError?) {}
    override fun onAccessTokenRefreshed(accessToken: String?, status: RDNA.RDNARequestStatus?, error: RDNA.RDNAError?) {}
    override fun onUserEnrollmentResponse(userId: String?, status: RDNA.RDNARequestStatus?, error: RDNA.RDNAError?) {}
    override fun getUserConsentForLDA(userId: String?, mode: RDNA.RDNAChallengeOpMode?, capabilities: RDNA.RDNALDACapabilities?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun onAuthenticateUserAndSignData(details: RDNA.RDNADataSigningDetails?, status: RDNA.RDNARequestStatus?, error: RDNA.RDNAError?) {}
    override fun onAuthenticationOptionsAvailable(userId: String?, options: Array<out String>?, mode: RDNA.RDNAChallengeOpMode?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun onGetUserConsentForSimBinding(mode: RDNA.RDNAChallengeOpMode?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun onGetUserDetailsForSimBinding(mode: RDNA.RDNAChallengeOpMode?, simDetails: ArrayList<RDNA.RDNASimDetails>?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
    override fun onGetUserAccountConfirmationForSimBinding(mode: RDNA.RDNAChallengeOpMode?, accounts: Array<out String>?, response: RDNA.RDNAChallengeResponse?, error: RDNA.RDNAError?) {}
}

Key features of the callback manager:

The RELID service provides the main interface for SDK operations using Kotlin coroutines:

// app/src/main/java/*/uniken/services/RDNAService.kt
package com.yourapp.uniken.services

import android.content.Context
import android.util.Log
import com.uniken.rdna.RDNA
import com.yourapp.uniken.models.ConnectionProfile
import com.yourapp.uniken.utils.ConnectionProfileParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

/**
 * RDNAService - Simple wrapper for REL-ID SDK methods
 *
 * Provides Kotlin-friendly coroutine-based API for SDK methods.
 */
object RDNAService {

    private const val TAG = "RDNAService"
    private lateinit var rdna: RDNA

    /**
     * Get RDNA instance
     */
    fun getInstance(context: Context): RDNA {
        if (!::rdna.isInitialized) {
            rdna = RDNA.getInstance()
        }
        return rdna
    }

    /**
     * Get SDK version
     *
     * @return SDK version string or "Unknown" if error
     */
     fun getSDKVersion(): String {
        return try {
            Log.d(TAG, "Getting SDK version")
            val versionStatus = RDNA.getSDKVersion()

            val version = versionStatus?.result
            if (version != null) {
                Log.d(TAG, "SDK Version: $version")
                version
            } else {
                Log.w(TAG, "SDK version returned null")
                "Unknown"
            }
        } catch (e: Exception) {
            Log.e(TAG, "Failed to get SDK version", e)
            "Unknown"
        }
    }

    /**
     * Initialize REL-ID SDK
     *
     * @param context Application context
     * @param callbacks RDNACallbackManager instance
     * @return RDNAError - Error with longErrorCode = 0 indicates success (async events follow)
     * @throws Exception if initialization synchronously fails
     */
    suspend fun initialize(
        context: Context,
        callbacks: RDNACallbackManager
    ): RDNA.RDNAError = withContext(Dispatchers.IO) {
        Log.d(TAG, "Starting SDK initialization")

        // Load connection profile
        val profile = ConnectionProfileParser.loadFromRawResource(context)
            ?: throw Exception("Failed to load connection profile")

        Log.d(TAG, "Loaded connection profile: host=${profile.host}, port=${profile.port}")

        suspendCoroutine { continuation ->
            try {
                // Call SDK Initialize method
                val error = rdna.Initialize(
                    profile.relId,                          // agentInfo: REL-ID encrypted string
                    callbacks,                              // callbacks: RDNACallbacks interface
                    profile.host,                           // host: Gateway hostname
                    profile.port.toInt(),                   // port: Gateway port
                    "",                                     // cipherSpecs: Encryption format (optional)
                    "",                                     // cipherSalt: Cryptographic salt (optional)
                    null,                                   // proxySettings: Proxy config (optional)
                    null,                                   // sslCertificate: SSL cert (optional)
                    null,                                   // dnsServerList: DNS servers (optional)
                    RDNA.RDNALoggingLevel.RDNA_NO_LOGS,    // logLevel: Logging level
                    null                                    // reserved: Reserved parameter
                )

                Log.d(TAG, "Initialize sync response received - Error code: ${error?.longErrorCode}")

                if (error != null) {
                    if (error.longErrorCode == 0) {
                        // Success - async events will follow via callbacks
                        Log.d(TAG, "Initialize sync success, waiting for async events")
                        continuation.resume(error)
                    } else {
                        // Synchronous error
                        Log.e(TAG, "Initialize sync error: ${error.errorString}")
                        continuation.resumeWithException(
                            Exception("Initialize failed: ${error.errorString} (${error.longErrorCode})")
                        )
                    }
                } else {
                    Log.e(TAG, "Initialize returned null error")
                    continuation.resumeWithException(Exception("Initialize returned null"))
                }
            } catch (e: Exception) {
                Log.e(TAG, "Initialize exception", e)
                continuation.resumeWithException(e)
            }
        }
    }

    /**
     * Initialize REL-ID SDK with advanced configuration options
     *
     * @param context Application context
     * @param callbacks RDNACallbackManager instance
     * @param initOptions Optional configuration for language, permissions, and telemetry.
     *                    If not provided, uses default values suitable for most applications
     * @return RDNAError - Error with longErrorCode = 0 indicates success (async events follow)
     * @throws Exception if initialization synchronously fails
     *
     * Example usage:
     * ```kotlin
     * // Basic initialization (uses defaults)
     * rdnaService.initialize(context, callbacks)
     *
     * // Advanced initialization with custom configuration
     * val initOptions = RDNA.RDNAInitOptions().apply {
     *     // Language configuration
     *     rdnaLanguageOptions = RDNA.RDNALanguageOptions().apply {
     *         localeCode = "ar"                                        // ISO 639-1 code: 'en', 'ar', 'es', etc.
     *         localeName = "Arabic"                                    // Display name for the locale
     *         languageDirection = RDNA.RDNALanguageDirection.RDNA_LOCALE_RTL  // RTL for Arabic
     *     }
     *
     *     // Permission configuration
     *     permissionOptions = RDNA.RDNAPermissionOptions().apply {
     *         isLocationPermissionRequired = true    // Request location permission
     *         isLocationPermissionMandatory = false  // SDK can work without it
     *     }
     *
     *     // OpenTelemetry configuration
     *     rdnaotelConfig = RDNA.RDNAOtelConfig().apply {
     *         otelHTTPEndpointURL = ""      // Empty = disabled
     *         enableEncoding = ""
     *         disableTrace = 1              // 1 = disabled, 0 = enabled
     *         otelTraceFlushTimeout = 0
     *     }
     * }
     *
     * rdnaService.initialize(context, callbacks, initOptions)
     * ```
     */
    suspend fun initialize(
        context: Context,
        callbacks: RDNACallbackManager,
        initOptions: RDNA.RDNAInitOptions? = null
    ): RDNA.RDNAError = withContext(Dispatchers.IO) {
        Log.d(TAG, "Starting SDK initialization with initOptions")

        // Load connection profile
        val profile = ConnectionProfileParser.loadFromRawResource(context)
            ?: throw Exception("Failed to load connection profile")

        Log.d(TAG, "Loaded connection profile: host=${profile.host}, port=${profile.port}")

        // If no initOptions provided, create default configuration
        // Default configuration:
        // - Language: Empty (SDK defaults to 'en'), LTR direction
        // - Permissions: Location required but not mandatory
        // - Telemetry: Disabled (disableTrace: 1)
        val finalInitOptions = initOptions ?: RDNA.RDNAInitOptions()
        if (initOptions == null) {
            Log.d(TAG, "Using default initOptions (SDK defaults)")
        } else {
            Log.d(TAG, "Using provided initOptions")
        }

        suspendCoroutine { continuation ->
            try {
                // Call SDK Initialize method with initOptions
                // 12-parameter Initialize() method from AAR
                val error = rdna.Initialize(
                    profile.relId,                          // agentInfo: REL-ID encrypted string
                    callbacks,                              // callbacks: RDNACallbacks interface
                    profile.host,                           // host: Gateway hostname
                    profile.port.toInt(),                   // port: Gateway port
                    "",                                     // cipherSpecs: Encryption format (optional)
                    "",                                     // cipherSalt: Cryptographic salt (optional)
                    null,                                   // proxySettings: Proxy config (optional)
                    null,                                   // sslCertificate: SSL cert (optional)
                    null,                                   // dnsServerList: DNS servers (optional)
                    RDNA.RDNALoggingLevel.RDNA_NO_LOGS,    // logLevel: Logging level
                    null,                                   // reserved: Reserved parameter
                    finalInitOptions                        // initOptions: Advanced SDK configuration
                )

                Log.d(TAG, "Initialize sync response received - Error code: ${error?.longErrorCode}")

                if (error != null) {
                    if (error.longErrorCode == 0) {
                        // Success - async events will follow via callbacks
                        Log.d(TAG, "Initialize sync success, waiting for async events")
                        continuation.resume(error)
                    } else {
                        // Synchronous error
                        Log.e(TAG, "Initialize sync error: ${error.errorString}")
                        continuation.resumeWithException(
                            Exception("Initialize failed: ${error.errorString} (${error.longErrorCode})")
                        )
                    }
                } else {
                    Log.e(TAG, "Initialize returned null error")
                    continuation.resumeWithException(Exception("Initialize returned null"))
                }
            } catch (e: Exception) {
                Log.e(TAG, "Initialize exception", e)
                continuation.resumeWithException(e)
            }
        }
    }
}

Important API Parameters

The rdna.Initialize() call requires specific parameter ordering:

Parameter

Purpose

Example

agentInfo

RelId

From connection profile Ex. {"Name": "YourRELIDAgentName", "RelId": "your-rel-id-string-here"}

callbacks

RDNACallbackManager

Implements RDNACallbacks interface

host

Server hostname

From connection profile Ex. {"Host": "your-gateway-host.com", "Port": "443"}

port

Server port

From connection profile Ex. {"Host": "your-gateway-host.com", "Port": "443"}

logLevel

Logging setting

RDNA_NO_LOGS for production

initOptions

SDK configuration (optional)

RDNAInitOptions object with language, permissions, and telemetry settings

Key Patterns

The RELID SDK supports advanced configuration through the initOptions parameter. This allows you to customize language settings, permission requirements, and telemetry options based on your application's needs.

RDNAInitOptions Classes

The SDK provides the following classes for configuration (defined in the REL-ID AAR):

// Main configuration class
RDNA.RDNAInitOptions().apply {
    rdnaLanguageOptions = RDNA.RDNALanguageOptions()
    permissionOptions = RDNA.RDNAPermissionOptions()
    rdnaotelConfig = RDNA.RDNAOtelConfig()
}

// Language configuration
RDNA.RDNALanguageOptions().apply {
    localeCode: String         // ISO 639-1 language code (e.g., 'en', 'ar', 'es')
    localeName: String          // Display name for the locale
    languageDirection: RDNALanguageDirection  // RDNA_LOCALE_LTR or RDNA_LOCALE_RTL
}

// Permission configuration
RDNA.RDNAPermissionOptions().apply {
    isLocationPermissionRequired: Boolean   // Whether location permission is needed
    isLocationPermissionMandatory: Boolean  // If true, SDK fails without permission
}

// OpenTelemetry configuration
RDNA.RDNAOtelConfig().apply {
    otelHTTPEndpointURL: String      // OpenTelemetry HTTP endpoint URL
    enableEncoding: String            // Encoding format for OTel data
    disableTrace: Int                 // 0 = enable tracing, 1 = disable tracing
    otelTraceFlushTimeout: Int        // Timeout in milliseconds for flushing traces
}

Language Options Configuration

Language options control the SDK's display language and text direction (LTR/RTL):

// Example: Arabic (RTL) configuration
val language = "ar"  // Get from i18n library or user settings
val languageDirection = if (language in listOf("ar", "he", "fa", "ur")) {
    RDNA.RDNALanguageDirection.RDNA_LOCALE_RTL
} else {
    RDNA.RDNALanguageDirection.RDNA_LOCALE_LTR
}

val initOptions = RDNA.RDNAInitOptions().apply {
    rdnaLanguageOptions = RDNA.RDNALanguageOptions().apply {
        localeCode = language        // ISO 639-1 code: 'ar' for Arabic
        localeName = "Arabic"         // Display name
        languageDirection = RDNA.RDNALanguageDirection.RDNA_LOCALE_RTL  // RTL direction
    }
    // ... other options
}

RTL Languages: Arabic (ar), Hebrew (he), Persian (fa), Urdu (ur) LTR Languages: All others (default)

Common Use Cases:

Permission Options Configuration

Permission options define whether the SDK requires location access and if it's mandatory:

val initOptions = RDNA.RDNAInitOptions().apply {
    // ... language options
    permissionOptions = RDNA.RDNAPermissionOptions().apply {
        isLocationPermissionRequired = true   // Request location permission
        isLocationPermissionMandatory = false  // SDK works without it (limited functionality)
    }
    // ... otel config
}

Configuration Scenarios:

Scenario

Required

Mandatory

Behavior

Location-based features

true

false

SDK requests permission but works without it

Location required for compliance

true

true

SDK initialization fails without permission

No location needed

false

false

SDK doesn't request location permission

Use Cases:

OpenTelemetry (OTel) Configuration

OpenTelemetry configuration enables enterprise-level monitoring and distributed tracing:

val initOptions = RDNA.RDNAInitOptions().apply {
    // ... language options
    // ... permission options
    rdnaotelConfig = RDNA.RDNAOtelConfig().apply {
        otelHTTPEndpointURL = "https://your-otel-collector.example.com"  // Your OTel endpoint
        enableEncoding = ""           // Encoding format (empty for default)
        disableTrace = 0              // 0 = enabled, 1 = disabled
        otelTraceFlushTimeout = 0      // Flush timeout in milliseconds (0 = default)
    }
}

Default Configuration (Recommended for Most Apps):

rdnaotelConfig = RDNA.RDNAOtelConfig().apply {
    otelHTTPEndpointURL = ""  // Empty = telemetry disabled
    enableEncoding = ""
    disableTrace = 1          // Tracing disabled
    otelTraceFlushTimeout = 0
}

Use Cases:

Complete Configuration Example

Here's a complete example showing all three configuration areas:

// Example: Arabic banking app with location and telemetry
val initOptions = RDNA.RDNAInitOptions().apply {
    rdnaLanguageOptions = RDNA.RDNALanguageOptions().apply {
        localeCode = "ar"
        localeName = "Arabic"
        languageDirection = RDNA.RDNALanguageDirection.RDNA_LOCALE_RTL  // RTL
    }

    permissionOptions = RDNA.RDNAPermissionOptions().apply {
        isLocationPermissionRequired = true
        isLocationPermissionMandatory = false
    }

    rdnaotelConfig = RDNA.RDNAOtelConfig().apply {
        otelHTTPEndpointURL = "https://telemetry.yourbank.com"
        enableEncoding = ""
        disableTrace = 0  // Enabled for enterprise monitoring
        otelTraceFlushTimeout = 5000
    }
}

// Call initialize with custom configuration
val error = rdnaService.initialize(context, callbackManager, initOptions)

Backward Compatibility

The initOptions parameter is optional. Existing code without initOptions continues to work with default values:

// Works with default configuration
rdnaService.initialize(context, callbacks)

// Works with custom configuration
rdnaService.initialize(context, callbacks, initOptions)

Create the SDKEventProvider to handle global SDK events and navigation:

// app/src/main/java/*/uniken/providers/SDKEventProvider.kt
package com.yourapp.uniken.providers

import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import com.yourapp.tutorial.navigation.Routes
import com.yourapp.uniken.services.RDNACallbackManager
import com.uniken.rdna.RDNA
import kotlinx.coroutines.launch

/**
 * SDK Event Provider
 *
 * Centralized provider for REL-ID SDK global event handling.
 * Manages all global SDK events and navigation logic in one place.
 *
 * Key Features:
 * - Consolidated global event handling
 * - Success navigation routing to appropriate screens
 * - Lifecycle-aware event collection
 */
object SDKEventProvider {

    private const val TAG = "SDKEventProvider"

    /**
     * Navigation controller for routing
     */
    private var navController: NavController? = null

    /**
     * Initialize SDK Event Provider
     *
     * Sets up global event handlers that persist for the app lifecycle.
     *
     * @param lifecycleOwner Activity or Fragment lifecycle owner
     * @param callbackManager RDNACallbackManager instance
     * @param navController NavController for navigation
     */
    fun initialize(
        lifecycleOwner: LifecycleOwner,
        callbackManager: RDNACallbackManager,
        navController: NavController
    ) {
        Log.d(TAG, "Initializing SDK Event Provider")

        // Store navigation controller
        this.navController = navController

        // Setup event subscriptions with lifecycle awareness
        lifecycleOwner.lifecycleScope.launch {
            lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                setupEventHandlers(callbackManager)
            }
        }

        Log.d(TAG, "SDK Event Provider initialized successfully")
    }

    /**
     * Setup SDK Event Subscriptions
     *
     * Only handles GLOBAL events (success). Local events handled in ViewModels.
     */
    private suspend fun setupEventHandlers(callbackManager: RDNACallbackManager) {
        // Handle initialize success events (GLOBAL)
        callbackManager.initializedEvent.collect { response ->
            handleInitialized(response)
        }
    }

    /**
     * Event handler for successful initialization
     *
     * @param response RDNAChallengeResponse from SDK
     */
    private fun handleInitialized(response: RDNA.RDNAChallengeResponse) {
        // Extract data from SDK response
        val status = response.status
        val session = response.sessionInfo
        val sessionId = session?.sessionID ?: ""

        Log.d(TAG, "Successfully initialized, Session ID: $sessionId")

        // Extract session type using reflection
        val sessionType = try {
            session?.sessionType?.let { sessionTypeEnum ->
                val field = sessionTypeEnum.javaClass.getField("intValue")
                field.getInt(sessionTypeEnum)
            } ?: 0
        } catch (e: Exception) {
            Log.w(TAG, "Failed to extract sessionType", e)
            0
        }

        // Navigate to success screen
        navController?.navigate(
            Routes.tutorialSuccess(
                statusCode = status?.statusCode ?: 0,
                statusMessage = status?.statusMessage ?: "Unknown",
                sessionId = sessionId,
                sessionType = sessionType
            )
        )
    }

    /**
     * Cleanup event handlers
     * Called when provider is destroyed
     */
    fun cleanup() {
        Log.d(TAG, "Cleaning up SDK Event Provider")
        navController = null
    }
}

SDKEventProvider Pattern

The SDKEventProvider acts as a centralized global event handler for SDK events.

Architecture:

Why This Pattern?

Create a screen component using Jetpack Compose that handles user interaction and displays progress:

ViewModel Implementation

// app/src/main/java/*/tutorial/viewmodels/TutorialHomeViewModel.kt
package com.yourapp.tutorial.viewmodels

import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yourapp.uniken.services.RDNACallbackManager
import com.yourapp.uniken.services.RDNAService
import com.yourapp.uniken.utils.ProgressHelper
import com.uniken.rdna.RDNA
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

data class TutorialHomeUiState(
    val sdkVersion: String = "Loading...",
    val isInitializing: Boolean = false,
    val progressMessage: String = "",
    val showAlert: Boolean = false,
    val alertTitle: String = "",
    val alertMessage: String = ""
)

class TutorialHomeViewModel(
    private val context: Context,
    private val rdnaService: RDNAService,
    private val callbackManager: RDNACallbackManager
) : ViewModel() {

    // UI State as StateFlow
    private val _uiState = MutableStateFlow(TutorialHomeUiState())
    val uiState: StateFlow<TutorialHomeUiState> = _uiState.asStateFlow()

    // Local error navigation (success handled globally)
    private val _navigateToError = MutableSharedFlow<RDNA.RDNAError>()
    val navigateToError: SharedFlow<RDNA.RDNAError> = _navigateToError.asSharedFlow()

    init {
        loadSDKVersion()
        setupEventHandlers()
    }

    private fun loadSDKVersion() {
        val version = rdnaService.getSDKVersion()
        _uiState.update { it.copy(sdkVersion = version) }
    }

    private fun setupEventHandlers() {
        // Progress events
        viewModelScope.launch {
            callbackManager.initializeProgressEvent.collect { progress ->
                val message = ProgressHelper.getProgressMessage(progress)
                _uiState.update { it.copy(progressMessage = message) }
            }
        }

        // Error events
        viewModelScope.launch {
            callbackManager.initializeErrorEvent.collect { error ->
                _uiState.update { it.copy(isInitializing = false, progressMessage = "") }
                _navigateToError.emit(error)
            }
        }
    }

    fun onInitializeClick() {
        viewModelScope.launch {
            _uiState.update { it.copy(isInitializing = true, progressMessage = "Starting RDNA initialization...") }

            // ========================================
            // CONFIGURE SDK INITIALIZATION OPTIONS
            // ========================================
            // Customers: Customize this section based on your application requirements
            // If you don't provide initOptions, the SDK will use default values

            // OPTION 1: Use default configuration (recommended for most apps)
            // Simply call: rdnaService.initialize(context, callbackManager)
            // This uses: localeCode='en', languageDirection=LTR, location required
            // but not mandatory, telemetry disabled

            // OPTION 2: Customize configuration for your app needs
            // Example scenarios:

            // Scenario A: Arabic/RTL language support
            // val appLanguage = "ar"  // Get from your i18n system (e.g., Locale.getDefault().language, SharedPreferences, device settings)
            // val appLanguageDirection = if (appLanguage in listOf("ar", "he", "fa", "ur")) RDNA.RDNALanguageDirection.RDNA_LOCALE_RTL else RDNA.RDNALanguageDirection.RDNA_LOCALE_LTR

            // Scenario B: English/LTR language support (default)
            val appLanguage = "en"  // TODO: Replace with dynamic language from your app's i18n system
            val appLanguageDirection = RDNA.RDNALanguageDirection.RDNA_LOCALE_LTR  // LTR (Left-to-Right)

            // Scenario C: Location permission configuration
            // Set based on your app's requirements for location-based risk analysis
            val requireLocationPermission = true   // Does your app need location for fraud detection?
            val locationIsMandatory = false        // If false, SDK works with limited functionality without location

            // Scenario D: OpenTelemetry (OTel) configuration
            // Enable only if your organization uses OpenTelemetry for distributed tracing
            val enableTelemetry = false  // Set to true for enterprise monitoring and observability

            // Build the initOptions configuration object
            val initOptions = RDNA.RDNAInitOptions().apply {
                // Language options
                rdnaLanguageOptions = RDNA.RDNALanguageOptions().apply {
                    localeCode = appLanguage               // ISO 639-1 code: 'en', 'ar', 'es', 'fr', etc. (empty string defaults to 'en')
                    localeName = appLanguage               // Display name for the locale
                    languageDirection = appLanguageDirection  // RDNA_LOCALE_LTR or RDNA_LOCALE_RTL
                }

                // Permission options
                permissionOptions = RDNA.RDNAPermissionOptions().apply {
                    isLocationPermissionRequired = requireLocationPermission
                    isLocationPermissionMandatory = locationIsMandatory
                }

                // OpenTelemetry configuration
                rdnaotelConfig = RDNA.RDNAOtelConfig().apply {
                    otelHTTPEndpointURL = if (enableTelemetry) "https://your-otel-collector.example.com" else ""
                    enableEncoding = ""
                    disableTrace = if (enableTelemetry) 0 else 1  // 0 = enabled, 1 = disabled
                    otelTraceFlushTimeout = 0
                }
            }

            Log.d(TAG, "Initializing with custom options: language=$appLanguage, direction=$appLanguageDirection")

            // Call SDK initialize with custom configuration
            // Pass initOptions to customize SDK behavior
            // Or call without initOptions parameter: rdnaService.initialize(context, callbackManager) to use defaults
            try {
                val error = rdnaService.initialize(context, callbackManager, initOptions)

                // Check if there's an immediate error (not callback-based)
                if (error.longErrorCode != 0) {
                    Log.e(TAG, "Initialize returned error: ${error.errorString} (code: ${error.longErrorCode})")

                    // Show alert for certain error codes
                    // Error Code: 88 (RDNA_ERR_RDNA_ALREADY_INITIALIZED)
                    // Error Code: 179 (RDNA_ERR_INITIALIZE_ALREADY_IN_PROGRESS)
                    // Error Code: 218 (RDNA_ERR_DEVICE_SECURITY_CHECKS_FAILED_FRIDA_MODULES_DETECTED)
                    _uiState.update { it.copy(
                        isInitializing = false,
                        progressMessage = "",
                        showAlert = true,
                        alertTitle = "Initialization Failed",
                        alertMessage = "${error.errorString}\n\nError Codes:\nLong: ${error.longErrorCode}\nShort: ${error.shortErrorCode}"
                    )}
                }
            } catch (e: Exception) {
                Log.e(TAG, "Exception during initialize", e)
                _uiState.update { it.copy(
                    isInitializing = false,
                    progressMessage = "",
                    showAlert = true,
                    alertTitle = "Initialization Error",
                    alertMessage = "An unexpected error occurred: ${e.message}"
                )}
            }
        }
    }

    fun dismissAlert() {
        _uiState.update { it.copy(showAlert = false) }
    }
}

Composable Screen

// app/src/main/java/*/tutorial/screens/TutorialHomeScreen.kt
package com.yourapp.tutorial.screens

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourapp.tutorial.viewmodels.TutorialHomeViewModel

@Composable
fun TutorialHomeScreen(
    viewModel: TutorialHomeViewModel,
    onNavigateToError: (shortErrorCode: Int, longErrorCode: Int, errorString: String) -> Unit
) {
    // Collect state with lifecycle awareness
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // Collect error navigation events
    LaunchedEffect(Unit) {
        viewModel.navigateToError.collect { error ->
            onNavigateToError(error.shortErrorCode, error.longErrorCode, error.errorString ?: "")
        }
    }

    TutorialHomeScreenContent(
        uiState = uiState,
        onInitializeClick = viewModel::onInitializeClick,
        onDismissAlert = viewModel::dismissAlert
    )
}

@Composable
private fun TutorialHomeScreenContent(
    uiState: TutorialHomeUiState,
    onInitializeClick: () -> Unit,
    onDismissAlert: () -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // SDK Info Card
        Card {
            Column(modifier = Modifier.padding(16.dp)) {
                Text("REL-ID SDK", style = MaterialTheme.typography.titleLarge)
                Text("Version: ${uiState.sdkVersion}")
            }
        }

        // Initialize Button
        Button(
            onClick = onInitializeClick,
            enabled = !uiState.isInitializing,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(if (uiState.isInitializing) "Initializing..." else "Initialize SDK")
        }

        // Progress Container
        if (uiState.isInitializing && uiState.progressMessage.isNotEmpty()) {
            Card {
                Column(modifier = Modifier.padding(16.dp)) {
                    CircularProgressIndicator()
                    Spacer(modifier = Modifier.height(8.dp))
                    Text(uiState.progressMessage)
                }
            }
        }
    }

    // Alert Dialog
    if (uiState.showAlert) {
        AlertDialog(
            onDismissRequest = onDismissAlert,
            title = { Text(uiState.alertTitle) },
            text = { Text(uiState.alertMessage) },
            confirmButton = {
                TextButton(onClick = onDismissAlert) {
                    Text("OK")
                }
            }
        )
    }
}

Success Event Handling

The sample app uses SDKEventProvider for handling the onInitialized success event. This provider is initialized in MainActivity and automatically navigates to the success screen when initialization completes.

Key Implementation Features

Jetpack Compose Architecture:

Event-Driven Architecture:

Progress Tracking:

The following images showcase screens from the sample application:

Initialize Progress Screen

Initialize Error Screen

Initialize Screen

Build and Run

Using Android Studio:

  1. Open project in Android Studio
  2. Select target device or emulator
  3. Click Run (▶) or press Shift+F10

Using command line:

# Build APK
./gradlew assembleDebug

# Install on device
./gradlew installDebug

# Or combined
adb install app/build/outputs/apk/debug/app-debug.apk

Debugging

Logcat: View real-time logs with filtering

adb logcat | grep -E "(RDNA|YourApp)"

Android Studio Debugger: Use breakpoints and variable inspection

Layout Inspector: Inspect Compose UI hierarchy

Verification Steps

Test your implementation with the following scenarios:

  1. SDK Version Retrieval: Verify SDK version displays correctly on launch
  2. Successful Initialization: Click Initialize and verify complete flow works end-to-end
  3. Progress Tracking: Verify progress events are properly displayed during initialization
  4. Success Navigation: Verify automatic navigation to success screen on completion
  5. Error Handling: Test with invalid host/port to verify error screen navigation
  6. Invalid Credentials: Test with incorrect RelId values to verify error handling

Key Test Scenarios

Test initialization with various configurations:

// Test SDK version retrieval
val version = rdnaService.getSDKVersion()
Log.d(TAG, "SDK Version: $version")

// Test initialization
viewModelScope.launch {
    try {
        val result = rdnaService.initialize(context, callbacks)
        Log.d(TAG, "Initialization successful: ${result.longErrorCode}")
    } catch (e: Exception) {
        Log.e(TAG, "Initialization failed", e)
    }
}

Connection Profile Issues

Error: "Failed to load connection profile" Solution: Verify your JSON file exists in app/src/main/res/raw/agent_info.json and matches the required structure

Error: Parser returns null Solution: Ensure the JSON structure matches with RelIds and Profiles arrays properly formatted

Network Connectivity

Error: Network connection failures Solution: Check host/port values and network accessibility, verify INTERNET permission in AndroidManifest.xml

Error: REL-ID connection issues Solution: Verify the REL-ID server is set up and running, check firewall rules

SDK Initialization

Error Code 88: SDK already initialized Solution: Terminate the SDK before re-initializing

Error Code 288: SDK detected dynamic attack performed on the app Solution: Terminate the app and investigate security issues

Error Code 179: Initialization in progress Solution: Wait for current initialization to complete before retrying

Security Considerations

Memory Management

Threading

Performance

App Lifecycle

User Experience

Congratulations! You've successfully implemented RELID SDK initialization in Android with:

Key Android Patterns Learned

Next Steps