Send push notifications on Android
In this tutorial, we make a PubNub message arrive on an Android device as a system notification. We get a Firebase Cloud Messaging (FCM) registration token from the device, then register that token on one PubNub channel with Mobile Push Notifications. We publish a message carrying an FCM push payload to the same channel, and watch the notification appear in the status bar. Every step needs a device or emulator that has Google Play services, because FCM won't issue a registration token without it.
Before you begin
You need:
- An Android Studio project with the PubNub Kotlin or Java SDK already added. If you haven't added it yet, follow Quickstart first, in Kotlin or Java, and come back here.
- A PubNub keyset. A keyset is the set of publish, subscribe, and secret keys that identifies your application to the PubNub network. You'll need its publish key and subscribe key. If you don't have a keyset yet, follow Set up your account to create one.
- A Firebase project. Create one in the Firebase console with Create a project, then add your Android app to it so Firebase knows its package name.
- An Android device, or an emulator image that includes Google Play services. Emulator images without the Play Store can't run FCM.
We'll use one channel throughout, called push-tutorial, and one device.
Give PubNub your Firebase credentials
PubNub sends the notification to FCM on your behalf, so it needs credentials for your Firebase project. Those credentials are a service-account private key file that you generate in Firebase and upload to your keyset. This is a one-time setup task per keyset.
First, create the service account and download its key:
- In the Firebase console, open your project.
- Click the settings icon next to Project Overview and select Project settings.
- Open the Service accounts tab.
- Under All service accounts, click the link that opens the Google Cloud Platform IAM page for the project. The default Firebase service account has near-admin privileges over the whole project, and PubNub only needs to send messages, so create a dedicated account instead.
- Create a new service account, and give it the Firebase Cloud Messaging API Admin role. Click Done.
- Back in the Firebase console Service accounts tab, open the new service account and click Generate new private key. Firebase downloads a JSON private key file. Keep it somewhere you can find it again.
Now hand that key to PubNub:
- Open the Admin Portal and select the app and keyset you'll use for this tutorial.
- Find the Mobile Push Notifications section and enable it.
- In the Firebase Cloud Messaging part of that section, use Private key file to upload the JSON file you just downloaded.
- Save the keyset.
Notice that this configuration lives on the keyset, not on a channel. Every channel on this keyset now pushes through this one Firebase project.
Connect the app to Firebase
Your app needs the Firebase configuration file and the FCM library.
- In the Firebase console, go to Project settings > Your apps, select your Android app, and download
google-services.json. - In Android Studio, switch the Project window to the Project view, and drop
google-services.jsoninto your app module's root directory, next to the module'sbuild.gradle.ktsfile. - Add the
google-servicesGradle plugin and the Firebase Cloud Messaging dependency. The Gradle mechanics aren't PubNub-specific, so follow Firebase's own Add Firebase to your Android project and add thefirebase-messaginglibrary. - Sync the project.
If the sync fails complaining that google-services.json is missing, the file is in the wrong directory. It belongs in the app module, not the project root.
Get the device's FCM registration token
FCM identifies a single installation of your app on a single device by a registration token. That token is what you register with PubNub, so this is the first thing your app needs.
Declare two things in AndroidManifest.xml: permission to post notifications, which Android 13 and later require, and the service that receives token updates.
1<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
3 <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
4
5 <application>
6
7 <service
8 android:name=".MyFirebaseMessagingService"
9 android:exported="false">
10 <intent-filter>
11 <action android:name="com.google.firebase.MESSAGING_EVENT" />
12 </intent-filter>
13 </service>
14
15 </application>
show all 16 linesNow ask for the token in your activity. We do this in onCreate so it runs every time the app starts, and we ask for the notification permission in the same place. The code blocks in this tutorial show class members rather than whole files, so let Android Studio add the missing imports as you paste them. The complete files are at the end of this page.
- Java
- Kotlin
1public class MainActivity extends AppCompatActivity {
2 private static final String TAG = "PushTutorial";
3
4 @Override
5 protected void onCreate(Bundle savedInstanceState) {
6 super.onCreate(savedInstanceState);
7
8 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
9 ActivityCompat.requestPermissions(
10 this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, 1);
11 }
12
13 FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {
14 if (!task.isSuccessful()) {
15 Log.w(TAG, "Fetching FCM registration token failed", task.getException());
show all 22 lines1class MainActivity : AppCompatActivity() {
2 private val tag = "PushTutorial"
3
4 override fun onCreate(savedInstanceState: Bundle?) {
5 super.onCreate(savedInstanceState)
6
7 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
8 ActivityCompat.requestPermissions(
9 this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1
10 )
11 }
12
13 FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
14 if (!task.isSuccessful) {
15 Log.w(tag, "Fetching FCM registration token failed", task.exception)
show all 21 linesFCM can also replace a token later, for example after the user reinstalls or clears the app's data. When that happens, FCM calls onNewToken on the service you declared in the manifest. Create that class now:
- Java
- Kotlin
1public class MyFirebaseMessagingService extends FirebaseMessagingService {
2 private static final String TAG = "PushTutorial";
3
4 @Override
5 public void onNewToken(@NonNull String token) {
6 Log.d(TAG, "FCM issued a new registration token: " + token);
7 }
8}
1class MyFirebaseMessagingService : FirebaseMessagingService() {
2 private val tag = "PushTutorial"
3
4 override fun onNewToken(token: String) {
5 Log.d(tag, "FCM issued a new registration token: $token")
6 }
7}
A replaced token invalidates the registration you made with the old one. A production app registers the new token here the same way you're about to register the first one, and removes the old registration. See Available SDKs for the mobile push API reference for your platform.
Run the app now. In Logcat, filter on PushTutorial, and you should see a line like:
FCM registration token: dK3f9...:APA91bH...
That long string is the token. If you see Fetching FCM registration token failed instead, the device has no Google Play services.
Register the token on a PubNub channel
PubNub keeps a list of device tokens per channel name. Registering the token on push-tutorial is what tells PubNub to forward pushes from that channel to this device. Registration is per channel, so one call covers one channel.
Create the PubNub client and register the token you just received:
- Java
- Kotlin
1public class MainActivity extends AppCompatActivity {
2 private static final String TAG = "PushTutorial";
3 private static final String CHANNEL = "push-tutorial";
4
5 private PubNub pubnub;
6
7 @Override
8 protected void onCreate(Bundle savedInstanceState) {
9 super.onCreate(savedInstanceState);
10
11 try {
12 PNConfiguration config = PNConfiguration
13 .builder(new UserId("push-tutorial-user"), "YOUR_SUBSCRIBE_KEY")
14 .publishKey("YOUR_PUBLISH_KEY")
15 .build();
show all 35 linesCall registerForPush(token) from the getToken() callback in step 3, right after the line that logs the token.
1class MainActivity : AppCompatActivity() {
2 private val tag = "PushTutorial"
3 private val channel = "push-tutorial"
4
5 private lateinit var pubnub: PubNub
6
7 override fun onCreate(savedInstanceState: Bundle?) {
8 super.onCreate(savedInstanceState)
9
10 val config = PNConfiguration
11 .builder(UserId("push-tutorial-user"), "YOUR_SUBSCRIBE_KEY").apply {
12 publishKey = "YOUR_PUBLISH_KEY"
13 }.build()
14
15 pubnub = PubNub.create(config)
show all 31 linesCall registerForPush(token) from the token callback in step 3, right after the line that logs the token.
Replace YOUR_PUBLISH_KEY and YOUR_SUBSCRIBE_KEY with the keys from the keyset you configured in step 1. Push works only for the keyset that holds the Firebase private key, so keys from a different keyset produce a registration that never delivers anything.
PNPushType.FCM is what selects Firebase. The deviceId is the FCM registration token, not the Android device ID.
Run the app again. Logcat should now show, after the token line:
Registered for push on push-tutorial
Publish a message with an FCM push payload
A publish becomes a push notification when its payload contains a pn_fcm key. PubNub's push gateway reads that key, looks up the tokens registered on the channel, and forwards the request to FCM. Anything else in the message travels over pub/sub as usual.
This is the payload we're going to send, on the wire:
1{
2 "pn_fcm": {
3 "notification": {
4 "title": "Chat Invitation",
5 "body": "John invited you to chat"
6 }
7 }
8}
The title and body are what the device displays. Keep the whole payload within the limit.
| Item | Limit |
|---|---|
| Push credentials per keyset | 1 APNs certificate and 1 FCM key |
| Push notification payload size | 2 KB for APNs, 4 KB for FCM |
The Kotlin and Java SDKs build that shape for you with PushPayloadHelper, so you don't assemble the JSON by hand. Add a method that builds the payload and publishes it:
- Java
- Kotlin
1 private void publishPushMessage() {
2 PushPayloadHelper.FCMPayloadV2.Notification notification =
3 new PushPayloadHelper.FCMPayloadV2.Notification();
4 notification.setTitle("Chat Invitation");
5 notification.setBody("John invited you to chat");
6
7 PushPayloadHelper.FCMPayloadV2 fcmPayload = new PushPayloadHelper.FCMPayloadV2();
8 fcmPayload.setNotification(notification);
9
10 PushPayloadHelper payloadHelper = new PushPayloadHelper();
11 payloadHelper.setFcmPayloadV2(fcmPayload);
12
13 Map<String, Object> payload = payloadHelper.build();
14
15 pubnub.channel(CHANNEL).publish(payload)
show all 19 lines1 private fun publishPushMessage() {
2 val fcmPayload = PushPayloadHelper.FCMPayloadV2().apply {
3 notification = PushPayloadHelper.FCMPayloadV2.Notification().apply {
4 title = "Chat Invitation"
5 body = "John invited you to chat"
6 }
7 }
8
9 val payloadHelper = PushPayloadHelper().apply {
10 fcmPayloadV2 = fcmPayload
11 }
12
13 val payload = payloadHelper.build()
14
15 pubnub.channel(channel).publish(payload).async { result ->
show all 20 linespayloadHelper.build() returns the map that matches the JSON above, with pn_fcm at the top level.
We need this publish to happen while the app is in the background. FCM only puts a notification in the status bar when the app isn't in the foreground, so call publishPushMessage() from onStop(), which Android runs as the activity leaves the screen:
- Java
- Kotlin
1 @Override
2 protected void onStop() {
3 super.onStop();
4 publishPushMessage();
5 }
1 override fun onStop() {
2 super.onStop()
3 publishPushMessage()
4 }
The device that publishes is also registered on push-tutorial, so it receives the push it just triggered. That's exactly what we want here. One device does everything, and you see the result.
See the notification
- Run the app on your physical device, or on an emulator image with Google Play services.
- Tap Allow when Android asks whether the app may send notifications.
- Watch Logcat and wait for
Registered for push on push-tutorial. Don't skip this. If you background the app before registration finishes, there's no token on the channel yet and nothing to push to. - Press the device's Home button.
Within a second or two, a notification appears in the status bar:
Chat Invitation
John invited you to chat
Pull down the notification shade to see it in full. Logcat also shows Published with push payload, which confirms the publish itself succeeded.
Try it again: open the app and press Home once more. The same notification arrives every time, because onStop() publishes again and the registration is still in place.
What happened
Your device and your keyset played different parts in the same round trip:
- Firebase gave the app a registration token that identifies this installation on this device.
- Your app sent that token to PubNub with
addPushNotificationsOnChannels, which associated the token with the channelpush-tutorial. - Your app published a message to
push-tutorialcontaining apn_fcmkey. - PubNub's push gateway spotted
pn_fcm, looked up the tokens registered onpush-tutorial, and found yours. - PubNub authenticated to FCM with the private key you uploaded in step 1 and forwarded the notification.
- FCM delivered it to your device, and because the app was in the background, Android displayed it in the status bar.
Notice that nothing subscribed to push-tutorial in this tutorial. Push doesn't need an open connection, and that's the point of it. The notification reaches a device whose app isn't running in the foreground. For the full model, including what happens when a device is both subscribed and push-registered, see Mobile push notifications.
Complete files
Both classes, with the imports they need.
- Java
- Kotlin
MainActivity.java:
1import android.Manifest;
2import android.os.Build;
3import android.os.Bundle;
4import android.util.Log;
5
6import androidx.appcompat.app.AppCompatActivity;
7import androidx.core.app.ActivityCompat;
8
9import com.google.firebase.messaging.FirebaseMessaging;
10import com.pubnub.api.PubNubException;
11import com.pubnub.api.UserId;
12import com.pubnub.api.enums.PNPushType;
13import com.pubnub.api.java.PubNub;
14import com.pubnub.api.java.v2.PNConfiguration;
15import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper;
show all 93 linesMyFirebaseMessagingService.java:
1import android.util.Log;
2
3import androidx.annotation.NonNull;
4
5import com.google.firebase.messaging.FirebaseMessagingService;
6
7public class MyFirebaseMessagingService extends FirebaseMessagingService {
8 private static final String TAG = "PushTutorial";
9
10 @Override
11 public void onNewToken(@NonNull String token) {
12 Log.d(TAG, "FCM issued a new registration token: " + token);
13 }
14}
MainActivity.kt:
1import android.Manifest
2import android.os.Build
3import android.os.Bundle
4import android.util.Log
5import androidx.appcompat.app.AppCompatActivity
6import androidx.core.app.ActivityCompat
7import com.google.firebase.messaging.FirebaseMessaging
8import com.pubnub.api.PubNub
9import com.pubnub.api.UserId
10import com.pubnub.api.enums.PNPushType
11import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper
12import com.pubnub.api.v2.PNConfiguration
13
14class MainActivity : AppCompatActivity() {
15 private val tag = "PushTutorial"
show all 84 linesMyFirebaseMessagingService.kt:
1import android.util.Log
2import com.google.firebase.messaging.FirebaseMessagingService
3
4class MyFirebaseMessagingService : FirebaseMessagingService() {
5 private val tag = "PushTutorial"
6
7 override fun onNewToken(token: String) {
8 Log.d(tag, "FCM issued a new registration token: $token")
9 }
10}
Troubleshooting
If no notification arrives:
- The device or emulator has no Google Play services. FCM depends on it. Logcat shows
Fetching FCM registration token failedand there's no token to register. Use a physical device, or an emulator image listed with the Play Store. - You backgrounded the app before the registration succeeded. Reopen the app, wait for
Registered for push on push-tutorialin Logcat, then press Home. To confirm which channels a token is currently registered on, see Check push device registration. - FCM replaced the token. If
onNewTokenlogged a token that differs from the one you registered, the old registration is stale and PubNub is pushing to a token FCM no longer routes. Register the new token. - The keys and the Firebase credentials don't match. The private key you uploaded in step 1 must come from the same Firebase project as the
google-services.jsonin your app. The publish and subscribe keys in your code must also belong to the keyset you uploaded it to. A mismatch produces a successful publish and no notification. - You denied the notification permission. On Android 13 and later, a denied
POST_NOTIFICATIONSpermission means the push arrives but nothing is displayed. Grant it in the app's system settings. - The app was in the foreground. FCM hands a notification to your app instead of the status bar when the app is in the foreground. Press Home first.
If the publish succeeds and you still see nothing, ask PubNub what FCM said. Debug push notification messages shows how to read the errors FCM returns to the push gateway.
Next steps
You've delivered a PubNub message to an Android device as a push notification. From here:
- Send push notifications on iOS. Do the same thing for an iOS device through APNs.
- Available SDKs. Find the mobile push API reference for your platform.
- Mobile push notifications. The conceptual model behind the push gateway, including duplicate delivery, self-notification, and what can't trigger a push.
- Check the FCM payload. Shape
pn_fcmfor the behavior you want once a plain title and body isn't enough. - Debug push notification messages. Read the
-pndebugchannel to see why a push failed. - Forward push errors and device removals to your endpoint. Send push errors and device-removal events to your endpoint.
- Check push device registration. Confirm which channels a device token is registered on.
- Test mobile push notifications externally. Send a push straight through FCM to rule PubNub out of a delivery problem.