🎯 Learning Path:
Welcome to the REL-ID Device Management codelab! This tutorial builds upon your existing MFA implementation to add comprehensive device management capabilities using REL-ID Android SDK's device management APIs.
In this codelab, you'll enhance your existing MFA application with:
getRegisteredDeviceDetails() APIBy completing this codelab, you'll master:
Before starting this codelab, ensure you have:
The code to get started can be found 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-device-management folder in the repository you cloned earlier
This codelab extends your MFA application with three core device management components:
These are implemented using:
Before implementing device management screens, let's understand the key SDK events and APIs that power the device lifecycle management workflow.
The device management process follows this event-driven pattern:
User Logs In → Navigate to Device Management →
getRegisteredDeviceDetails() Called → onGetRegistredDeviceDetails Callback →
Device List Displayed with Cooling Period Check →
User Taps Device → Navigate to Detail Screen →
User Taps Rename/Delete → Device Object Modified → updateDeviceDetails() Called →
onUpdateDeviceDetails Callback → Success/Error Alert → Navigate Back
The Android SDK uses callback-based events that are converted to reactive flows:
The REL-ID SDK provides these APIs and events for device management:
API/Event | Type | Description | User Action Required |
API | Fetch all registered devices with cooling period info | System calls automatically | |
Callback | Receives device list with metadata | System processes response | |
API | Rename or delete device with SDK objects | User taps action button | |
Callback | Update operation result with status codes | System handles response |
Android device management uses direct SDK object modification (no JSON):
Operation | SDK Method | Description |
Rename |
| Sets new name on SDK object |
Delete |
| Marks device for deletion |
Submit |
| Submits modified SDK object |
The onGetRegistredDeviceDetails callback returns SDK types:
// Callback signature
override fun onGetRegistredDeviceDetails(
status: RDNA.RDNAStatusGetRegisteredDeviceDetails
): Int
// Extract data using SDK getters
val devices = status.getDevices() // Array<RDNA.RDNADeviceDetails>
val coolingPeriodEnd = status.getDeviceManagementCoolingPeriodEndTimestamp()
val responseStatus = status.getStatus()
val statusCode = responseStatus?.statusCode?.intValue // 100 or 146
RDNADeviceDetails Key Methods:
device.getDeviceUUID(): String // Device unique identifier
device.getDeviceName(): String // Device display name
device.getDeviceStatus(): RDNADeviceStatus // ACTIVE, BLOCKED, etc.
device.getCurrentDevice(): Boolean // true if current device
device.getLastAccessEpochTime(): Long // Last access timestamp (ms)
device.getDeviceRegistrationEpochTime(): Long // Creation timestamp (ms)
device.getAppUUID(): String // Application identifier
device.getDeviceBinding(): RDNADeviceBinding // Device binding status
Cooling periods are server-enforced timeouts between device operations:
Status Code | Meaning | Cooling Period Active | Actions Allowed |
| Success | No | All actions enabled |
| Cooling period active | Yes | All actions disabled |
The getCurrentDevice() method identifies the active device:
getCurrentDevice() Value | Delete Button | Rename Button | Reason |
| ❌ Disabled/Hidden | ✅ Enabled | Cannot delete active device |
| ✅ Enabled | ✅ Enabled | Can delete non-current devices |
Device management implements comprehensive error detection:
// Layer 1: API-level errors
val error = status.getError()
if (error != null && error.longErrorCode != 0) {
// Handle API error
}
// Layer 2: Status code validation
val responseStatus = status.getStatus()
val statusCode = responseStatus?.statusCode?.intValue ?: 0
when (statusCode) {
0, 100 -> // Success
146 -> // Cooling period
else -> // Other errors
}
Let's implement the device management APIs in your RDNAService singleton following established patterns.
Add this method to
RDNAService.kt
:
// RDNAService.kt (addition to existing object)
/**
* Get registered device details
*
* NEW for Device Management codelab
* Retrieves list of all registered devices for the specified user.
* Results delivered via onGetRegistredDeviceDetails callback event.
*
* Response includes:
* - List of registered devices with their details
* - Cooling period end timestamp (if active)
* - Status codes (100 = success, 146 = cooling period active)
*
* @param userId The user ID to fetch device details for
* @return RDNAError with status (longErrorCode = 0 indicates success, async event follows)
*/
fun getRegisteredDeviceDetails(userId: String): RDNAError {
Log.d(TAG, "getRegisteredDeviceDetails() called for user: $userId")
val error = rdna.getRegisteredDeviceDetails(userId)
if (error.longErrorCode != 0) {
Log.e(TAG, "getRegisteredDeviceDetails sync error: ${error.errorString} (code: ${error.longErrorCode})")
} else {
Log.d(TAG, "getRegisteredDeviceDetails sync success - waiting for onGetRegistredDeviceDetails callback")
}
return error
}
Add this method to
RDNAService.kt
:
// RDNAService.kt (addition to existing object)
/**
* Update device details (rename or delete)
*
* NEW for Device Management codelab
* Updates device details - can rename or delete a device.
* Results delivered via onUpdateDeviceDetails callback event.
*
* Operation Pattern (Android-specific):
* 1. For RENAME: Call device.setNewDeviceName(newName) on the SDK object
* 2. For DELETE: Call device.deleteDevice() on the SDK object
* 3. Pass the modified SDK object to this method
* 4. SDK submits the changes to server
*
* The SDK object modification pattern:
* - device.setNewDeviceName() sets internal status to "Update"
* - device.deleteDevice() sets internal status to "Delete"
* - No manual JSON payload construction needed
*
* @param userId The user ID
* @param deviceDetails Array of RDNADeviceDetails with update information
* @return RDNAError with status (longErrorCode = 0 indicates success, async event follows)
*/
fun updateDeviceDetails(userId: String, deviceDetails: Array<RDNADeviceDetails>): RDNAError {
Log.d(TAG, "updateDeviceDetails() called for user: $userId")
Log.d(TAG, " Device count: ${deviceDetails.size}")
val error = rdna.updateDeviceDetails(userId, deviceDetails)
if (error.longErrorCode != 0) {
Log.e(TAG, "updateDeviceDetails sync error: ${error.errorString} (code: ${error.longErrorCode})")
} else {
Log.d(TAG, "updateDeviceDetails sync success - waiting for onUpdateDeviceDetails callback")
}
return error
}
RDNAService Singleton Pattern
The RDNAService is a Kotlin object singleton that wraps SDK methods:
object RDNAService {
private const val TAG = "RDNAService"
private lateinit var rdna: RDNA
fun getInstance(context: Context): RDNA {
if (!::rdna.isInitialized) {
rdna = RDNA.getInstance()
}
return rdna
}
// Device management methods
fun getRegisteredDeviceDetails(userId: String): RDNAError { /* ... */ }
fun updateDeviceDetails(userId: String, deviceDetails: Array<RDNADeviceDetails>): RDNAError { /* ... */ }
}
Key Characteristics:
Now let's enhance your RDNACallbackManager to handle device management events and convert them to reactive flows.
Add these SharedFlows to
RDNACallbackManager.kt
:
// RDNACallbackManager.kt (additions)
// === Device Management Events (NEW for Device Management Codelab) ===
/**
* Get registered device details events - Using SDK type directly
* NEW for Device Management codelab
* Triggered when getRegisteredDeviceDetails() response received
*/
private val _getRegisteredDeviceDetailsEvent = MutableSharedFlow<RDNA.RDNAStatusGetRegisteredDeviceDetails>()
val getRegisteredDeviceDetailsEvent: SharedFlow<RDNA.RDNAStatusGetRegisteredDeviceDetails> = _getRegisteredDeviceDetailsEvent.asSharedFlow()
/**
* Update device details events - Using SDK type directly
* NEW for Device Management codelab
* Triggered when updateDeviceDetails() response received
*/
private val _updateDeviceDetailsEvent = MutableSharedFlow<RDNA.RDNAStatusUpdateDeviceDetails>()
val updateDeviceDetailsEvent: SharedFlow<RDNA.RDNAStatusUpdateDeviceDetails> = _updateDeviceDetailsEvent.asSharedFlow()
Add these callback implementations to
RDNACallbackManager.kt
:
// RDNACallbackManager.kt (callback implementations)
/**
* onGetRegistredDeviceDetails callback - NEW for Device Management codelab
* Triggered when getRegisteredDeviceDetails() response received from server
* Emits SDK object directly - parsing handled in ViewModel layer
*
* Response contains:
* - Device list with details (devUUID, devName, status, timestamps, etc.)
* - Cooling period information (deviceManagementCoolingPeriodEndTimestamp)
* - Status codes (100 = success, 146 = cooling period active)
*/
override fun onGetRegistredDeviceDetails(status: RDNA.RDNAStatusGetRegisteredDeviceDetails): Int {
Log.d(TAG, "onGetRegistredDeviceDetails callback received")
scope.launch {
_getRegisteredDeviceDetailsEvent.emit(status)
}
return 0
}
/**
* onUpdateDeviceDetails callback - NEW for Device Management codelab
* Triggered when updateDeviceDetails() response received from server
* Emits SDK object directly - parsing handled in ViewModel layer
*
* Response contains:
* - Status codes (100 = success, 146 = cooling period active)
* - Updated device information
*/
override fun onUpdateDeviceDetails(status: RDNA.RDNAStatusUpdateDeviceDetails): Int {
Log.d(TAG, "onUpdateDeviceDetails callback received")
scope.launch {
_updateDeviceDetailsEvent.emit(status)
}
return 0
}
The RDNACallbackManager converts SDK callbacks to reactive flows:
Key Patterns:
Event Flow Architecture:
SDK Callback (Background Thread)
↓
RDNACallbackManager.onGetRegistredDeviceDetails()
↓
scope.launch { _getRegisteredDeviceDetailsEvent.emit(status) }
↓
DeviceManagementViewModel collects via Flow
↓
StateFlow updates UI state
↓
Composable observes with collectAsStateWithLifecycle()
↓
UI updates reactively
Create the DeviceManagementScreen that displays the device list with pull-to-refresh, cooling period detection, and navigation.
Create
DeviceManagementViewModel.kt
with this state class:
package com.relidcodelab.tutorial.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.relidcodelab.uniken.services.RDNACallbackManager
import com.relidcodelab.uniken.services.RDNAService
import com.uniken.rdna.RDNA
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
/**
* UI State for DeviceManagementScreen
*
* Stores original SDK RDNADeviceDetails objects directly
* instead of converting to custom model classes.
*/
data class DeviceManagementUiState(
val isLoading: Boolean = true,
val isRefreshing: Boolean = false,
val devices: List<RDNA.RDNADeviceDetails> = emptyList(), // ← SDK objects directly!
val isCoolingPeriodActive: Boolean = false,
val coolingPeriodEndTimestamp: Long? = null,
val coolingPeriodMessage: String = "",
val error: String? = null
)
State Properties:
devices: List - Stores SDK objects directly (no conversion)isCoolingPeriodActive: Boolean - Status code 146 indicates cooling periodisLoading/isRefreshing - Different loading states for UXerror: String? - Error messages for user feedbackContinue in
DeviceManagementViewModel.kt
:
/**
* Navigation event for device tap
* Passes original SDK object to detail screen
*/
data class NavigateToDeviceDetail(
val device: RDNA.RDNADeviceDetails, // ← SDK object directly!
val userID: String,
val isCoolingPeriodActive: Boolean,
val coolingPeriodEndTimestamp: Long?,
val coolingPeriodMessage: String
)
/**
* DeviceManagementViewModel
*
* Handles device list loading and cooling period management.
* Key Features:
* - Auto-load devices on screen mount
* - Pull-to-refresh functionality
* - Cooling period detection with status code 146
* - Current device highlighting
* - Navigation to device details
*/
class DeviceManagementViewModel(
private val rdnaService: RDNAService,
private val callbackManager: RDNACallbackManager,
private val userID: String
) : ViewModel() {
companion object {
private const val TAG = "DeviceManagementVM"
}
// UI State
private val _uiState = MutableStateFlow(DeviceManagementUiState())
val uiState: StateFlow<DeviceManagementUiState> = _uiState.asStateFlow()
// Navigation events
private val _navigateToDeviceDetail = MutableSharedFlow<NavigateToDeviceDetail>()
val navigateToDeviceDetail: SharedFlow<NavigateToDeviceDetail> = _navigateToDeviceDetail.asSharedFlow()
init {
setupEventHandlers()
}
/**
* Setup event handlers for SDK callbacks
*/
private fun setupEventHandlers() {
viewModelScope.launch {
callbackManager.getRegisteredDeviceDetailsEvent.collect { status ->
handleGetRegisteredDeviceDetailsResponse(status)
}
}
}
/**
* Handle get registered device details response
* Uses SDK getters to extract data
*/
private fun handleGetRegisteredDeviceDetailsResponse(
status: RDNA.RDNAStatusGetRegisteredDeviceDetails
) {
Log.d(TAG, "Received device details response")
try {
// Extract status code first
val responseStatus = status.getStatus()
val statusCode = responseStatus?.statusCode?.intValue ?: 0
val statusMsg = responseStatus?.message ?: ""
Log.d(TAG, "Response status code: $statusCode, message: $statusMsg")
// Check for errors
val error = status.getError()
if (error != null && error.longErrorCode != 0) {
Log.e(TAG, "Device details error: ${error.errorString} (code: ${error.longErrorCode})")
_uiState.update { it.copy(
isLoading = false,
isRefreshing = false,
error = error.errorString ?: "Failed to load devices"
)}
return
}
// Validate status code - only 0, 100, or 146 are valid
if (statusCode != 0 && statusCode != 100 && statusCode != 146) {
Log.e(TAG, "Invalid status code: $statusCode - $statusMsg")
_uiState.update { it.copy(
isLoading = false,
isRefreshing = false,
error = statusMsg.ifEmpty { "Failed to load devices. Status code: $statusCode" }
)}
return
}
// Extract device list from SDK response
val sdkDevices = status.getDevices() // Returns RDNADeviceDetails[]
val deviceList = if (sdkDevices != null && sdkDevices.isNotEmpty()) {
Log.d(TAG, "Device count from SDK: ${sdkDevices.size}")
sdkDevices.toList() // Convert array to list, keep SDK objects!
} else {
emptyList()
}
// Sort devices - current device first, then by last accessed
val sortedDevices = deviceList.sortedWith(
compareByDescending<RDNA.RDNADeviceDetails> { it.getCurrentDevice() }
.thenByDescending { it.getLastAccessEpochTime() ?: 0L }
)
// Extract cooling period info
val coolingPeriodEnd = status.getDeviceManagementCoolingPeriodEndTimestamp()
// Check if cooling period is active based on status code 146
val isCoolingActive = statusCode == 146
Log.d(TAG, "Loaded ${sortedDevices.size} devices, cooling period active: $isCoolingActive")
_uiState.update { it.copy(
isLoading = false,
isRefreshing = false,
devices = sortedDevices,
isCoolingPeriodActive = isCoolingActive,
coolingPeriodEndTimestamp = if (coolingPeriodEnd > 0) coolingPeriodEnd else null,
coolingPeriodMessage = statusMsg,
error = null
)}
} catch (e: Exception) {
Log.e(TAG, "Error parsing device details", e)
_uiState.update { it.copy(
isLoading = false,
isRefreshing = false,
error = "Failed to parse device details: ${e.message}"
)}
}
}
/**
* Load devices from SDK
*/
fun loadDevices() {
if (userID.isEmpty()) {
Log.e(TAG, "No userID available")
_uiState.update { it.copy(
isLoading = false,
error = "User ID is required to load devices"
)}
return
}
Log.d(TAG, "Loading devices for user: $userID")
_uiState.update { it.copy(isLoading = true, error = null) }
val error = rdnaService.getRegisteredDeviceDetails(userID)
if (error.longErrorCode != 0) {
Log.e(TAG, "getRegisteredDeviceDetails sync error: ${error.errorString}")
_uiState.update { it.copy(
isLoading = false,
error = error.errorString ?: "Failed to load devices"
)}
}
// Success - wait for callback event
}
/**
* Handle pull-to-refresh
*/
fun onRefresh() {
Log.d(TAG, "Pull to refresh triggered")
_uiState.update { it.copy(isRefreshing = true) }
loadDevices()
}
/**
* Handle device tap - navigate to detail screen
* Passes original SDK object to detail screen
*/
fun onDeviceTap(device: RDNA.RDNADeviceDetails) {
Log.d(TAG, "Device tapped: ${device.getDeviceUUID()}")
viewModelScope.launch {
_navigateToDeviceDetail.emit(NavigateToDeviceDetail(
device = device, // ← Pass original SDK object!
userID = userID,
isCoolingPeriodActive = _uiState.value.isCoolingPeriodActive,
coolingPeriodEndTimestamp = _uiState.value.coolingPeriodEndTimestamp,
coolingPeriodMessage = _uiState.value.coolingPeriodMessage
))
}
}
/**
* Clear error message
*/
fun clearError() {
_uiState.update { it.copy(error = null) }
}
}
ViewModels manage UI state using StateFlow:
Key ViewModel Patterns:
StateFlow - Holds UI stateSharedFlow - One-time navigation eventssetupEventHandlers() - Collects SDK events in init blockviewModelScope.launch - Lifecycle-aware coroutines_uiState.update { } - Thread-safe state updatesCreate
DeviceManagementScreen.kt
:
package com.relidcodelab.tutorial.screens.devicemanagement
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.relidcodelab.tutorial.viewmodels.DeviceManagementViewModel
import com.relidcodelab.tutorial.viewmodels.NavigateToDeviceDetail
import com.relidcodelab.ui.theme.*
import com.uniken.rdna.RDNA
import java.text.SimpleDateFormat
import java.util.*
/**
* Device Management Screen
*
* Displays all registered devices for the current user with pull-to-refresh functionality.
* Key Features:
* - Auto-load devices on screen mount
* - Pull-to-refresh functionality
* - Cooling period banner with countdown timer
* - Current device highlighting
* - Device list with friendly UI
* - Tap device to view details
*/
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
fun DeviceManagementScreen(
viewModel: DeviceManagementViewModel,
onMenuClick: () -> Unit,
onNavigateToDeviceDetail: (NavigateToDeviceDetail) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// Load devices on first composition
LaunchedEffect(Unit) {
viewModel.loadDevices()
}
// Handle navigation events
LaunchedEffect(Unit) {
viewModel.navigateToDeviceDetail.collect { event ->
onNavigateToDeviceDetail(event)
}
}
// Pull-to-refresh state
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.isRefreshing,
onRefresh = { viewModel.onRefresh() }
)
Scaffold(
containerColor = PageBackground,
topBar = {
DeviceManagementHeader(
onMenuClick = onMenuClick,
onRefreshClick = { viewModel.loadDevices() }
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
// Cooling Period Banner
if (uiState.isCoolingPeriodActive) {
CoolingPeriodBanner(message = uiState.coolingPeriodMessage)
}
// Main Content
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
when {
uiState.isLoading && !uiState.isRefreshing -> {
LoadingContent()
}
uiState.error != null -> {
ErrorContent(
error = uiState.error!!,
onRetry = { viewModel.loadDevices() }
)
}
uiState.devices.isEmpty() -> {
EmptyContent()
}
else -> {
// Device list
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = uiState.devices,
key = { it.getDeviceUUID() ?: "" } // ← Use SDK getter!
) { device ->
DeviceCard(
device = device,
onClick = { viewModel.onDeviceTap(device) }
)
}
}
}
}
// Pull-to-refresh indicator
PullRefreshIndicator(
refreshing = uiState.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
contentColor = PrimaryBlue
)
}
}
}
}
/**
* Header with menu button, title, and refresh button
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DeviceManagementHeader(
onMenuClick: () -> Unit,
onRefreshClick: () -> Unit
) {
TopAppBar(
title = {
Text(
text = "Device Management",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = DarkGray
)
},
navigationIcon = {
IconButton(onClick = onMenuClick) {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = "Menu",
tint = DarkGray
)
}
},
actions = {
IconButton(onClick = onRefreshClick) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = "Refresh",
tint = DarkGray
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = CardBackground
)
)
}
(Continue with helper composables...)
/**
* Cooling period warning banner
*/
@Composable
private fun CoolingPeriodBanner(message: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.background(
color = Color(0xFFFFF3CD),
shape = RoundedCornerShape(8.dp)
)
.padding(start = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Orange left border effect
Box(
modifier = Modifier
.width(4.dp)
.height(60.dp)
.background(Color(0xFFFF9800))
)
Spacer(modifier = Modifier.width(12.dp))
Text(text = "⏳", fontSize = 24.sp)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Cooling Period Active",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF856404)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = message,
fontSize = 14.sp,
color = Color(0xFF856404),
lineHeight = 20.sp
)
}
}
}
/**
* Device card component
* Accepts SDK RDNADeviceDetails object and uses getters
*/
@Composable
private fun DeviceCard(
device: RDNA.RDNADeviceDetails, // ← SDK object!
onClick: () -> Unit
) {
val isCurrentDevice = device.getCurrentDevice()
val deviceStatus = device.getDeviceStatus()
val isActive = deviceStatus == RDNA.RDNADeviceStatus.RDNA_DEVSTATUS_ACTIVE
// Get status string for display
val statusString = when (deviceStatus) {
RDNA.RDNADeviceStatus.RDNA_DEVSTATUS_ACTIVE -> "ACTIVE"
RDNA.RDNADeviceStatus.RDNA_DEVSTATUS_BLOCKED -> "BLOCKED"
RDNA.RDNADeviceStatus.RDNA_DEVSTATUS_SUSPEND -> "SUSPENDED"
else -> "UNKNOWN"
}
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = if (isCurrentDevice) Color(0xFFF1F8F4) else Color.White
),
border = if (isCurrentDevice) {
androidx.compose.foundation.BorderStroke(2.dp, Color(0xFF4CAF50))
} else {
androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFE0E0E0))
},
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Box(modifier = Modifier.padding(16.dp)) {
Column {
// Current Device Badge
if (isCurrentDevice) {
Box(
modifier = Modifier
.align(Alignment.End)
.background(
color = Color(0xFF4CAF50),
shape = RoundedCornerShape(4.dp)
)
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Text(
text = "Current Device",
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
// Device Name
Text(
text = device.getDeviceName() ?: "Unknown Device",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF2C3E50),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(
end = if (isCurrentDevice) 100.dp else 0.dp
)
)
Spacer(modifier = Modifier.height(8.dp))
// Device Status
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(8.dp)
.background(
color = if (isActive) Color(0xFF4CAF50) else Color(0xFFF44336),
shape = CircleShape
)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = statusString,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = if (isActive) Color(0xFF4CAF50) else Color(0xFFF44336)
)
}
Spacer(modifier = Modifier.height(12.dp))
// Device Details
DetailRow(
label = "Last Accessed:",
value = formatDate(device.getLastAccessEpochTime() ?: 0L)
)
Spacer(modifier = Modifier.height(6.dp))
DetailRow(
label = "Created:",
value = formatDate(device.getDeviceRegistrationEpochTime() ?: 0L)
)
Spacer(modifier = Modifier.height(12.dp))
// Tap indicator
Text(
text = "Tap for details →",
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF007AFF),
modifier = Modifier.align(Alignment.End)
)
}
}
}
}
/**
* Detail row component
*/
@Composable
private fun DetailRow(label: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = label,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF7F8C8D)
)
Text(
text = value,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF2C3E50)
)
}
}
/**
* Loading content
*/
@Composable
private fun LoadingContent() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(color = PrimaryBlue)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Loading devices...",
fontSize = 16.sp,
color = MediumGray
)
}
}
}
/**
* Error content
*/
@Composable
private fun ErrorContent(error: String, onRetry: () -> Unit) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Error: $error",
fontSize = 16.sp,
color = PrimaryRed
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onRetry,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)
) {
Text("Retry")
}
}
}
}
/**
* Empty content
*/
@Composable
private fun EmptyContent() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "No devices found",
fontSize = 16.sp,
color = MediumGray
)
}
}
/**
* Format timestamp to readable date string
*/
private fun formatDate(timestamp: Long): String {
return try {
val date = Date(timestamp)
val formatter = SimpleDateFormat("MMM d, yyyy, h:mm a", Locale.US)
formatter.format(date)
} catch (e: Exception) {
"Unknown"
}
}
Composables observe ViewModel state reactively:
Key Compose Patterns:
collectAsStateWithLifecycle() - Lifecycle-aware state collectionLaunchedEffect(Unit) - Run side effects on compositionrememberPullRefreshStateitems()getRegisteredDeviceDetails() when screen first loads using LaunchedEffect(Unit)rememberPullRefreshStatestatusCode == 146 to detect active cooling periodcollectAsStateWithLifecycle() to prevent memory leaksThe following image showcases the screen from the sample application:

Create the DeviceDetailScreen that shows device information and provides rename/delete actions.
Create
DeviceDetailViewModel.kt
with state:
package com.relidcodelab.tutorial.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.relidcodelab.uniken.services.RDNACallbackManager
import com.relidcodelab.uniken.services.RDNAService
import com.uniken.rdna.RDNA
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
/**
* UI State for DeviceDetailScreen
* Holds original SDK RDNADeviceDetails object
*/
data class DeviceDetailUiState(
val device: RDNA.RDNADeviceDetails, // ← SDK object directly!
val currentDeviceName: String,
val userID: String,
val isCoolingPeriodActive: Boolean,
val coolingPeriodMessage: String,
val isRenaming: Boolean = false,
val isDeleting: Boolean = false,
val showRenameDialog: Boolean = false,
val actionAlert: DeviceActionAlertState? = null
)
/**
* Alert dialog state (like React Native Alert.alert)
*/
data class DeviceActionAlertState(
val title: String,
val message: String,
val isSuccess: Boolean, // true = success (navigate after OK), false = error (stay on screen)
val operation: String // "rename" or "delete"
)
/**
* Navigation/result events
*/
sealed class DeviceDetailEvent {
object NavigateBack : DeviceDetailEvent()
}
Continue in
DeviceDetailViewModel.kt
:
/**
* DeviceDetailViewModel
*
* Handles device detail display and actions (rename/delete).
* Key Features:
* - Display device information
* - Rename device functionality
* - Delete device functionality
* - Cooling period awareness
*
* Receives original SDK RDNADeviceDetails object
* and uses it directly without recreation
*/
class DeviceDetailViewModel(
private val rdnaService: RDNAService,
private val callbackManager: RDNACallbackManager,
private val device: RDNA.RDNADeviceDetails, // ← Original SDK object!
userID: String,
isCoolingPeriodActive: Boolean,
coolingPeriodMessage: String
) : ViewModel() {
companion object {
private const val TAG = "DeviceDetailVM"
const val OPERATION_RENAME = 0
const val OPERATION_DELETE = 1
}
// UI State
private val _uiState = MutableStateFlow(DeviceDetailUiState(
device = device, // ← Store SDK object directly!
currentDeviceName = device.getDeviceName() ?: "Unknown Device",
userID = userID,
isCoolingPeriodActive = isCoolingPeriodActive,
coolingPeriodMessage = coolingPeriodMessage
))
val uiState: StateFlow<DeviceDetailUiState> = _uiState.asStateFlow()
// Events
private val _events = MutableSharedFlow<DeviceDetailEvent>()
val events: SharedFlow<DeviceDetailEvent> = _events.asSharedFlow()
init {
setupEventHandlers()
}
/**
* Setup event handlers for SDK callbacks
*/
private fun setupEventHandlers() {
viewModelScope.launch {
callbackManager.updateDeviceDetailsEvent.collect { status ->
handleUpdateDeviceDetailsResponse(status)
}
}
}
/**
* Handle update device details response
*/
private fun handleUpdateDeviceDetailsResponse(
status: RDNA.RDNAStatusUpdateDeviceDetails
) {
Log.d(TAG, "Received update device details response")
val isRenaming = _uiState.value.isRenaming
val isDeleting = _uiState.value.isDeleting
val operation = if (isRenaming) "rename" else "delete"
try {
// Check for errors
val error = status.getError()
if (error != null && error.longErrorCode != 0) {
Log.e(TAG, "$operation error: ${error.errorString} (code: ${error.longErrorCode})")
_uiState.update { it.copy(
actionAlert = DeviceActionAlertState(
title = "${operation.capitalize()} Failed",
message = error.errorString ?: "Failed to $operation device",
isSuccess = false,
operation = operation
),
isRenaming = false,
isDeleting = false
)}
return
}
// Check status code
val responseStatus = status.getStatus()
val statusCode = responseStatus?.statusCode?.intValue ?: 0
val statusMsg = responseStatus?.message ?: ""
Log.d(TAG, "$operation response - statusCode: $statusCode, statusMsg: $statusMsg")
// Validate status code
when {
statusCode == 0 || statusCode == 100 -> {
// Success - show alert dialog
Log.d(TAG, "$operation successful")
val successMessage = if (isRenaming) {
"Device renamed successfully"
} else {
"Device deleted successfully"
}
_uiState.update { it.copy(
actionAlert = DeviceActionAlertState(
title = "Success",
message = successMessage,
isSuccess = true,
operation = operation
),
isRenaming = false,
isDeleting = false
)}
}
statusCode == 146 -> {
// Cooling period active
Log.w(TAG, "Cooling period active")
_uiState.update { it.copy(
actionAlert = DeviceActionAlertState(
title = "$operation Failed",
message = "Device management is currently in cooling period. Please try again later.",
isSuccess = false,
operation = operation
),
isRenaming = false,
isDeleting = false
)}
}
else -> {
// Other error
Log.e(TAG, "$operation failed with status: $statusCode - $statusMsg")
_uiState.update { it.copy(
actionAlert = DeviceActionAlertState(
title = "$operation Failed",
message = statusMsg.ifEmpty { "Failed to $operation device" },
isSuccess = false,
operation = operation
),
isRenaming = false,
isDeleting = false
)}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error handling update response", e)
_uiState.update { it.copy(
actionAlert = DeviceActionAlertState(
title = "${operation.capitalize()} Failed",
message = "Failed to $operation device: ${e.message}",
isSuccess = false,
operation = operation
),
isRenaming = false,
isDeleting = false
)}
}
}
/**
* Show rename dialog
*/
fun showRenameDialog() {
_uiState.update { it.copy(showRenameDialog = true) }
}
/**
* Hide rename dialog
*/
fun hideRenameDialog() {
_uiState.update { it.copy(showRenameDialog = false) }
}
/**
* Rename device
*/
fun renameDevice(newName: String) {
Log.d(TAG, "Renaming device to: $newName")
_uiState.update { it.copy(
isRenaming = true,
currentDeviceName = newName // Optimistic update
)}
performDeviceUpdate(newName, OPERATION_RENAME)
}
/**
* Delete device
*/
fun deleteDevice() {
Log.d(TAG, "Deleting device: ${device.getDeviceUUID()}")
_uiState.update { it.copy(isDeleting = true) }
performDeviceUpdate(_uiState.value.currentDeviceName, OPERATION_DELETE)
}
/**
* Perform device update (rename or delete)
*
* Uses the original SDK RDNADeviceDetails object directly!
* No object recreation needed.
*
* For RENAME: call device.setNewDeviceName()
* For DELETE: call device.deleteDevice()
*/
private fun performDeviceUpdate(newName: String, operationType: Int) {
val userID = _uiState.value.userID
try {
// Use the original SDK object directly (no recreation!)
if (operationType == OPERATION_RENAME) {
// For rename: call setNewDeviceName() - SDK handles status internally
device.setNewDeviceName(newName)
Log.d(TAG, "Set new device name: $newName")
} else {
// For delete: call deleteDevice() - sets status to RDNA_DEVSTATUS_DELETE
device.deleteDevice()
Log.d(TAG, "Delete device: ${device.getDeviceUUID()}")
}
// Pass the same SDK object to updateDeviceDetails
val error = rdnaService.updateDeviceDetails(userID, arrayOf(device))
if (error.longErrorCode != 0) {
Log.e(TAG, "updateDeviceDetails sync error: ${error.errorString}")
_uiState.update { it.copy(
actionAlert = DeviceActionAlertState(
title = "${operationType.toString().capitalize()} Failed",
message = error.errorString ?: "Failed to update device",
isSuccess = false,
operation = if (operationType == OPERATION_RENAME) "rename" else "delete"
),
isRenaming = false,
isDeleting = false
)}
}
// Success - wait for callback event
} catch (e: Exception) {
Log.e(TAG, "Error creating device details", e)
_uiState.update { it.copy(
actionAlert = DeviceActionAlertState(
title = "${operationType.toString().capitalize()} Failed",
message = "Failed to update device: ${e.message}",
isSuccess = false,
operation = if (operationType == OPERATION_RENAME) "rename" else "delete"
),
isRenaming = false,
isDeleting = false
)}
}
}
/**
* Dismiss alert dialog
*/
fun dismissAlert() {
_uiState.update { it.copy(actionAlert = null) }
}
}
The key difference from React Native is the direct SDK object modification:
// Android: Direct SDK object modification
device.setNewDeviceName(newName) // Sets internal status to "Update"
rdnaService.updateDeviceDetails(userID, arrayOf(device))
// OR
device.deleteDevice() // Sets internal status to "Delete"
rdnaService.updateDeviceDetails(userID, arrayOf(device))
Create
DeviceDetailScreen.kt
: (showing key sections)
package com.relidcodelab.tutorial.screens.devicemanagement
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.relidcodelab.tutorial.viewmodels.*
import com.relidcodelab.ui.theme.*
import com.uniken.rdna.RDNA
import java.text.SimpleDateFormat
import java.util.*
/**
* Device Detail Screen
*
* Displays detailed information about a selected device.
* Key Features:
* - Complete device information display
* - Current device indicator
* - Status display
* - Cooling period awareness
* - Action buttons (rename/delete)
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DeviceDetailScreen(
viewModel: DeviceDetailViewModel,
onNavigateBack: () -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
// Handle navigation events
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is DeviceDetailEvent.NavigateBack -> {
onNavigateBack()
}
}
}
}
val device = uiState.device
val isCurrentDevice = device.getCurrentDevice()
val deviceStatus = device.getDeviceStatus()
val isActive = deviceStatus == RDNA.RDNADeviceStatus.RDNA_DEVSTATUS_ACTIVE
Scaffold(
containerColor = PageBackground,
topBar = {
DeviceDetailHeader(onBackClick = onNavigateBack)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
) {
// Current Device Banner
if (isCurrentDevice) {
CurrentDeviceBanner()
}
// Device Information Card
DeviceInfoCard(
deviceName = uiState.currentDeviceName,
status = when (deviceStatus) {
RDNA.RDNADeviceStatus.RDNA_DEVSTATUS_ACTIVE -> "ACTIVE"
RDNA.RDNADeviceStatus.RDNA_DEVSTATUS_BLOCKED -> "BLOCKED"
RDNA.RDNADeviceStatus.RDNA_DEVSTATUS_SUSPEND -> "SUSPENDED"
else -> "UNKNOWN"
},
isActive = isActive
)
// Device Details Card
DeviceDetailsCard(device = device)
// Access Information Card
AccessInfoCard(
lastAccessedEpoch = device.getLastAccessEpochTime() ?: 0L,
createdEpoch = device.getDeviceRegistrationEpochTime() ?: 0L
)
// Cooling Period Warning
if (uiState.isCoolingPeriodActive) {
CoolingPeriodWarning(message = uiState.coolingPeriodMessage)
}
// Action Buttons Card
ActionsCard(
isCurrentDevice = isCurrentDevice,
isCoolingPeriodActive = uiState.isCoolingPeriodActive,
isRenaming = uiState.isRenaming,
isDeleting = uiState.isDeleting,
onRenameClick = { viewModel.showRenameDialog() },
onDeleteClick = { showDeleteConfirmDialog = true }
)
Spacer(modifier = Modifier.height(32.dp))
}
}
// Rename Dialog
if (uiState.showRenameDialog) {
RenameDeviceDialog(
currentDeviceName = uiState.currentDeviceName,
isSubmitting = uiState.isRenaming,
onSubmit = { newName -> viewModel.renameDevice(newName) },
onCancel = { viewModel.hideRenameDialog() }
)
}
// Delete Confirmation Dialog
if (showDeleteConfirmDialog) {
AlertDialog(
onDismissRequest = { showDeleteConfirmDialog = false },
title = { Text("Delete Device") },
text = {
Text("Are you sure you want to delete \"${uiState.currentDeviceName}\"? This action cannot be undone.")
},
confirmButton = {
TextButton(
onClick = {
showDeleteConfirmDialog = false
viewModel.deleteDevice()
},
colors = ButtonDefaults.textButtonColors(contentColor = PrimaryRed)
) {
Text("Delete")
}
},
dismissButton = {
TextButton(onClick = { showDeleteConfirmDialog = false }) {
Text("Cancel")
}
}
)
}
// Success/Error Alert Dialog
uiState.actionAlert?.let { alert ->
AlertDialog(
onDismissRequest = {
viewModel.dismissAlert()
if (alert.isSuccess) {
onNavigateBack() // Navigate back after success
}
},
title = { Text(alert.title) },
text = { Text(alert.message) },
confirmButton = {
TextButton(
onClick = {
viewModel.dismissAlert()
if (alert.isSuccess) {
onNavigateBack() // Navigate back after success
}
}
) {
Text("OK")
}
}
)
}
}
(Include all helper composables from the reference file - DeviceDetailHeader, CurrentDeviceBanner, DeviceInfoCard, DeviceDetailsCard, AccessInfoCard, CoolingPeriodWarning, ActionsCard, InfoRow, formatDateLong, formatRelativeTime)
device.setNewDeviceName() and device.deleteDevice() SDK methodsisCoolingPeriodActive == truedevice.getCurrentDevice() checked before enabling deleteThe following image showcase the screen from the sample application:

Create the modal dialog for device renaming with input validation.
Create
RenameDeviceDialog.kt
:
package com.relidcodelab.tutorial.screens.devicemanagement
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
/**
* Rename Device Dialog
*
* Modal dialog for renaming a device with input validation.
*/
@Composable
fun RenameDeviceDialog(
currentDeviceName: String,
isSubmitting: Boolean,
onSubmit: (String) -> Unit,
onCancel: () -> Unit
) {
var newName by remember(currentDeviceName) { mutableStateOf(currentDeviceName) }
var error by remember { mutableStateOf<String?>(null) }
// Reset state when dialog opens
LaunchedEffect(currentDeviceName) {
newName = currentDeviceName
error = null
}
Dialog(onDismissRequest = { if (!isSubmitting) onCancel() }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Column {
// Header
Box(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
Text(
text = "Rename Device",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF2C3E50)
)
}
HorizontalDivider(color = Color(0xFFE0E0E0))
// Content
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
// Current Name
Text(
text = "Current Name:",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF7F8C8D)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = currentDeviceName,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF2C3E50)
)
Spacer(modifier = Modifier.height(20.dp))
// New Name
Text(
text = "New Name:",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF7F8C8D)
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = newName,
onValueChange = {
newName = it
error = null
},
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Enter new device name") },
enabled = !isSubmitting,
isError = error != null,
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF007AFF),
unfocusedBorderColor = Color(0xFFE0E0E0),
errorBorderColor = Color(0xFFE74C3C),
focusedContainerColor = Color(0xFFF8F9FA),
unfocusedContainerColor = Color(0xFFF8F9FA)
),
shape = RoundedCornerShape(8.dp)
)
// Error message
if (error != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = error!!,
fontSize = 12.sp,
color = Color(0xFFE74C3C)
)
}
}
HorizontalDivider(color = Color(0xFFE0E0E0))
// Actions
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
) {
// Cancel button
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(Color.Transparent),
contentAlignment = Alignment.Center
) {
TextButton(
onClick = onCancel,
enabled = !isSubmitting,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Cancel",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF7F8C8D)
)
}
}
// Vertical divider
Box(
modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.background(Color(0xFFE0E0E0))
)
// Submit button
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(
if (isSubmitting) Color(0xFF95A5A6) else Color(0xFF007AFF)
),
contentAlignment = Alignment.Center
) {
TextButton(
onClick = {
// Validation
val trimmedName = newName.trim()
when {
trimmedName.isEmpty() -> {
error = "Device name cannot be empty"
}
trimmedName == currentDeviceName -> {
error = "New name must be different from current name"
}
trimmedName.length < 3 -> {
error = "Device name must be at least 3 characters"
}
trimmedName.length > 50 -> {
error = "Device name must be less than 50 characters"
}
else -> {
onSubmit(trimmedName)
}
}
},
enabled = !isSubmitting,
modifier = Modifier.fillMaxSize()
) {
if (isSubmitting) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Text(
text = "Rename",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
}
}
}
}
}
}
}
}
The dialog implements comprehensive validation:
rememberThe following image showcases the screen from the sample application:

Integrate device management into your drawer navigation system.
Update
DrawerNavigator.kt
:
// DrawerNavigator.kt (additions)
import com.relidcodelab.tutorial.screens.devicemanagement.DeviceManagementScreen
import com.relidcodelab.tutorial.screens.devicemanagement.DeviceDetailScreen
import com.relidcodelab.tutorial.viewmodels.DeviceManagementViewModel
import com.relidcodelab.tutorial.viewmodels.DeviceDetailViewModel
import com.relidcodelab.tutorial.viewmodels.NavigateToDeviceDetail
// In SideMenuContent - add device management menu item
onDeviceManagementClick = {
scope.launch {
drawerState.close()
navController.navigate("DeviceManagement")
}
}
// In DrawerNavHost - add navigation state
var currentDeviceDetailData by remember { mutableStateOf<NavigateToDeviceDetail?>(null) }
// In NavHost - add composables
composable("DeviceManagement") {
val userID = dashboardViewModel.uiState.value.userID
val deviceManagementViewModel = remember(userID) {
DeviceManagementViewModel(
rdnaService = rdnaService,
callbackManager = callbackManager,
userID = userID
)
}
DeviceManagementScreen(
viewModel = deviceManagementViewModel,
onMenuClick = onOpenDrawer,
onNavigateToDeviceDetail = { event ->
// Store device data directly
currentDeviceDetailData = event
navController.navigate("DeviceDetail")
}
)
}
composable("DeviceDetail") {
val detailData = currentDeviceDetailData
if (detailData != null) {
val deviceDetailViewModel = remember(detailData) {
DeviceDetailViewModel(
rdnaService = rdnaService,
callbackManager = callbackManager,
device = detailData.device,
userID = detailData.userID,
isCoolingPeriodActive = detailData.isCoolingPeriodActive,
coolingPeriodMessage = detailData.coolingPeriodMessage
)
}
DeviceDetailScreen(
viewModel = deviceDetailViewModel,
onNavigateBack = {
currentDeviceDetailData = null
navController.popBackStack("DeviceManagement", inclusive = false)
}
)
} else {
// No device data available, navigate back
LaunchedEffect(Unit) {
navController.popBackStack("DeviceManagement", inclusive = false)
}
}
}
Device data is passed using in-memory state variables:
// Store navigation data
currentDeviceDetailData = NavigateToDeviceDetail(
device = device, // Original SDK object
userID = userID,
isCoolingPeriodActive = isCoolingPeriodActive,
coolingPeriodEndTimestamp = coolingPeriodEndTimestamp,
coolingPeriodMessage = coolingPeriodMessage
)
navController.navigate("DeviceDetail")
// Access in destination
val detailData = currentDeviceDetailData
if (detailData != null) {
// Use detailData.device (original SDK object)
}
Let's verify your device management implementation with comprehensive manual testing scenarios.
Steps:
Expected Logcat Output:
D/RDNAService: getRegisteredDeviceDetails() called for user: user@example.com
D/RDNAService: getRegisteredDeviceDetails sync success - waiting for callback
D/RDNACallbackManager: onGetRegistredDeviceDetails callback received
D/DeviceManagementViewModel: Device count: 3, Status code: 100
Expected Result: ✅ Device list displays with:
Steps:
Expected Logcat Output:
D/DeviceManagementViewModel: Pull-to-refresh triggered
D/RDNAService: getRegisteredDeviceDetails() called for user: user@example.com
D/RDNACallbackManager: onGetRegistredDeviceDetails callback received
Expected Behavior: ✅ Device list refreshes, refresh indicator disappears, updated device data displayed
Steps:
Expected Logcat Output:
D/DeviceManagementViewModel: Cooling period detected, Status code: 146
D/DeviceManagementViewModel: Cooling period end: [timestamp]
Expected Result: ✅
Steps:
Expected Logcat Output:
D/DeviceDetailViewModel: Renaming device to: My Updated Device
D/RDNAService: updateDeviceDetails() called for user: user@example.com
D/RDNACallbackManager: onUpdateDeviceDetails callback received
D/DeviceDetailViewModel: Rename successful, Status code: 100
Expected Result: ✅
Steps:
Expected Logcat Output:
D/DeviceDetailViewModel: Deleting device: [device name]
D/RDNAService: updateDeviceDetails() called for user: user@example.com
D/RDNACallbackManager: onUpdateDeviceDetails callback received
D/DeviceDetailViewModel: Delete successful, Status code: 100
Expected Result: ✅
Steps:
Expected Result: ✅
Steps:
Expected Logcat Output:
E/RDNAService: getRegisteredDeviceDetails sync error: Network error
E/DeviceManagementViewModel: Failed to load devices: Network timeout
Expected Result: ✅
Steps:
Expected Result: ✅
Enable verbose logging for debugging:
// In RDNAService.kt
companion object {
private const val TAG = "RDNAService"
private const val DEBUG = true // Set to false for production
}
// In ViewModels
companion object {
private const val TAG = "DeviceManagementViewModel" // or DeviceDetailViewModel
}
Key log patterns to watch:
D/RDNAService: - SDK API calls and responsesD/RDNACallbackManager: - SDK callback events receivedD/DeviceManagementViewModel: - State updates and business logicE/ prefix - Errors that need attentionUse Logcat filtering:
# Filter for device management logs
adb logcat | grep -E "(RDNAService|DeviceManagement|RDNACallback)"
# Filter for errors only
adb logcat *:E
# Filter with tag
adb logcat -s DeviceManagementViewModel
Device list not updating after refresh
Cooling period banner not showing
Error Code | Description | Solution |
0 | Success | Operation completed successfully |
StatusCode 100 | Success | Device operation successful |
StatusCode 146 | Cooling period active | Disable operations, show banner |
StatusCode != 100/146 | Server error | Check status message, contact support |
longErrorCode 1001 | Network error | Check connectivity, retry |
longErrorCode 1002 | Invalid user ID | Verify user ID parameter |
longErrorCode 1003 | Invalid device UUID | Verify device UUID |
Enable verbose logging:
// In RDNAService methods
Log.d(TAG, "Device details: ${status.getDevices()?.joinToString { it.getDeviceName() ?: "" }}")
Check event flow:
// In ViewModel
private fun setupEventHandlers() {
viewModelScope.launch {
callbackManager.getRegisteredDeviceDetailsEvent.collect { status ->
Log.d(TAG, "Event received: $status")
handleGetRegisteredDeviceDetailsResponse(status)
}
}
}
Organize your Android device management implementation following these file structure and architectural patterns.
Directory Organization:
app/src/main/
├── java/com/yourapp/
│ ├── uniken/
│ │ ├── services/
│ │ │ ├── RDNAService.kt (✅ Add getRegisteredDeviceDetails, updateDeviceDetails)
│ │ │ └── RDNACallbackManager.kt (✅ Add device management event flows)
│ │ ├── providers/
│ │ │ └── SDKEventProvider.kt (✅ Optional: Global device management events)
│ │ ├── models/
│ │ │ └── RDNAModels.kt (✅ Optional: Wrapper types if needed)
│ │ └── utils/
│ │ └── DeviceUtils.kt (✅ Optional: Helper functions)
│ └── tutorial/
│ ├── navigation/
│ │ ├── DrawerNavigator.kt (✅ Add DeviceManagement routes)
│ │ └── AppNavigation.kt (✅ Route definitions)
│ ├── screens/
│ │ └── devicemanagement/
│ │ ├── DeviceManagementScreen.kt (✅ NEW)
│ │ ├── DeviceDetailScreen.kt (✅ NEW)
│ │ ├── RenameDeviceDialog.kt (✅ NEW)
│ │ └── DeviceManagementComponents.kt (✅ NEW - Reusable UI components)
│ └── viewmodels/
│ ├── DeviceManagementViewModel.kt (✅ NEW)
│ └── DeviceDetailViewModel.kt (✅ NEW)
└── res/
├── values/
│ ├── strings.xml (✅ Add device management strings)
│ └── colors.xml (✅ Optional: Custom colors)
└── drawable/
└── ic_device.xml (✅ Optional: Device icons)
ViewModels:
Composable Screens:
Service Layer:
Compose Optimization:
collectAsStateWithLifecycle() for lifecycle-aware state observationremember for expensive computationsMemory Management:
State Management:
update { it.copy(...) }Network Optimization:
getRegisteredDeviceDetails() only when screen is first displayedBefore deploying to production, verify:
Device List:
Device Detail:
Device Operations:
Cooling Period:
Performance & Memory:
Logging & Debugging:
Congratulations! You've successfully implemented Device Management in Android with: