This codelab demonstrates how to implement Session Management flow using the REL-ID Android SDK. Session management provides critical security features including automatic session timeout handling, idle session warnings with extension capabilities, and seamless session lifecycle management to prevent unexpected user logouts.

What You'll Learn

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-MFA-session-management folder in the repository you cloned earlier

What You'll Need

The sample app provides a complete session management implementation. Let's examine the key components:

Component

Purpose

Sample App Reference

Session Manager

Global session state management

app/src/main/java/*/uniken/session/SessionManager.kt

Session Provider

Composable wrapper with event handling

app/src/main/java/*/uniken/session/SessionProvider.kt

Session Dialog

UI with countdown and extension

app/src/main/java/*/uniken/session/SessionTimeoutDialog.kt

Event Data Classes

Session event models

app/src/main/java/*/uniken/models/RDNAModels.kt

Session Management Event Types

The RELID SDK triggers three main session management events:

Event Type

Description

User Action Required

onSessionTimeout

Hard session timeout - session already expired

User must acknowledge and app navigates to home

onSessionTimeOutNotification

Idle session warning - session will expire soon

User can extend session or let it expire

onSessionExtensionResponse

Response from session extension API call

Handle success/failure of extension attempt

Session Management Flow Architecture

The session management flow follows this pattern:

  1. SDK monitors session activity based on gateway configuration
  2. Idle Warning: onSessionTimeOutNotification triggers with countdown and extension option
  3. Extension Request: User can call extendSessionIdleTimeout() API
  4. Extension Response: onSessionExtensionResponse provides success/failure result
  5. Hard Timeout: onSessionTimeout forces app navigation when session expires

Define Kotlin data classes for comprehensive session timeout handling:

// app/src/main/java/*/uniken/models/RDNAModels.kt (additions)

/**
 * Session timeout event data (hard timeout - mandatory session expired)
 * Triggered when a mandatory session expires
 */
data class SessionTimeoutEventData(
    val sessionId: String?,
    val message: String
)

/**
 * Session timeout notification event data (idle timeout warning - can be extended)
 * Triggered when idle session is about to expire
 */
data class SessionTimeoutNotificationEventData(
    val userId: String?,
    val sessionId: String?,
    val timeLeftInSeconds: Int,
    val canExtend: Boolean,
    val generalInfo: RDNA.RDNAGeneralInfo?
)

/**
 * Session extension response event data
 * Triggered after calling extendSessionIdleTimeout()
 */
data class SessionExtensionResponseEventData(
    val sessionId: String?,
    val generalInfo: RDNA.RDNAGeneralInfo?,
    val status: RDNA.RDNARequestStatus?,
    val error: RDNA.RDNAError?
)

Understanding Session Timeout Types

Session management handles two distinct scenarios:

Session Type

Trigger

User Options

Implementation

Hard Timeout

Session already expired

Close button only

Navigate to home screen

Idle Warning

Session expiring soon

Extend or Close

API call or natural expiry

Extend your RDNACallbackManager to handle session management callbacks:

// app/src/main/java/*/uniken/services/RDNACallbackManager.kt (additions)
class RDNACallbackManager(
    private val context: Context,
    private val currentActivity: Activity?
) : RDNA.RDNACallbacks {

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

    // Session Management Events (SharedFlow for reactive handling)

    /**
     * Session timeout events - Hard timeout (mandatory session expired)
     */
    private val _sessionTimeoutEvent = MutableSharedFlow<SessionTimeoutEventData>()
    val sessionTimeoutEvent: SharedFlow<SessionTimeoutEventData> = _sessionTimeoutEvent.asSharedFlow()

    /**
     * Session timeout notification events - Idle timeout warning (can be extended)
     */
    private val _sessionTimeoutNotificationEvent = MutableSharedFlow<SessionTimeoutNotificationEventData>()
    val sessionTimeoutNotificationEvent: SharedFlow<SessionTimeoutNotificationEventData> = _sessionTimeoutNotificationEvent.asSharedFlow()

    /**
     * Session extension response events - Result after extending session
     */
    private val _sessionExtensionResponseEvent = MutableSharedFlow<SessionExtensionResponseEventData>()
    val sessionExtensionResponseEvent: SharedFlow<SessionExtensionResponseEventData> = _sessionExtensionResponseEvent.asSharedFlow()

    // Implement session management callbacks

    /**
     * onSessionTimeout - Hard session timeout (mandatory session expired)
     */
    override fun onSessionTimeout(sessionId: String?): Int {
        Log.d(TAG, "onSessionTimeout callback received:")
        Log.d(TAG, "  - sessionId: $sessionId")

        scope.launch {
            val eventData = SessionTimeoutEventData(
                sessionId = sessionId,
                message = "Your session has expired."
            )
            _sessionTimeoutEvent.emit(eventData)
        }

        return 0
    }

    /**
     * onSessionTimeOutNotification - Idle session timeout warning (can be extended)
     */
    override fun onSessionTimeOutNotification(
        userId: String?,
        sessionId: String?,
        timeOut: Int,
        warningTime: Int,
        generalInfo: RDNA.RDNAGeneralInfo?
    ) {
        Log.d(TAG, "onSessionTimeOutNotification callback received:")
        Log.d(TAG, "  - userId: $userId")
        Log.d(TAG, "  - sessionId: $sessionId")
        Log.d(TAG, "  - timeOut: $timeOut")
        Log.d(TAG, "  - warningTime: $warningTime")

        scope.launch {
            val eventData = SessionTimeoutNotificationEventData(
                userId = userId,
                sessionId = sessionId,
                timeLeftInSeconds = timeOut,
                canExtend = true, // Usually true for idle timeouts
                generalInfo = generalInfo
            )
            _sessionTimeoutNotificationEvent.emit(eventData)
        }
    }

    /**
     * onSessionExtensionResponse - Session extension result
     */
    override fun onSessionExtensionResponse(
        sessionId: String?,
        generalInfo: RDNA.RDNAGeneralInfo?,
        status: RDNA.RDNARequestStatus?,
        error: RDNA.RDNAError?
    ) {
        Log.d(TAG, "onSessionExtensionResponse callback received:")
        Log.d(TAG, "  - sessionId: $sessionId")
        Log.d(TAG, "  - status.statusCode: ${status?.statusCode}")
        Log.d(TAG, "  - status.statusMessage: ${status?.statusMessage}")
        Log.d(TAG, "  - error.longErrorCode: ${error?.longErrorCode}")
        Log.d(TAG, "  - error.errorString: ${error?.errorString}")

        scope.launch {
            val eventData = SessionExtensionResponseEventData(
                sessionId = sessionId,
                generalInfo = generalInfo,
                status = status,
                error = error
            )
            _sessionExtensionResponseEvent.emit(eventData)
        }
    }

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

Key features of session event handling:

Add session extension capability to your RELID service:

// app/src/main/java/*/uniken/services/RDNAService.kt (addition)
object RDNAService {

    /**
     * Extend idle session timeout
     *
     * This method extends the current idle session timeout when the session is eligible for extension.
     * Should be called in response to onSessionTimeOutNotification events when session can be extended.
     * After calling this method, the SDK will trigger an onSessionExtensionResponse event with the result.
     *
     * @see https://developer.uniken.com/docs/extend-session-timeout
     *
     * Response Validation Logic:
     * 1. Check error.longErrorCode: 0 = success, > 0 = error
     * 2. An onSessionExtensionResponse event will be triggered with detailed response
     * 3. The extension success/failure will be determined by the async event response
     *
     * @return RDNAError with status (longErrorCode = 0 indicates success, async event follows)
     */
    fun extendSessionIdleTimeout(): RDNAError {
        Log.d(TAG, "extendSessionIdleTimeout() called")

        val error = rdna.extendSessionIdleTimeout()

        if (error.longErrorCode != 0) {
            Log.e(TAG, "extendSessionIdleTimeout sync error: ${error.errorString} (code: ${error.longErrorCode})")
        } else {
            Log.d(TAG, "extendSessionIdleTimeout sync success, waiting for onSessionExtensionResponse event")
        }

        return error
    }

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

Important Session Extension Logic

When handling session extension, two response layers must be considered:

Response Layer

Purpose

Success Criteria

Failure Handling

Sync Response

API call validation

error.longErrorCode === 0

Immediate UI feedback

Async Event

Extension result

status.statusCode === 100 & error.longErrorCode === 0

Display error message

Note: The sync response only indicates the API call was accepted. The actual extension success/failure is communicated through the onSessionExtensionResponse event.

Create a SessionManager singleton to manage session state across your application:

// app/src/main/java/*/uniken/session/SessionManager.kt
object SessionManager {

    private const val TAG = "SessionManager"

    /**
     * Session state data class
     * Holds all session-related state for UI observation
     */
    data class SessionState(
        val isDialogVisible: Boolean = false,
        val sessionTimeoutData: SessionTimeoutEventData? = null,
        val sessionTimeoutNotificationData: SessionTimeoutNotificationEventData? = null,
        val isProcessing: Boolean = false,
        val shouldNavigateToHome: Boolean = false,
        val errorMessage: String? = null
    )

    // Session state flow (reactive state management)
    private val _sessionState = MutableStateFlow(SessionState())
    val sessionState: StateFlow<SessionState> = _sessionState.asStateFlow()

    // Service references
    private var rdnaService: RDNAService? = null

    /**
     * Initialize SessionManager
     * Sets up RDNAService reference. Called from MainActivity.
     */
    fun initialize(rdnaService: RDNAService) {
        Log.d(TAG, "Initializing SessionManager")
        this.rdnaService = rdnaService
        Log.d(TAG, "SessionManager initialized successfully")
    }

    /**
     * Show session timeout (hard timeout)
     * Called by SessionProvider when hard timeout event is received.
     * User must acknowledge and will be redirected to home screen.
     */
    fun showSessionTimeout(eventData: SessionTimeoutEventData) {
        Log.d(TAG, "Showing session timeout: ${eventData.message}")

        _sessionState.value = SessionState(
            isDialogVisible = true,
            sessionTimeoutData = eventData,
            sessionTimeoutNotificationData = null,
            isProcessing = false,
            shouldNavigateToHome = false
        )
    }

    /**
     * Show session timeout notification (idle timeout warning)
     * Called by SessionProvider when idle timeout notification is received.
     * User can choose to extend session or let it expire.
     */
    fun showSessionTimeoutNotification(eventData: SessionTimeoutNotificationEventData) {
        Log.d(TAG, "Showing session timeout notification:")
        Log.d(TAG, "  - userId: ${eventData.userId}")
        Log.d(TAG, "  - timeLeft: ${eventData.timeLeftInSeconds}s")
        Log.d(TAG, "  - canExtend: ${eventData.canExtend}")

        _sessionState.value = SessionState(
            isDialogVisible = true,
            sessionTimeoutData = null,
            sessionTimeoutNotificationData = eventData,
            isProcessing = false,
            shouldNavigateToHome = false
        )
    }

    /**
     * Handle session extension response
     * Called by SessionProvider when extension response is received.
     * Updates UI based on success or failure.
     */
    fun handleSessionExtensionResponse(eventData: SessionExtensionResponseEventData) {
        Log.d(TAG, "Handling session extension response:")
        Log.d(TAG, "  - status: ${eventData.status?.statusCode}")
        Log.d(TAG, "  - error: ${eventData.error?.longErrorCode}")

        val isSuccess = eventData.error?.longErrorCode == 0

        if (isSuccess) {
            Log.d(TAG, "Session extension successful - hiding dialog")
            hideDialog()
        } else {
            Log.w(TAG, "Session extension failed:")
            Log.w(TAG, "  - errorString: ${eventData.error?.errorString}")
            Log.w(TAG, "  - statusMessage: ${eventData.status?.statusMessage}")

            // Determine error message (prefer error string, fallback to status message)
            val errorMessage = if (eventData.error?.longErrorCode != 0) {
                eventData.error?.errorString ?: "Unknown error"
            } else {
                eventData.status?.statusMessage ?: "Extension failed"
            }

            // Update state to show error (processing false)
            _sessionState.value = _sessionState.value.copy(
                isProcessing = false,
                errorMessage = "Failed to extend session:\n$errorMessage"
            )
        }
    }

    /**
     * Extend session
     * Calls SDK to extend the idle session timeout. Shows loading state while processing.
     */
    fun extendSession() {
        Log.d(TAG, "User requested to extend session")

        if (_sessionState.value.isProcessing) {
            Log.w(TAG, "Extension already in progress, ignoring request")
            return
        }

        _sessionState.value = _sessionState.value.copy(isProcessing = true)

        try {
            val error = rdnaService?.extendSessionIdleTimeout()

            if (error?.longErrorCode == 0) {
                Log.d(TAG, "Session extension API called successfully")
                // Wait for onSessionExtensionResponse event
            } else {
                Log.e(TAG, "Session extension API failed: ${error?.errorString}")
                _sessionState.value = _sessionState.value.copy(isProcessing = false)
            }
        } catch (e: Exception) {
            Log.e(TAG, "Session extension exception", e)
            _sessionState.value = _sessionState.value.copy(isProcessing = false)
        }
    }

    /**
     * Dismiss dialog
     * Hides the session dialog. For hard timeout, sets navigation flag to return to home screen.
     */
    fun dismissDialog() {
        Log.d(TAG, "User dismissed session dialog")

        val shouldNavigate = _sessionState.value.sessionTimeoutData != null

        _sessionState.value = SessionState(
            isDialogVisible = false,
            sessionTimeoutData = null,
            sessionTimeoutNotificationData = null,
            isProcessing = false,
            shouldNavigateToHome = shouldNavigate
        )

        if (shouldNavigate) {
            Log.d(TAG, "Hard session timeout - navigation to home required")
        }
    }

    /**
     * Hide dialog
     * Hides the dialog without navigation (for successful extension).
     */
    fun hideDialog() {
        Log.d(TAG, "Hiding session dialog")

        _sessionState.value = SessionState(
            isDialogVisible = false,
            sessionTimeoutData = null,
            sessionTimeoutNotificationData = null,
            isProcessing = false,
            shouldNavigateToHome = false
        )
    }

    /**
     * Reset navigation flag
     * Clears the shouldNavigateToHome flag after navigation is handled.
     */
    fun resetNavigationFlag() {
        if (_sessionState.value.shouldNavigateToHome) {
            _sessionState.value = _sessionState.value.copy(shouldNavigateToHome = false)
            Log.d(TAG, "Navigation flag reset")
        }
    }

    /**
     * Clear error message
     * Clears the error message after alert is dismissed.
     */
    fun clearErrorMessage() {
        if (_sessionState.value.errorMessage != null) {
            _sessionState.value = _sessionState.value.copy(errorMessage = null)
            Log.d(TAG, "Error message cleared")
        }
    }
}

Key features of the session manager:

Create a Composable dialog to display session information and handle user interactions:

// app/src/main/java/*/uniken/session/SessionTimeoutDialog.kt
@Composable
fun SessionTimeoutDialog(
    visible: Boolean,
    sessionTimeoutData: SessionTimeoutEventData?,
    sessionTimeoutNotificationData: SessionTimeoutNotificationEventData?,
    isProcessing: Boolean,
    onExtendSession: () -> Unit,
    onDismiss: () -> Unit
) {
    if (!visible) return

    // Determine session type
    val isHardTimeout = sessionTimeoutData != null
    val isIdleTimeout = sessionTimeoutNotificationData != null
    val canExtend = sessionTimeoutNotificationData?.canExtend == true

    // Countdown timer for idle timeout
    val countdownKey = remember(sessionTimeoutNotificationData) {
        "${sessionTimeoutNotificationData?.userId}_${sessionTimeoutNotificationData?.timeLeftInSeconds}_${System.currentTimeMillis()}"
    }

    var countdown by remember(countdownKey) {
        val initialTime = sessionTimeoutNotificationData?.timeLeftInSeconds ?: 0
        Log.d("SessionTimeoutDialog", "Countdown initialized with: $initialTime seconds")
        mutableStateOf(initialTime)
    }

    // Countdown timer effect - updates every second
    LaunchedEffect(countdownKey) {
        while (countdown > 0 && isIdleTimeout) {
            delay(1000)
            countdown -= 1
        }
    }

    // Get display message
    val displayMessage = when {
        sessionTimeoutData != null -> sessionTimeoutData.message
        sessionTimeoutNotificationData != null -> "Your session will expire soon. Please extend to continue."
        else -> "Session timeout occurred."
    }

    // Get modal configuration (colors, titles, icons)
    val config = getModalConfig(isHardTimeout, isIdleTimeout, canExtend)

    Dialog(
        onDismissRequest = { /* Prevent dismissal - user must take action */ },
        properties = DialogProperties(
            dismissOnBackPress = false,
            dismissOnClickOutside = false
        )
    ) {
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 20.dp),
            shape = RoundedCornerShape(16.dp),
            colors = CardDefaults.cardColors(containerColor = Color.White)
        ) {
            Column(modifier = Modifier.fillMaxWidth()) {
                // Header with session type indicator
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(
                            color = config.headerColor,
                            shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
                        )
                        .padding(20.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Column(horizontalAlignment = Alignment.CenterHorizontally) {
                        Text(
                            text = config.title,
                            fontSize = 20.sp,
                            fontWeight = FontWeight.Bold,
                            color = Color.White,
                            textAlign = TextAlign.Center
                        )
                        Spacer(modifier = Modifier.height(8.dp))
                        Text(
                            text = config.subtitle,
                            fontSize = 14.sp,
                            color = Color.White.copy(alpha = 0.8f),
                            textAlign = TextAlign.Center,
                            lineHeight = 20.sp
                        )
                    }
                }

                // Content with countdown for idle timeout
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp),
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    // Icon
                    Text(
                        text = config.icon,
                        fontSize = 48.sp,
                        modifier = Modifier.padding(bottom = 16.dp)
                    )

                    // Message
                    Text(
                        text = displayMessage,
                        fontSize = 16.sp,
                        color = Color(0xFF1f2937),
                        textAlign = TextAlign.Center,
                        lineHeight = 24.sp,
                        modifier = Modifier.padding(bottom = 20.dp)
                    )

                    // Countdown display for idle timeout
                    if (isIdleTimeout) {
                        CountdownBox(countdown = countdown)
                    }
                }

                // Action Buttons
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp)
                        .padding(top = 0.dp)
                ) {
                    // Hard timeout - only Close option
                    if (isHardTimeout) {
                        Button(
                            onClick = onDismiss,
                            modifier = Modifier
                                .fillMaxWidth()
                                .height(56.dp),
                            shape = RoundedCornerShape(8.dp),
                            colors = ButtonDefaults.buttonColors(
                                containerColor = Color(0xFF6b7280)
                            )
                        ) {
                            Text(
                                text = "Close",
                                fontSize = 16.sp,
                                fontWeight = FontWeight.SemiBold
                            )
                        }
                    }

                    // Idle timeout - extend or dismiss options
                    if (isIdleTimeout) {
                        if (canExtend) {
                            Button(
                                onClick = onExtendSession,
                                enabled = !isProcessing,
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .height(56.dp),
                                shape = RoundedCornerShape(8.dp),
                                colors = ButtonDefaults.buttonColors(
                                    containerColor = Color(0xFF3b82f6)
                                )
                            ) {
                                if (isProcessing) {
                                    Row(
                                        horizontalArrangement = Arrangement.Center,
                                        verticalAlignment = Alignment.CenterVertically
                                    ) {
                                        CircularProgressIndicator(
                                            modifier = Modifier.size(20.dp),
                                            color = Color.White,
                                            strokeWidth = 2.dp
                                        )
                                        Spacer(modifier = Modifier.width(8.dp))
                                        Text(
                                            text = "Extending...",
                                            fontSize = 16.sp,
                                            fontWeight = FontWeight.SemiBold
                                        )
                                    }
                                } else {
                                    Text(
                                        text = "Extend Session",
                                        fontSize = 16.sp,
                                        fontWeight = FontWeight.SemiBold
                                    )
                                }
                            }

                            Spacer(modifier = Modifier.height(12.dp))
                        }

                        Button(
                            onClick = onDismiss,
                            enabled = !isProcessing,
                            modifier = Modifier
                                .fillMaxWidth()
                                .height(56.dp),
                            shape = RoundedCornerShape(8.dp),
                            colors = ButtonDefaults.buttonColors(
                                containerColor = Color(0xFF6b7280)
                            )
                        ) {
                            Text(
                                text = "Close",
                                fontSize = 16.sp,
                                fontWeight = FontWeight.SemiBold
                            )
                        }
                    }
                }
            }
        }
    }
}

