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.
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
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 |
|
Session Provider | Composable wrapper with event handling |
|
Session Dialog | UI with countdown and extension |
|
Event Data Classes | Session event models |
|
The RELID SDK triggers three main session management events:
Event Type | Description | User Action Required |
Hard session timeout - session already expired | User must acknowledge and app navigates to home | |
Idle session warning - session will expire soon | User can extend session or let it expire | |
Response from session extension API call | Handle success/failure of extension attempt |
The session management flow follows this pattern:
onSessionTimeOutNotification triggers with countdown and extension optionextendSessionIdleTimeout() APIonSessionExtensionResponse provides success/failure resultonSessionTimeout forces app navigation when session expiresDefine 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?
)
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
}
When handling session extension, two response layers must be considered:
Response Layer | Purpose | Success Criteria | Failure Handling |
Sync Response | API call validation |
| Immediate UI feedback |
Async Event | Extension result |
| 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:
|
|
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
}
}
}
}
}
}
The SessionProvider approach offers several advantages:
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 |
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
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"
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
}")
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
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
}
}
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
}
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.
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 |
// Proper coroutine management in SessionProvider
LaunchedEffect(callbackManager) {
// Coroutines launched here are automatically cancelled
// when SessionProvider leaves composition
launch { /* event collection */ }
}
Congratulations! You've successfully learned how to implement comprehensive session management functionality with:
Your session management implementation now provides: