Push Notification
Push Notifications Guide
Complete guide to setting up push notifications for incoming calls in your Kasookoo Android SDK integration.
Overview
Push notifications are essential for receiving incoming calls when your app is in the background or closed. The Kasookoo SDK uses Firebase Cloud Messaging (FCM) to deliver push notifications, which then trigger ConnectionService to show the native Android telecom UI.
Flow Diagram
Caller's Device Kasookoo Backend Your Device
┌─────┐ ┌─────┐ ┌─────┐
│ App │──── start call ──────→│Server│ │ App │
└─────┘ └──┬──┘ └─────┘
│ ↑
│ trigger push │
▼ │
┌──────────┐ │
│ Firebase │ │
│ FCM │ │
└────┬─────┘ │
│ │
▼ │
┌──────────┐ │
│ Google │ │
│ Play │ │
│ Services │ │
└────┬─────┘ │
│ │
└─── delivered ─────────────┘
│
▼
┌──────────────────┐
│ ConnectionService │
│ shows native UI │
└──────────────────┘
Prerequisites
Before starting, ensure you have:
- ✅ Firebase project with Android app added
- ✅ google-services.json added to your project's
app/folder - ✅ Real Android device (Google Play Services required)
- ✅ SHA-1 fingerprint added to Firebase
See Installation Guide for setup details.
Setup: Standard Push Notifications
Step 1: Add Permissions
In AndroidManifest.xml:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />Step 2: Request Notification Permission (Android 13+)
For Android 13 (API 33) and above, runtime permission is required:
private val requestNotificationPermission = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Log.d("Permission", "✅ Notifications granted")
} else {
Log.d("Permission", "❌ Notifications denied")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}Step 3: Create FirebaseMessagingService
Create MyFirebaseMessagingService.kt:
package com.yourcompany.yourapp
import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.kasookoo.sdk.models.ParticipantType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MyFirebaseMessagingService : FirebaseMessagingService() {
/**
* Called when a new FCM token is generated.
* Forward this token to Kasookoo SDK for device registration.
*/
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d("FCM", "🔑 New token: $token")
// Forward to Kasookoo SDK
CoroutineScope(Dispatchers.IO).launch {
try {
val sdk = (application as MyApplication).sdk
sdk.notifications.registerDevice(
fcmToken = token,
userType = ParticipantType.CUSTOMER
)
Log.d("FCM", "✅ Token registered with Kasookoo")
} catch (e: Exception) {
Log.e("FCM", "❌ Token registration failed", e)
}
}
}
/**
* Called when a push notification is received.
* Forward incoming call data to SDK.
*/
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
Log.d("FCM", "📨 Message received from: ${remoteMessage.from}")
Log.d("FCM", "📦 Data: ${remoteMessage.data}")
// Check if it's a call notification
val type = remoteMessage.data["type"]
if (type == "incoming_call") {
// Forward to Kasookoo SDK for ConnectionService UI
CoroutineScope(Dispatchers.IO).launch {
try {
val sdk = (application as MyApplication).sdk
sdk.notifications.handlePush(payload = remoteMessage.data)
} catch (e: Exception) {
Log.e("FCM", "❌ Push handling failed", e)
}
}
} else if (remoteMessage.notification != null) {
// Standard notification - just log
Log.d("FCM", "Notification title: ${remoteMessage.notification?.title}")
Log.d("FCM", "Notification body: ${remoteMessage.notification?.body}")
}
}
}Step 4: Register Service in Manifest
Inside <application> tag:
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>Step 5: Get Current Token (Optional)
To get the current FCM token at runtime:
import com.google.firebase.messaging.FirebaseMessaging
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w("FCM", "Fetching token failed", task.exception)
return@addOnCompleteListener
}
val token = task.result
Log.d("FCM", "Current token: $token")
// Register with backend if needed
lifecycleScope.launch {
sdk.notifications.registerDevice(
fcmToken = token,
userType = ParticipantType.CUSTOMER
)
}
}Setup: ConnectionService for Incoming Calls
Android's ConnectionService provides native telecom-like UI for incoming calls.
Step 1: Add Permissions
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />Step 2: Register ConnectionService
SDK auto-registers, but if you need to declare manually in AndroidManifest.xml:
<service
android:name="com.kasookoo.sdk.telecom.KasookooConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>Step 3: Register Phone Account
In your Application class or main Activity:
import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import androidx.core.content.ContextCompat
fun registerPhoneAccount() {
val telecomManager = ContextCompat.getSystemService(this, TelecomManager::class.java)
?: return
val componentName = ComponentName(
this,
com.kasookoo.sdk.telecom.KasookooConnectionService::class.java
)
val accountHandle = PhoneAccountHandle(componentName, "KasookooSDK")
val phoneAccount = PhoneAccount.builder(accountHandle, "Kasookoo")
.setCapabilities(
PhoneAccount.CAPABILITY_SELF_MANAGED or
PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
)
.setShortDescription("Kasookoo voice calls")
.setSupportedUriSchemes(listOf("kasookoo"))
.build()
telecomManager.registerPhoneAccount(phoneAccount)
}Step 4: Configure ConnectionService
The SDK handles ConnectionService internally. To customize:
val connectionConfig = ConnectionServiceConfig(
appDisplayName = "Your App Name",
ringtoneUri = Uri.parse("android.resource://your.package/raw/ringtone"),
supportsVideo = false,
supportsHolding = true,
iconResId = R.drawable.ic_call
)
sdk.configure(connectionService = connectionConfig)Testing Push Notifications
Method 1: Firebase Console (Manual Test)
Test that push notifications reach your device without setting up backend trigger:
- Run your app on a real device
- Note the FCM token printed to Logcat
- Open Firebase Console
- Go to Engage → Messaging → Create your first campaign
- Select "Firebase Notification messages"
- Enter:
- Notification title: "Test Push"
- Notification body: "Hello from Firebase!"
- Click "Send test message"
- Paste your FCM token → Test
✅ Notification should appear on your device.
Method 2: ADB Direct Send
For programmatic testing:
# Save your token first
TOKEN="your_fcm_token_here"
SERVER_KEY="your_firebase_server_key"
# Send via curl
curl -X POST -H "Authorization: key=$SERVER_KEY" \
-H "Content-Type: application/json" \
-d "{
\"to\": \"$TOKEN\",
\"data\": {
\"type\": \"incoming_call\",
\"call_id\": \"test_call_123\",
\"room_name\": \"test_room\",
\"caller_name\": \"Test Caller\",
\"caller_email\": \"[email protected]\"
}
}" \
https://fcm.googleapis.com/fcm/sendMethod 3: Real Backend Test
Once everything is set up, your backend can trigger pushes:
curl -X POST https://your-backend.com/test-push \
-H "Content-Type: application/json" \
-d '{
"device_token": "YOUR_FCM_TOKEN",
"type": "incoming_call",
"caller_name": "Alice"
}'Push Payload Structure
The Kasookoo SDK expects this payload format for incoming calls:
{
"data": {
"type": "incoming_call",
"call_id": "call_abc123",
"room_name": "demo_room_xyz",
"caller_name": "Alice Smith",
"caller_email": "[email protected]",
"caller_phone": "+1234567890",
"caller_type": "agent",
"callee_email": "[email protected]",
"is_video": "false",
"is_call_recording": "false"
},
"android": {
"priority": "high",
"notification": {
"channel_id": "kasookoo_calls",
"sound": "default"
}
}
}Required Fields
type— Must be"incoming_call"for call notificationscall_id— Unique identifier for the callroom_name— LiveKit room to joincaller_name— Display name shown in ConnectionService UI
Optional Fields
caller_email,caller_phone— For caller identificationcaller_type— Role of the caller (customer,agent,driver,bot)is_video— Whether this is a video call (default:"false")is_call_recording— Whether call should be recorded
Note: Android FCM data payloads send all values as strings, even booleans.
Notification Channels (Android 8.0+)
For Android 8.0 (API 26) and above, you must create notification channels:
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val callsChannel = NotificationChannel(
"kasookoo_calls",
"Incoming Calls",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Channel for incoming voice and video calls"
enableLights(true)
enableVibration(true)
setSound(
Settings.System.DEFAULT_RINGTONE_URI,
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
)
}
val messagesChannel = NotificationChannel(
"kasookoo_messages",
"Messages",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Channel for incoming messages"
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(callsChannel)
notificationManager.createNotificationChannel(messagesChannel)
}
}Call createNotificationChannels() in your Application's onCreate().
Common Issues & Troubleshooting
Issue: No FCM token received
Causes:
- Firebase not configured properly
- Google Play Services not installed (emulator without Google APIs)
- Wrong
google-services.jsonfile
Solutions:
- Verify
google-services.jsonis inapp/folder - Verify
id("com.google.gms.google-services")plugin applied - Use emulator with Google APIs (Pixel images recommended)
- Test on real device
Issue: Token received but push not delivered
Causes:
- Server key incorrect
- Project ID mismatch
- Android battery optimization killing FCM
Solutions:
- Verify SHA-1 fingerprint matches in Firebase
- Check Firebase Console → Project Settings → Cloud Messaging
- Disable battery optimization for your app:
- Settings → Apps → Your App → Battery → Unrestricted
Issue: Push received but no ConnectionService UI
Causes:
- ConnectionService not registered
- Phone account not registered
- MANAGE_OWN_CALLS permission missing
- Payload format incorrect
Solutions:
- Verify permissions in Manifest
- Call
registerPhoneAccount()at app startup - Verify payload includes
"type": "incoming_call" - Check Logcat for ConnectionService errors
Issue: Push works in foreground but not background
Causes:
- App being killed by battery optimization
- Doze mode interfering
- Missing foreground service
Solutions:
- Use
priority: "high"in push payload - Request battery optimization exception:
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) intent.data = Uri.parse("package:$packageName") startActivity(intent) - SDK will start foreground service automatically for active calls
Issue: Notification permission denied on Android 13+
Cause: User denied POST_NOTIFICATIONS permission.
Solution:
- Re-request permission with explanation
- Show a Snackbar/Dialog explaining why notifications are needed
- Provide a button to open app settings:
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) startActivity(intent)
Best Practices
1. Initialize Firebase Early
Initialize in Application.onCreate(), not in Activities:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Firebase is auto-initialized via google-services plugin
// Just verify it's ready
FirebaseApp.initializeApp(this)
}
}2. Handle Token Refresh
FCM tokens can change. Always re-register:
override fun onNewToken(token: String) {
super.onNewToken(token)
// Save locally
getSharedPreferences("fcm", MODE_PRIVATE).edit {
putString("token", token)
}
// Register with backend
lifecycleScope.launch {
sdk.notifications.registerDevice(fcmToken = token, userType = ParticipantType.CUSTOMER)
}
}3. Use High Priority for Calls
For incoming call pushes, use priority: "high":
- Bypasses Doze mode
- Wakes up device faster
- Better user experience
4. Test Different App States
Test push delivery in:
- ✅ App in foreground
- ✅ App in background
- ✅ App killed (swiped from recents)
- ✅ Phone locked
- ✅ Doze mode (idle device)
- ✅ Battery saver mode
- ✅ Data saver mode
5. Handle Notification Tap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Check if launched from notification
intent.extras?.let { extras ->
val callId = extras.getString("call_id")
if (callId != null) {
// Navigate to call screen
}
}
}Privacy & Compliance
Battery Optimization Disclosure
If you request battery optimization exception, you must disclose this in Play Store listing:
"This app uses background services for voice calls. Battery optimization exemption ensures calls aren't missed."
GDPR Compliance
- Get explicit consent for push notifications
- Document what data is sent in push payloads
- Provide opt-out mechanism
- Honor users' deletion requests
Push Payload Security
⚠️ Push payloads can be intercepted at the OS level. Don't include:
- Passwords
- API keys
- Personal medical information
- Financial details
Use call IDs and reference data, not sensitive payload content.
Next Steps
- 📖 Receiving Calls Guide — Full incoming call flow
- 🎯 ConnectionService Best Practices — Customize incoming call UI
- 🔧 Troubleshooting — Common issues
Need Help?
Updated 15 days ago