/**
 * Countdown Box - Yellow countdown timer display
 * Shows time remaining in MM:SS format with yellow background.
 */
@Composable
private fun CountdownBox(countdown: Int) {
    val minutes = countdown / 60
    val seconds = countdown % 60
    val timeText = String.format("%d:%02d", minutes, seconds)

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .background(
                color = Color(0xFFfef3c7),
                shape = RoundedCornerShape(12.dp)
            )
            .border(
                width = 2.dp,
                color = Color(0xFFf59e0b),
                shape = RoundedCornerShape(12.dp)
            )
            .padding(16.dp),
        contentAlignment = Alignment.Center
    ) {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            Text(
                text = "Time Remaining:",
                fontSize = 14.sp,
                fontWeight = FontWeight.SemiBold,
                color = Color(0xFF92400e),
                modifier = Modifier.padding(bottom = 8.dp)
            )
            Text(
                text = timeText,
                fontSize = 32.sp,
                fontWeight = FontWeight.Bold,
                color = Color(0xFFd97706),
                fontFamily = FontFamily.Monospace
            )
        }
    }
}

/**
 * Modal configuration data
 */
private data class ModalConfig(
    val title: String,
    val subtitle: String,
    val headerColor: Color,
    val icon: String
)

/**
 * Get modal configuration based on session type
 */
private fun getModalConfig(
    isHardTimeout: Boolean,
    isIdleTimeout: Boolean,
    canExtend: Boolean
): ModalConfig {
    return when {
        isHardTimeout -> ModalConfig(
            title = "🔐 Session Expired",
            subtitle = "Your session has expired. You will be redirected to the home screen.",
            headerColor = Color(0xFFdc2626), // Red for hard timeout
            icon = "🔐"
        )
        isIdleTimeout && canExtend -> ModalConfig(
            title = "⚠️ Session Timeout Warning",
            subtitle = "Your session will expire soon. You can extend it or let it timeout.",
            headerColor = Color(0xFFf59e0b), // Orange for idle timeout
            icon = "⏱️"
        )
        isIdleTimeout -> ModalConfig(
            title = "⚠️ Session Timeout Warning",
            subtitle = "Your session will expire soon.",
            headerColor = Color(0xFFf59e0b), // Orange for idle timeout
            icon = "⏱️"
        )
        else -> ModalConfig(
            title = "⏰ Session Management",
            subtitle = "Session timeout notification",
            headerColor = Color(0xFF6b7280), // Gray default
            icon = "🔐"
        )
    }
}

Key features of the session dialog:

The following images showcase screens from the sample application:

Session Extend Screen

Session Timeout Screen

Create a SessionProvider Composable that subscribes to session events and wraps your app content:

// app/src/main/java/*/uniken/session/SessionProvider.kt
@Composable
fun SessionProvider(
    callbackManager: RDNACallbackManager,
    navController: NavHostController,
    content: @Composable () -> Unit
) {
    // Observe session state from SessionManager
    val sessionState by SessionManager.sessionState.collectAsStateWithLifecycle()

    // Subscribe to session callbacks directly (self-contained pattern)
    LaunchedEffect(callbackManager) {
        Log.d(TAG, "SessionProvider: Subscribing to session events")

        // Handle session timeout (hard timeout - mandatory)
        launch {
            callbackManager.sessionTimeoutEvent.collect { eventData ->
                Log.d(TAG, "SessionProvider: Session timeout received")
                SessionManager.showSessionTimeout(eventData)
            }
        }

        // Handle session timeout notification (idle timeout - can extend)
        launch {
            callbackManager.sessionTimeoutNotificationEvent.collect { eventData ->
                Log.d(TAG, "SessionProvider: Session timeout notification received")
                Log.d(TAG, "  - userId: ${eventData.userId}")
                Log.d(TAG, "  - timeLeft: ${eventData.timeLeftInSeconds}s")
                Log.d(TAG, "  - canExtend: ${eventData.canExtend}")
                SessionManager.showSessionTimeoutNotification(eventData)
            }
        }

        // Handle session extension response
        launch {
            callbackManager.sessionExtensionResponseEvent.collect { eventData ->
                Log.d(TAG, "SessionProvider: Session extension response received")
                Log.d(TAG, "  - status: ${eventData.status?.statusCode}")
                Log.d(TAG, "  - error: ${eventData.error?.longErrorCode}")
                SessionManager.handleSessionExtensionResponse(eventData)
            }
        }
    }

    // Handle navigation for hard timeout (redirect to home)
    LaunchedEffect(sessionState.shouldNavigateToHome) {
        if (sessionState.shouldNavigateToHome) {
            Log.d(TAG, "SessionProvider: Hard timeout - navigating to home screen")

            // Navigate to home and clear back stack
            navController.navigate(Routes.TUTORIAL_HOME) {
                popUpTo(0) { inclusive = true }
            }

            // Reset navigation flag
            SessionManager.resetNavigationFlag()
        }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        // App content (navigation, screens)
        content()

        // Global Session Timeout Dialog (overlay)
        SessionTimeoutDialog(
            visible = sessionState.isDialogVisible,
            sessionTimeoutData = sessionState.sessionTimeoutData,
            sessionTimeoutNotificationData = sessionState.sessionTimeoutNotificationData,
            isProcessing = sessionState.isProcessing,
            onExtendSession = {
                Log.d(TAG, "User chose to extend session")
                SessionManager.extendSession()
            },
            onDismiss = {
                Log.d(TAG, "User dismissed session dialog")
                SessionManager.dismissDialog()
            }
        )

        // Error Alert Dialog (for extension failures)
        sessionState.errorMessage?.let { errorMessage ->
            AlertDialog(
                onDismissRequest = {
                    Log.d(TAG, "User dismissed error alert")
                    SessionManager.clearErrorMessage()
                },
                title = { Text("Extension Failed") },
                text = { Text(errorMessage) },
                confirmButton = {
                    TextButton(
                        onClick = {
                            Log.d(TAG, "User confirmed error alert")
                            SessionManager.clearErrorMessage()
                        }
                    ) {
                        Text("OK")
                    }
                }
            )
        }
    }
}

private const val TAG = "SessionProvider"

Key features of the session provider:

Wrap your application with the SessionProvider:

// MainActivity.kt
class MainActivity : ComponentActivity() {

    private val rdnaService = RDNAService
    private lateinit var callbackManager: RDNACallbackManager
    private lateinit var navController: NavHostController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 1. Get RDNA instance
        rdnaService.getInstance(applicationContext)

        // 2. Create callback manager
        callbackManager = RDNACallbackManager(applicationContext, this)

        // 3. Initialize SessionManager
        SessionManager.initialize(rdnaService)

        // 4. Set Compose content
        setContent {
            RelidCodelabTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    // 5. Setup Navigation (store NavController)
                    val navCtrl = AppNavigation(
                        currentActivity = this,
                        rdnaService = rdnaService,
                        callbackManager = callbackManager
                    )
                    navController = navCtrl

                    // 6. Wrap in SessionProvider
                    SessionProvider(callbackManager, navCtrl) {
                        // Your app navigation and content
                    }
                }
            }
        }
    }
}

Integration Benefits

The SessionProvider approach offers several advantages:

Test Scenarios

  1. Hard Session Timeout: Test mandatory session expiration
  2. Idle Session Timeout with Extension: Test warning with successful extension
  3. Extension Failure: Test extension API failure handling
  4. Lifecycle Handling: Test countdown accuracy across configuration changes
  5. Multiple Session Events: Test handling of rapid session events

Common Session Management Test Scenarios

Session Type

Test Case

Expected Behavior

Validation Points

Hard Timeout

Session expires

Dialog with Close button only

Navigate to home screen

Idle Warning

Session expiring soon

Dialog with countdown and Extend button

Extension API call works

Extension Success

Extend session API succeeds

Dialog dismisses, session continues

No navigation occurs

Extension Failure

Extend session API fails

Error alert, dialog remains

User can retry or close

Lifecycle Changes

Configuration change during countdown

Timer state preserved

Countdown continues accurately

Testing with Gradle Commands

Build and run your application:

# Build APK
./gradlew assembleDebug

# Install on device
./gradlew installDebug

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

Debugging Session Events

Use Logcat to verify session functionality:

# View session-related logs
adb logcat | grep -E "(SessionManager|SessionProvider|SessionTimeoutDialog)"

# View all RDNA logs
adb logcat | grep -E "RDNA"

Verifying Callback Registration

Check that callbacks are properly registered:

// In your test or debug build
Log.d("SessionTest", "Session callbacks registered: ${
    callbackManager.sessionTimeoutEvent != null &&
    callbackManager.sessionTimeoutNotificationEvent != null &&
    callbackManager.sessionExtensionResponseEvent != null
}")

Dialog Not Appearing

Cause: Session callbacks not properly registered Solution: Verify SessionProvider wraps your app and SessionManager is initialized

// In MainActivity.onCreate()
SessionManager.initialize(rdnaService)

setContent {
    SessionProvider(callbackManager, navController) {
        // Your app content
    }
}

Cause: Event collection not active Solution: Check that SessionProvider's LaunchedEffect is running

Timer Accuracy Issues

Cause: State not preserved across configuration changes Solution: StateFlow automatically handles state preservation, but ensure proper ViewModel usage if needed

Cause: Coroutine cancelled during countdown Solution: Use LaunchedEffect with proper keys to ensure countdown continues

// Correct countdown implementation
LaunchedEffect(countdownKey) {
    while (countdown > 0 && isIdleTimeout) {
        delay(1000)
        countdown -= 1
    }
}

Extension API Failures

Cause: Calling extension API when session cannot be extended Solution: Check extension eligibility before API call

val canExtend = sessionTimeoutNotificationData?.canExtend == true
if (!canExtend) {
    // Show message that extension is not available
    return
}

Cause: Multiple concurrent extension requests Solution: SessionManager automatically prevents duplicates using isProcessing flag

if (_sessionState.value.isProcessing) {
    Log.w(TAG, "Extension already in progress, ignoring request")
    return
}

Navigation Issues

Cause: NavController not accessible in SessionProvider Solution: Ensure NavController is passed to SessionProvider

val navController = rememberNavController()

SessionProvider(callbackManager, navController) {
    AppNavigation(navController = navController)
}

Cause: Back stack not cleared after hard timeout Solution: Use proper navigation flags

navController.navigate(Routes.TUTORIAL_HOME) {
    popUpTo(0) { inclusive = true }
}

Best Practice: Test session management behavior on both physical devices and emulators with different timeout scenarios.

Important Security Guidelines

  1. Never bypass session timeouts - Always respect hard timeout requirements
  2. Limit extension attempts - Implement reasonable limits on extension requests
  3. Log session events securely - Track session management for security analysis
  4. Keep SDK updated - Regular updates include latest session security features
  5. Test thoroughly - Verify session behavior across different usage patterns

Session Extension Guidelines

Extension Scenario

Recommended Action

Implementation

Frequent Extensions

Set reasonable limits

Track extension count per session

Critical Operations

Allow extensions during important tasks

Context-aware extension logic

Inactive Sessions

Enforce timeouts

Don't extend completely idle sessions

Memory and Performance

// Proper coroutine management in SessionProvider
LaunchedEffect(callbackManager) {
    // Coroutines launched here are automatically cancelled
    // when SessionProvider leaves composition
    launch { /* event collection */ }
}

Session Data Protection

Congratulations! You've successfully learned how to implement comprehensive session management functionality with:

Key Security Benefits

Your session management implementation now provides:

Next Steps