🎯 Learning Path:
Welcome to the REL-ID Push Notification Integration codelab! This tutorial enhances your existing REL-ID application with secure push notification capabilities using REL-ID SDK's getDeviceToken callback.
In this codelab, you'll enhance your existing REL-ID application with:
getDeviceToken callbackBy completing this codelab, you'll master:
Before starting this codelab, ensure you have:
google-services.json downloaded and placed in app/ directoryThe 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-push-notification-token folder in the repository you cloned earlier
REL-ID push notifications provide a secure, two-channel architecture that goes beyond standard push messaging with secure wake-up signals, MITM-proof channels, transaction approvals, and device-bound credentials.
REL-ID Flow: FCM Wake-up → App Launch → Secure REL-ID Channel → Encrypted Data Retrieval → User Action
This codelab implements two core components:
Before implementing push notifications, let's understand how REL-ID's secure notification system works.
REL-ID uses a sophisticated two-channel approach for maximum security:
📱 REL-ID Server → FCM (Wake-up Signal) → Mobile App → REL-ID Secure Channel → Encrypted Data
Here's how the getDeviceToken() callback enables secure REL-ID communications:
// Device Registration Flow
FCM Token Generation → Cache Token → SDK calls getDeviceToken() →
Return Cached Token → REL-ID Backend Registration → Secure Channel Establishment
Step | Description | Security Benefit |
1. FCM Token | Generate Firebase Cloud Messaging token | Device uniqueness |
2. Cache Token | Store token in PushNotificationService | Immediate callback response |
3. SDK Callback | SDK invokes | Token retrieval |
4. REL-ID Registration | SDK registers device with REL-ID backend | Device-server binding |
5. Secure Channel | Establish encrypted communication channel | MITM protection |
6. Transaction Support | Enable approve/reject actions with MFA | Multi-factor security |
Once integrated, your app can handle these secure notification types:
Firebase Role: Provides the platform infrastructure (FCM token generation and message delivery)
REL-ID Role: Provides the secure communication and transaction approval capabilities
Now let's implement the core push notification service that handles FCM token management and REL-ID SDK integration via the getDeviceToken() callback.
Create the singleton service that manages FCM token generation and caching:
// app/src/main/java/com/yourpackage/uniken/services/PushNotificationService.kt
package com.relidcodelab.uniken.services
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.tasks.await
/**
* PushNotificationService - FCM Token Manager for REL-ID SDK
*
* Handles Firebase Cloud Messaging (FCM) token generation and caching.
* The cached token is returned to SDK via getDeviceToken() callback.
*
* Flow:
* 1. App startup → initialize() generates FCM token
* 2. Token is cached in memory
* 3. SDK calls getDeviceToken() callback → returns cached token
*
* Features:
* - FCM token retrieval and caching
* - Android 13+ POST_NOTIFICATIONS permission handling
* - Automatic token refresh handling
* - Thread-safe token storage
*
* Usage:
* ```kotlin
* // Before SDK initialization
* PushNotificationService.initialize(context)
*
* // In RDNACallbackManager.getDeviceToken()
* return PushNotificationService.getCachedToken()
* ```
*/
object PushNotificationService {
private const val TAG = "PushNotificationService"
private var isInitialized = false
/**
* Cached FCM token - accessed by getDeviceToken() callback
* Thread-safe using @Volatile
*/
@Volatile
private var cachedFcmToken: String = ""
/**
* Get cached FCM token for SDK callback
*
* Called by RDNACallbackManager.getDeviceToken() when SDK requests the token.
*
* @return Cached FCM token string, or empty string if not available
*/
fun getCachedToken(): String {
Log.d(TAG, "getCachedToken() called, returning: ${if (cachedFcmToken.isNotEmpty()) "token (${cachedFcmToken.length} chars)" else "empty"}")
return cachedFcmToken
}
/**
* Initialize FCM and cache token for SDK
*
* MUST be called BEFORE SDK initialization so token is available
* when SDK invokes getDeviceToken() callback.
*
* Handles permission checks for Android 13+ (POST_NOTIFICATIONS).
*
* @param context Application context
*/
suspend fun initialize(context: Context) {
if (isInitialized) {
Log.d(TAG, "Already initialized")
return
}
Log.d(TAG, "Starting FCM initialization for Android ${Build.VERSION.SDK_INT}")
try {
// Check notification permission (Android 13+)
val hasPermission = checkNotificationPermission(context)
if (!hasPermission) {
Log.w(TAG, "POST_NOTIFICATIONS permission not granted")
Log.w(TAG, "Note: Permission should be requested in UI (Activity)")
// Continue anyway - token can still be generated
}
// Get and cache FCM token
generateAndCacheToken()
// Setup token refresh listener
setupTokenRefreshListener()
isInitialized = true
Log.d(TAG, "Initialization complete - token cached and ready for SDK callback")
} catch (error: Exception) {
Log.e(TAG, "Initialization failed", error)
throw error
}
}
/**
* Check notification permission (Android 13+)
*
* For Android 13 (API 33) and above, POST_NOTIFICATIONS permission is required.
* For older versions, notifications are enabled by default.
*
* @param context Application context
* @return true if permission granted or not required, false otherwise
*/
private fun checkNotificationPermission(context: Context): Boolean {
Log.d(TAG, "Platform Version: ${Build.VERSION.SDK_INT}")
// Android 13+ (API 33+) requires POST_NOTIFICATIONS permission
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val granted = ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
Log.d(TAG, "POST_NOTIFICATIONS permission: ${if (granted) "granted" else "denied"}")
granted
} else {
// Older Android versions don't require runtime permission
Log.d(TAG, "Android < 13, POST_NOTIFICATIONS not required")
true
}
}
/**
* Generate FCM token and cache it for SDK callback
*
* The cached token will be returned by getDeviceToken() callback
* when the SDK requests it during initialization.
*/
private suspend fun generateAndCacheToken() {
try {
Log.d(TAG, "Generating FCM token for Android")
// Get FCM token using coroutine-friendly await()
val token = FirebaseMessaging.getInstance().token.await()
if (token.isNullOrEmpty()) {
Log.w(TAG, "No FCM token received for Android")
cachedFcmToken = ""
return
}
// Cache token for getDeviceToken() callback
cachedFcmToken = token
Log.d(TAG, "FCM token generated and cached, length: ${token.length}")
Log.d(TAG, "FCM TOKEN: $token")
Log.d(TAG, "Token ready for SDK getDeviceToken() callback")
} catch (error: Exception) {
Log.e(TAG, "Token generation failed", error)
cachedFcmToken = ""
throw error
}
}
/**
* Set up automatic token refresh listener
*
* Handles token refresh and updates the cache.
* The SDK can retrieve the updated token on next getDeviceToken() call.
*/
private fun setupTokenRefreshListener() {
Log.d(TAG, "Setting up token refresh listener for Android")
// Note: Token refresh is handled by MyFirebaseMessagingService
// which extends FirebaseMessagingService and overrides onNewToken()
// That service should call updateCachedToken() when token refreshes
Log.d(TAG, "Token refresh will be handled by MyFirebaseMessagingService")
}
/**
* Update cached token (called by FirebaseMessagingService on refresh)
*
* This should be called from MyFirebaseMessagingService.onNewToken()
* to update the cached token when Firebase refreshes it.
*
* @param newToken Refreshed FCM token
*/
fun updateCachedToken(newToken: String) {
Log.d(TAG, "Updating cached token, length: ${newToken.length}")
cachedFcmToken = newToken
Log.d(TAG, "REFRESHED FCM TOKEN: $newToken")
Log.d(TAG, "Updated token will be available for next SDK callback")
}
/**
* Get current FCM token directly from Firebase (for debugging)
*
* This bypasses the cache and queries Firebase directly.
*
* @return Current FCM token or null if not available
*/
suspend fun getCurrentTokenFromFirebase(): String? {
return try {
FirebaseMessaging.getInstance().token.await()
} catch (error: Exception) {
Log.e(TAG, "Failed to get current token from Firebase", error)
null
}
}
/**
* Cleanup (reset initialization state and cached token)
*/
fun cleanup() {
Log.d(TAG, "Cleanup")
isInitialized = false
cachedFcmToken = ""
}
}
This implementation follows enterprise-grade patterns:
Pattern | Benefit | Implementation |
Singleton | Single point of control |
|
Caching | Immediate callback response |
|
Coroutines | Non-blocking async operations |
|
State Management | Prevents double initialization |
|
Permission Handling | Android 13+ compatibility | Version checks with |
Create a Firebase Messaging Service to handle token refresh events automatically.
This service extends FirebaseMessagingService to handle token refresh:
// app/src/main/java/com/yourpackage/uniken/services/MyFirebaseMessagingService.kt
package com.relidcodelab.uniken.services
import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
/**
* MyFirebaseMessagingService - Handles FCM token refresh and push messages
*
* Extends FirebaseMessagingService to handle:
* - Token refresh events (onNewToken)
* - Incoming push notifications (onMessageReceived)
*
* This service is automatically invoked by Firebase when:
* 1. App is first installed and token is generated
* 2. Token is refreshed by Firebase
* 3. Push notifications are received
*
* Registration:
* Must be declared in AndroidManifest.xml with intent-filter for firebase messaging
*/
class MyFirebaseMessagingService : FirebaseMessagingService() {
companion object {
private const val TAG = "MyFCMService"
}
/**
* Called when FCM token is refreshed
*
* This is called by Firebase when:
* - App is first installed
* - Token is refreshed
* - App is reinstalled
* - App data is cleared
*
* The new token must be sent to the REL-ID SDK by updating the cache.
*
* @param token The new FCM token
*/
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d(TAG, "FCM token refreshed")
Log.d(TAG, "New token length: ${token.length}")
Log.d(TAG, "NEW FCM TOKEN: $token")
// Update cached token for getDeviceToken() callback
PushNotificationService.updateCachedToken(token)
Log.d(TAG, "Token cache updated - will be available for next SDK callback")
}
/**
* Called when a push notification message is received
*
* This is called when the app is in the foreground and a notification arrives.
* When the app is in the background, the system handles the notification automatically.
*
* @param remoteMessage The message received from Firebase
*/
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
Log.d(TAG, "Push notification received from: ${remoteMessage.from}")
// Handle notification data payload
if (remoteMessage.data.isNotEmpty()) {
Log.d(TAG, "Message data payload: ${remoteMessage.data}")
// TODO: Handle notification data if needed
}
// Handle notification message
remoteMessage.notification?.let {
Log.d(TAG, "Message Notification Title: ${it.title}")
Log.d(TAG, "Message Notification Body: ${it.body}")
// TODO: Display notification or handle as needed
}
}
}
Here's how automatic token refresh works:
┌─────────────────────────────────────────────────────────┐
│ 1. Firebase detects token refresh needed │
│ - Token expiry, reinstall, clear data, etc. │
└─────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. MyFirebaseMessagingService.onNewToken(token) │
│ - Called by Firebase system automatically │
└─────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. PushNotificationService.updateCachedToken(token) │
│ - Update @Volatile cachedFcmToken variable │
└─────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. Next SDK callback retrieves refreshed token │
│ - RDNACallbackManager.getDeviceToken() returns new │
└─────────────────────────────────────────────────────────┘
Now integrate the push notification service with your REL-ID SDK callback manager.
Add the getDeviceToken() callback implementation to your existing RDNACallbackManager:
// app/src/main/java/com/yourpackage/uniken/services/RDNACallbackManager.kt
class RDNACallbackManager(
private val context: Context,
private val currentActivity: Activity?
) : RDNA.RDNACallbacks {
companion object {
private const val TAG = "RDNACallbackManager"
}
// ... other callback implementations ...
/**
* Get device FCM token for push notifications
*
* This callback is invoked by the SDK to retrieve the device's FCM push notification token.
* The token should be generated and cached by PushNotificationService before SDK initialization.
*
* @return FCM device token string, or empty string if not available
*/
override fun getDeviceToken(): String {
val token = PushNotificationService.getCachedToken()
Log.d(TAG, "SDK requested device token, returning: ${if (token.isNotEmpty()) "token (${token.length} chars)" else "empty"}")
return token
}
}
Initialize FCM BEFORE SDK initialization in your MainActivity:
// app/src/main/java/com/yourpackage/MainActivity.kt
package com.relidcodelab
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.lifecycleScope
import com.relidcodelab.uniken.services.PushNotificationService
import com.relidcodelab.uniken.services.RDNACallbackManager
import com.relidcodelab.uniken.services.RDNAService
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
companion object {
private const val TAG = "MainActivity"
}
private lateinit var callbackManager: RDNACallbackManager
private val rdnaService = RDNAService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
Log.d(TAG, "onCreate - Initializing MainActivity")
// CRITICAL: Initialize FCM BEFORE SDK initialization
// This ensures token is cached and ready for getDeviceToken() callback
lifecycleScope.launch {
try {
PushNotificationService.initialize(applicationContext)
Log.d(TAG, "FCM initialized - token cached and ready for SDK")
} catch (error: Exception) {
Log.e(TAG, "Failed to initialize FCM", error)
// Don't crash app - token will be empty but app can still function
}
}
// Initialize RDNA SDK instance
rdnaService.getInstance(applicationContext)
// Create callback manager (includes getDeviceToken implementation)
callbackManager = RDNACallbackManager(
context = applicationContext,
currentActivity = this
)
setContent {
// Your Compose UI here
}
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy - Cleaning up")
PushNotificationService.cleanup()
}
}
Here's the complete flow from app startup to token registration:
┌─────────────────────────────────────────────────────────┐
│ 1. App Startup (MainActivity.onCreate) │
│ - enableEdgeToEdge() │
│ - lifecycleScope.launch { ... } │
└─────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. PushNotificationService.initialize(context) │
│ - Check POST_NOTIFICATIONS permission │
│ - FirebaseMessaging.getInstance().token.await() │
│ - Cache token in @Volatile cachedFcmToken │
└─────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. RDNAService.getInstance(context) │
│ - Initialize RDNA instance (singleton) │
└─────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. RDNACallbackManager(context, activity) │
│ - Implements RDNA.RDNACallbacks interface │
│ - Includes getDeviceToken() implementation │
└─────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 5. RDNAService.initialize(context, callbackManager) │
│ - Call rdna.Initialize(..., callbacks, ...) │
│ - SDK invokes: callbacks.getDeviceToken() │
└─────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 6. RDNACallbackManager.getDeviceToken() │
│ - Get from PushNotificationService.getCachedToken() │
│ - Return cached FCM token to SDK │
└─────────────────────┬───────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 7. SDK registers token with backend │
│ - Token stored for push notification delivery │
│ - SDK continues initialization (async events) │
└─────────────────────────────────────────────────────────┘
Let's thoroughly test your push notification implementation with comprehensive scenarios to ensure production readiness.
Prerequisites:
google-services.jsonTest Steps:
./gradlew installDebug
adb logcat | grep -E "(PushNotificationService|MyFCMService|RDNACallbackManager)"
Expected log sequence:✅ MainActivity: onCreate - Initializing MainActivity
✅ PushNotificationService: Starting FCM initialization for Android 33
✅ PushNotificationService: POST_NOTIFICATIONS permission: granted
✅ PushNotificationService: FCM token generated and cached, length: 163
✅ PushNotificationService: FCM TOKEN: [token string]
✅ MainActivity: FCM initialized - token cached and ready for SDK
✅ RDNACallbackManager: SDK requested device token, returning: token (163 chars)
adb shell pm clear com.relidcodelab
./gradlew installDebug
Look for refresh logs:✅ MyFCMService: FCM token refreshed
✅ MyFCMService: NEW FCM TOKEN: [token string]
✅ PushNotificationService: Token cache updated
Expected Results:
Android 13+ Permission Test:
adb shell pm revoke com.relidcodelab android.permission.POST_NOTIFICATIONS
Then restart app and verify graceful handlingExpected Permission Flow:
📱 POST_NOTIFICATIONS permission request → User grants → FCM initialization
📱 POST_NOTIFICATIONS permission request → User denies → Graceful fallback
Test Missing google-services.json:
google-services.json to google-services.json.backup./gradlew clean buildTest Google Services Plugin:
# Verify Google Services plugin processing
cd android && ./gradlew app:dependencies | grep google-services
Before deploying to production, verify:
google-services.json from production projectHere are solutions to common issues you might encounter.
"No Firebase app found"
google-services.json not properly configured or plugin not appliedgoogle-services.json is in app/ directory and id("com.google.gms.google-services") is in app/build.gradle.kts"FirebaseMessaging: Failed to get token"
"POST_NOTIFICATIONS permission denied"
"SDK getDeviceToken callback returns empty string"
PushNotificationService.initialize() completes BEFORE calling RDNAService.initialize()"Token not cached when SDK calls callback"
lifecycleScope.launch with proper await/suspend patterns, ensure initialization order"Build fails with Google Services plugin error"
"ClassNotFoundException: FirebaseMessaging"
Gradle sync fails
./gradlew clean --refresh-dependenciesFollow these best practices for production-ready push notification implementation.
Token Security:
Permission Handling:
Code Obfuscation:
// Example ProGuard rules for Firebase
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.google.firebase.** { *; }
Singleton Pattern:
object declaration for PushNotificationServiceonDestroy()Coroutine Scope:
lifecycleScope for lifecycle-aware coroutinesToken Caching:
@Volatile for thread-safe cached tokenCoroutines for Async Operations:
suspend fun for Firebase token retrievalDispatchers.Main for UI updatesDispatchers.IO for network/disk operationslifecycleScope.launch {
// Runs on Main dispatcher by default
PushNotificationService.initialize(context)
}
Initialization Optimization:
Logging Strategy:
Handle Configuration Changes:
Background Token Refresh:
onNewToken() callbackApp Restart Scenarios:
Environment Configuration:
google-services.json files per environmentMonitoring:
Testing:
Congratulations! You've successfully implemented secure push notification integration with the REL-ID SDK. Here's your complete implementation overview.
✅ Secure Device Registration - FCM tokens registered with REL-ID backend for two-channel security ✅ Firebase Integration - Complete FCM setup with Google Services plugin and token management ✅ Production-Ready Service - Singleton architecture with error handling and token refresh ✅ Android Configuration - Google Services plugin, permissions, and notification handling ✅ SDK Callback Integration - getDeviceToken() callback implementation with cached tokens
app/src/main/java/com/yourpackage/
├── uniken/services/
│ ├── PushNotificationService.kt ✅ FCM token manager singleton
│ ├── MyFirebaseMessagingService.kt ✅ Token refresh handler
│ ├── RDNACallbackManager.kt ✅ Enhanced with getDeviceToken()
│ └── RDNAService.kt ✅ SDK wrapper (existing)
│
├── MainActivity.kt ✅ FCM initialization
app/
├── build.gradle.kts ✅ Firebase dependencies & Google Services plugin
├── google-services.json ✅ Firebase configuration
└── src/main/
└── AndroidManifest.xml ✅ FCM service & POST_NOTIFICATIONS permission
build.gradle.kts ✅ Project-level Google Services plugin
Your implementation demonstrates enterprise-grade patterns:
Component | Pattern | Benefit |
PushNotificationService | Singleton Object | Centralized token management |
MyFirebaseMessagingService | Service Extension | Automatic token refresh |
RDNACallbackManager | Callback Implementation | Clean SDK integration |
Firebase Configuration | Google Services Plugin | Auto-configuration |
Error Handling | Graceful Degradation | Production reliability |
Your REL-ID push notification integration now enables:
🎉 Congratulations!
You've successfully implemented secure push notification capabilities that integrate seamlessly with the REL-ID security ecosystem. Your app can now participate in secure, two-channel communications for transaction approvals, authentication challenges, and security notifications.
Your users now have a more secure, responsive authentication experience with the power of REL-ID's push notification infrastructure!