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.
[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
notificationobject, Android OS handles delivery directly when the app is in the background and does NOT invokeonMessageReceived(). If you need custom intent processing in the background, send a puredatapayload. - 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
channelIdcauses 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.
Comments (0)
No comments yet. Share your thoughts below!
Leave a Comment
Share your thoughts or questions. Your email address remains private.