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:

  1. Run your app on a real device
  2. Note the FCM token printed to Logcat
  3. Open Firebase Console
  4. Go to Engage → Messaging → Create your first campaign
  5. Select "Firebase Notification messages"
  6. Enter:
    • Notification title: "Test Push"
    • Notification body: "Hello from Firebase!"
  7. Click "Send test message"
  8. 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/send

Method 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 notifications
  • call_id — Unique identifier for the call
  • room_name — LiveKit room to join
  • caller_name — Display name shown in ConnectionService UI

Optional Fields

  • caller_email, caller_phone — For caller identification
  • caller_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.json file

Solutions:

  1. Verify google-services.json is in app/ folder
  2. Verify id("com.google.gms.google-services") plugin applied
  3. Use emulator with Google APIs (Pixel images recommended)
  4. Test on real device

Issue: Token received but push not delivered

Causes:

  • Server key incorrect
  • Project ID mismatch
  • Android battery optimization killing FCM

Solutions:

  1. Verify SHA-1 fingerprint matches in Firebase
  2. Check Firebase Console → Project Settings → Cloud Messaging
  3. 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:

  1. Verify permissions in Manifest
  2. Call registerPhoneAccount() at app startup
  3. Verify payload includes "type": "incoming_call"
  4. 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:

  1. Use priority: "high" in push payload
  2. Request battery optimization exception:
    val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
    intent.data = Uri.parse("package:$packageName")
    startActivity(intent)
  3. SDK will start foreground service automatically for active calls

Issue: Notification permission denied on Android 13+

Cause: User denied POST_NOTIFICATIONS permission.

Solution:

  1. Re-request permission with explanation
  2. Show a Snackbar/Dialog explaining why notifications are needed
  3. 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


Need Help?



Did this page help you?