How to Implement Push Notifications in Android using Firebase (FCM) — Complete Production Guide
Push notifications are the primary retention artery for modern mobile applications. Whether alerting users to transaction confirmations, breaking software updates, or engagement reminders, a robust notification pipeline is crucial. In this comprehensive engineering guide, we walk through building a production-grade notification infrastructure in Android using Firebase Cloud Messaging (FCM) and the modern HTTP v1 API.

1. Architectural Overview of Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) acts as a highly distributed message broker between your custom backend server and target Android devices. In 2024 and 2026, Google officially deprecated the legacy FCM HTTP server protocols in favor of the secure FCM HTTP v1 API, which uses short-lived OAuth 2.0 access tokens and strictly scoped service accounts.

Architecture Flow
[App Client] ──(Generates Device Token)──> [FCM Gateway]
     │                                           ā–²
     ā”œā”€ā”€ā”€(Syncs Token to MySQL via REST)──┐      │
     ā–¼                                    ā–¼      │
[Android Device] <──(Delivers Notification)── [App Server / PHP]

2. Android 13+ (API Level 33) Runtime Permissions

Starting with Android 13 (Tiramisu), the POST_NOTIFICATIONS runtime permission is mandatory. Without explicitly requesting and receiving this permission from the user, notifications will be silently dropped by the operating system.

First, declare the permission in your AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="in.jmdworld.pushdemo">

    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name">

        <service
            android:name=".services.AppFirebaseMessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
    </application>
</manifest>

In your MainActivity.kt, dynamically verify and request the permission before initializing token listeners:

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat

class MainActivity : AppCompatActivity() {

    private val requestNotificationPermission =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                // Permission granted: Register FCM token
                fetchAndSyncFcmToken()
            } else {
                // Handle denial gracefully (e.g., explain in settings)
                showPermissionRationaleDialog()
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        checkNotificationPermission()
    }

    private fun checkNotificationPermission() {
        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)
            } else {
                fetchAndSyncFcmToken()
            }
        } else {
            fetchAndSyncFcmToken()
        }
    }
}

3. Implementing the Custom FirebaseMessagingService

Create a dedicated service class that inherits from FirebaseMessagingService. This class intercepts two critical lifecycle events: onNewToken() and onMessageReceived().

package in.jmdworld.pushdemo.services

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import in.jmdworld.pushdemo.MainActivity
import in.jmdworld.pushdemo.R

class AppFirebaseMessagingService : FirebaseMessagingService() {

    override fun onNewToken(token: String) {
        super.onNewToken(token)
        // Send updated token to your PHP backend database
        syncTokenWithServer(token)
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)

        // Extract title and body from notification payload or custom data map
        val title = remoteMessage.notification?.title 
            ?: remoteMessage.data["title"] 
            ?: "New JMD WORLD Update"
        val message = remoteMessage.notification?.body 
            ?: remoteMessage.data["message"] 
            ?: "Check out our latest release!"

        showLocalNotification(title, message)
    }

    private fun showLocalNotification(title: String, message: String) {
        val channelId = "jmd_engineering_updates"
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // Create Notification Channel for Android 8.0 (API 26) and higher
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                channelId,
                "Engineering Updates",
                NotificationManager.IMPORTANCE_HIGH
            ).apply {
                description = "Critical software releases and push alerts"
                enableLights(true)
                enableVibration(true)
            }
            notificationManager.createNotificationChannel(channel)
        }

        val intent = Intent(this, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
        }

        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent,
            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
        )

        val builder = NotificationCompat.Builder(this, channelId)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(message)
            .setAutoCancel(true)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent)

        notificationManager.notify(System.currentTimeMillis().toInt(), builder.build())
    }

    private fun syncTokenWithServer(token: String) {
        // Implement Retrofit or OkHttp call to https://yourdomain.com/api/register-fcm.php
    }
}

4. Backend Dispatch: Sending Push Notifications with PHP & FCM HTTP v1

To communicate with FCM v1, generate a Google Service Account Private Key JSON from the Firebase Console (Project Settings āž” Service accounts āž” Generate new private key). Here is a robust PHP function utilizing OAuth 2.0 JWT assertion to deliver push notifications without external heavy SDKs:

<?php
/**
 * JMD WORLD - FCM v1 Push Notification Sender in Pure PHP
 */

function getGoogleAccessToken($serviceAccountPath) {
    $sa = json_decode(file_get_contents($serviceAccountPath), true);
    $now = time();
    $jwtHeader = base64_encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));
    $jwtClaim = base64_encode(json_encode([
        'iss' => $sa['client_email'],
        'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
        'aud' => 'https://oauth2.googleapis.com/token',
        'exp' => $now + 3600,
        'iat' => $now
    ]));

    $rawSign = "$jwtHeader.$jwtClaim";
    openssl_sign($rawSign, $signature, $sa['private_key'], OPENSSL_ALGO_SHA256);
    $jwt = $rawSign . '.' . base64_encode($signature);

    $ch = curl_init('https://oauth2.googleapis.com/token');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
        'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        'assertion' => $jwt
    ]));
    $res = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $res['access_token'] ?? null;
}

function sendFcmV1Notification($serviceAccountPath, $projectId, $deviceToken, $title, $body) {
    $accessToken = getGoogleAccessToken($serviceAccountPath);
    if (!$accessToken) return ['error' => 'Failed to obtain OAuth access token'];

    $url = "https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send";
    $payload = [
        'message' => [
            'token' => $deviceToken,
            'notification' => [
                'title' => $title,
                'body' => $body
            ],
            'data' => [
                'click_action' => 'OPEN_ARTICLE',
                'timestamp' => (string)time()
            ]
        ]
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $accessToken,
        'Content-Type: application/json'
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return ['status' => $httpCode, 'response' => json_decode($response, true)];
}
?>

5. Troubleshooting Common Production Pitfalls

  • Notification vs Data Messages: If your payload includes a notification object, Android OS handles delivery directly when the app is in the background and does NOT invoke onMessageReceived(). If you need custom intent processing in the background, send a pure data payload.
  • Aggressive OEM Battery Optimization: Xiaomi (MIUI), Huawei (EMUI), and Vivo kill background services aggressively. Advise users in your settings UI to whitelist the application in Auto-start and Battery Saver settings.
  • Missing Notification Channels: On Android 8.0+, attempting to post a notification without a valid channelId causes an immediate crash or silent rejection.

Conclusion

By pairing FCM HTTP v1 with strict permission workflows and modular Kotlin background services, your Android applications will deliver reliable, sub-second push notifications across all devices and Android versions. For production apps looking for zero-downtime messaging pipelines, test your payloads against both warm and cold app states.

Share this Guide Help colleagues and developers learn from this article
In-Article Sponsored Content
Mohit Kumaar
AUTHOR & FOUNDER

Mohit Kumaar

Founder & CEO of JMD WORLD with 8+ Years of industrial software engineering leadership (active since 2018). Creator and manager of 500+ production Google Play applications (proprietary & client solutions) and architect of apps.jmdworld.in. Operating from Pune & Mumbai under MSME Registration: UDYAM-MH-26-1071218 and D-U-N-SĀ® 581707246.

Previous Guide

Kotlin Coroutines and StateFlow Explained for Android Developers

Android Development
Next Guide

How to Monetize Android Apps in 2026: AdMob, In-App Purchases & Subscriptions

App Monetization
Recommended For You Ads by Google

Comments (0)

No comments yet. Share your thoughts below!

Leave a Comment

Share your thoughts or questions. Your email address remains private.