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.
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 |
|
Profile Parser | Utility to parse connection data |
|
Callback Manager | Handles SDK callbacks |
|
RELID Service | Main SDK interface |
|
Event Provider | Global event handling |
|
UI Components | Composable screens |
|
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
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:
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/
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")
}
}
}
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")
}
Click "Sync Now" in the banner or:
./gradlew sync
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:
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"
}
]
}
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:
context.resources.openRawResource() for accessing raw resourcesThe 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)
}
}
}
}
The rdna.Initialize() call requires specific parameter ordering:
Parameter | Purpose | Example |
| RelId | From connection profile Ex. {"Name": "YourRELIDAgentName", "RelId": "your-rel-id-string-here"} |
| RDNACallbackManager | Implements RDNACallbacks interface |
| Server hostname | From connection profile Ex. {"Host": "your-gateway-host.com", "Port": "443"} |
| Server port | From connection profile Ex. {"Host": "your-gateway-host.com", "Port": "443"} |
| Logging setting |
|
| SDK configuration (optional) | RDNAInitOptions object with language, permissions, and telemetry settings |
suspend and withContext(Dispatchers.IO) for async SDK callssuspendCoroutine for structured concurrencyRDNAError on success (longErrorCode = 0), throws exception on failureThe 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.
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 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 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 |
|
| SDK requests permission but works without it |
Location required for compliance |
|
| SDK initialization fails without permission |
No location needed |
|
| SDK doesn't request location permission |
Use Cases:
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:
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)
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
}
}
The SDKEventProvider acts as a centralized global event handler for SDK events.
Architecture:
repeatOnLifecycleWhy This Pattern?
Create a screen component using Jetpack Compose that handles user interaction and displays progress:
// 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) }
}
}
// 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")
}
}
)
}
}
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.
Jetpack Compose Architecture:
Event-Driven Architecture:
Progress Tracking:
The following images showcase screens from the sample application:
|
|
|
Using Android Studio:
Using command line:
# Build APK
./gradlew assembleDebug
# Install on device
./gradlew installDebug
# Or combined
adb install app/build/outputs/apk/debug/app-debug.apk
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
Test your implementation with the following 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)
}
}
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
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
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
RDNA.RDNALoggingLevel.RDNA_NO_LOGS in productionrelId.substring(0, 10) + "...")collectAsStateWithLifecycle()Congratulations! You've successfully implemented RELID SDK initialization in Android with: