Blog Infos
Author
Published

Most developers trust the Firebase SDK to ‘just work,’ but beneath the surface lies a complex web of system broadcasts, app signatures, and Google Play Services magic — this is the story your SDK doesn’t tell.
– Unknown Internals Enthusiast

It all started with a comment from Afnan on one of my earlier posts. He asked how FCM actually delivers messages under the hood — through broadcasts, exported receivers, and some kind of unique app signature.

Public comment from Medium user on my previous post

That one comment pulled a thread I couldn’t ignore.

I started tracing how Firebase, Google Play Services, and Android actually work together to deliver a push notification — and it led somewhere way deeper than I expected.

Firebase Cloud Messaging (FCM) is the go-to service for sending push notifications on Android. While the SDK makes it look easy, the actual journey of a message — from Firebase servers to your app — is full of clever mechanisms and deeply integrated Android internals.

Let’s unpack what really happens under the hood when your app receives a push message, and how Google ensures only your app gets it securely.

Firebase SDK Doesn’t Talk to the Cloud Directly

When your app requests an FCM token using:

FirebaseMessaging.getInstance().token
    .addOnCompleteListener { task ->
        if (!task.isSuccessful) {
            Log.w("FCM", "Fetching FCM registration token failed", task.exception)
            return@addOnCompleteListener
        }

        // Get the FCM token
        val token = task.result
        Log.d("FCM", "FCM Token: $token")
        
    }

you might assume it’s contacting Firebase servers directly.

But in reality, it talks to a system-level serviceGoogle Play Services, not Firebase.

Here’s what really happens:

  1. The call is routed internally to FirebaseMessaging.requestToken().
  2. That uses Android’s Binder IPC to send a request to:

 

com.google.android.gms.cloudmessaging.CloudMessagingService

 

This is a background service within the Play Services package (com.google.android.gms) that manages communication with Firebase’s cloud.

How FCM Knows Which App to Deliver the Message To

Each Android app that registers for FCM provides:

  1. Its package name
  2. Firebase App ID from google-services.json
  3. SHA-1 app signing certificate

Firebase then maps your instance to a unique internal AppId.

Even if someone tries to spoof your app’s package name, only the app with the correct signing key gets the message. This is verified by Google Play Services, not by your app. So, your app’s identity is cryptographically tied to its signature + package name combo. Messages won’t be delivered unless everything matches.

No, It’s Not a BroadcastReceiver Anymore

Old FCM versions (pre-Lollipop) used BroadcastReceivers to deliver push messages. Today, this mechanism has evolved for better security and battery performance.

Instead, Google Play Services directly starts a service in your app:

Intent("com.google.firebase.MESSAGING_EVENT")
 .setPackage("your.package.name")

This Intent is routed to your implementation of:

FirebaseMessagingService

You register this service in your manifest like so:

<service
    android:name=".MyFirebaseMessagingService"
    android:exported="true"
    android:permission="com.google.android.c2dm.permission.SEND">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

This service is protected with a signature-level permission. Only Google-signed components, like Play Services, can start it.

However, Play Services may use internal broadcasts between its components, such as:

  • Wakeful Intents — Special broadcast intents that ensure the device stays awake while a background task is being handled.
  • Wake Locks — Low-level power management tools that apps use to keep the CPU or screen awake during critical operations.
  • Background Receiver Hooks — BroadcastReceiver mechanisms that let apps respond to system or app-level events even when not in the foreground.

But from your app’s perspective, you only need to handle the service-based delivery — no need for a BroadcastReceiver.

Decompiling FCM Code

I decompiled the SDK to peek into its internals. Here’s how you can do it yourself:

1. Download the .aar from Maven Central

Head over to Maven Repository and download the version you want (I used 24.1.2).

2. Extract the classes.jar

A .aar file is just a ZIP archive. You can unzip it, or rename it to .zip and extract it. Inside, you’ll find a classes.jar — this is where the compiled bytecode lives.

3. Install JADX (GUI or CLI)

To decompile the bytecode into readable Java code, I used JADX:

brew install jadx  # for macOS

4. Open the classes.jar in JADX GUI

Launch jadx gui, drag and drop the classes.jar file, and you’ll be able to browse through decompiled code.

Look out for classes like:

1. FirebaseMessagingService
2. FirebaseInstanceIdReceiver
3. Internal handlers and broadcast receivers

Reverse Engineered: How FCM Messages Are Handled Internally

Decompiled from Firebase Messaging SDK (24.1.2) — internal delivery flow visualized. Flowchart created by author using Mermaid

Explanation Step by Step:

  1. Android OS: This is the base operating system of your phone. It passes along system events and messages to Google Play Services.

2. Google Play Services (CloudMessagingReceiver)

This is like a central post office (mailman) built into Android. It:

  1. Receives push messages directly from FCM servers.
  2. Identifies which app the message is for.
  3. Sends the message (via Intent) to your app.

A BroadcastReceiver in Google Play Services catches FCM messages. It sends an Intent to your app. Your app receives this in FirebaseMessagingService

intent.setAction("com.google.android.c2dm.intent.RECEIVE");
intent.setPackage("your.app.package");
context.sendOrderedBroadcast(intent, permission);

You never write this part yourself; it’s prebuilt by Google.

Play Services uses a combination of:

  1. Bound services (IGmsServiceBroker): A bound service interface that lets apps connect to Google Play Services to request functionality like messaging or location.
  2. Messenger IPC (MessengerCompat): A wrapper over Android’s Messenger class used for sending messages between processes using Handler and Mesage objects.
  3. BinderProxy communication: A low-level IPC bridge that represents a remote object, enabling direct communication with system services across process boundaries.

All of these are part of Android’s low-level IPC system that allows processes to communicate securely.

3. FirebaseMessagingService: Intent arrives in handleIntent() method in FirebaseMessagingService inherited from EnchantedIntentService.

Screenshot taken from the author’s system using jadx gui

4. Enchanted Intent Service: It is an internal abstract service in Firebase used to handle background work in a clean, lifecycle-aware, and async-safe way. It runs Intent handling off the main thread using an Executor Service. Method processIntent runs intent on background thread and handleIntent is an Abstract method to override on subclasses.

5. passMessageIntentToSdk: This is where the Firebase SDK internally processes the incoming intent. This method acts as the dispatcher — it reads the message contents and routes it based on what type of message it is.

Screenshot taken from the author’s system using jadx gui

6. Message Type: Once inside the SDK, it evaluates the message type.

1. gcm: A standard data or notification message
2. deleted_messages: The device missed messages (e.g. app inactive or offline)
3. send_event: A confirmation that a message sent from this device was delivered
4. send_error: An error occurred while sending a message from this device

7. gcm: If the message type is gcm, it could contain two types of payloads. Firebase will automatically decide what to do based on the payload structure.

8. Is It a Notification Payload?

Firebase checks if the message contains a “notification” key:

{
  "notification": {
    "title": "Hello",
    "body": "You are reading medium blog"
  },
  "data": {
    "key": "value"
  }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

There are two possible flows:

1. Notification Payload:

  1. Firebase will automatically display the notification in the system tray.
  2. You don’t need to write any code unless you want to customize the behavior.

2. Only Data Payload:

  1. Firebase will not show anything by default.
  2. Instead, it calls your onMessageReceived() method — you’re responsible for handling the message.

9. showNotification: If it’s a notification payload, Firebase handles everything. No need for custom notification code. Great for quick and simple use cases. onMessageReceived() is your custom logic.

This is where you come in. You override this method in your FirebaseMessagingService to respond to data messages.

Firebase will call this when:

  1. The message contains only data.
  2. The message has both notification and data, but your app is in the foreground.
  3. The message type is one of:
    deleted_messages
    send_event
    send_error

If your app is in the background and the message contains a notification payload, onMessageReceived() is not called — Firebase shows the notification directly instead.

FCM’s Journey: Doze Mode, Retries, TTL & More — All in One Flow

Flowchart created by author using Mermaid

This flowchart illustrates the end-to-end delivery flow of an FCM message, covering key behaviors like Doze mode handlingnetwork retry with TTL, and message replacement using collapse keys. Messages from the FCM server first reach Google Play Services, which checks network availability and Doze status. If the device is offline, the message is queued and retried using exponential backoff, as long as it’s within the specified TTL window. For devices in Doze mode, only high-priority messages are delivered immediately, while others are deferred. Once eligible for delivery, messages may replace older ones if a collapse_key is provided. Finally, depending on the payload type, Firebase either auto-displays the notification or routes it to your app’s onMessageReceived() for custom handling.

FCM Delivery in Doze Mode, Retry, Collapse Key & TTL

Doze Mode Behavior: FCM bypasses Doze using Google Play Services. Only high-priority messages are delivered immediately.

FCM Retry Mechanism: If the device is offline or unreachable: FCM queues the message. Retries are done using exponential backoff. The retry window is bounded by TTL.

No manual retry from client side for downstream messages.

Collapse Key: It prevents spamming multiple updates when only the latest matters. New messages with the same collapse_key will replace older ones. Max 1 pending message per collapse_key.

TTL (Time-To-Live): Defines how long FCM will keep trying if the device is offline. Default: 4 weeks. Set to 0 for “send only if online”.

All the above features — Doze handling, retries, collapse keys, and TTL — are managed by the FCM server based on what your backend sends. So no, your Android app isn’t doing any of this magic — it’s just waiting patiently for messages like a good listener. 😄📱

🙌 Thanks for Reading!

If you enjoyed this post or learned something new, I’d love your support:

👍 Clap (up to 50 times!) to help more devs discover it
🔁 Share it with your community
👀 Follow me for more deep dives, breakdowns, and behind-the-scenes insights

☕ Support & Connect

💬 Got questions or want to go deeper?
Book a 1:1 session with me on Topmate

❤️ Like what I write? You can now buy me a coffee to keep the research and writing flowing

Until next time — stay curious, keep building, and
Happy coding! 💻✨

This article was previously published on proandroiddev.com.

Menu