Android Mobile Push Notifications
PubNub Mobile Push Notifications gateway bridges native message publishing with third-party push notification services including Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM). The code below shows a sample implementation needed to register for remote notifications, receive the corresponding device token, register the device token to a PubNub channel, and publish a message.
Step 1: Configure Account Settings
Before you can use PubNub Mobile Push Notifications feature with Firebase Cloud Messaging, you must first configure your accounts. Follow these steps to configure both your Firebase Cloud Messaging and PubNub accounts for your application.
Step 1a: Download a Firebase Private Key File
Update your keyset configuration
If you use the legacy HTTP FCM to send Mobile Push Notifications, you must migrate to the new HTTP v1 FCM before June 2024. Refer to the migration guide for details.
Log into the Firebase console and click the Create a project button. Follow the project creation wizard.
Click the settings icon in the left top corner of your project overview page and go to Project settings.
Switch to the Service accounts tab, click the Generate new private key button, and download your FCM private key file.
Step 1b: Upload the Firebase Private Key File to Your Admin Portal
On the Admin Portal, go to the Mobile Push Notifications section on your app's keyset. Click the Upload Private Key File button in the Firebase Cloud Messaging section and upload the file downloaded from Firebase. Click the Save Changes button that pops up to save your new keyset configuration.
Migrate from legacy FCM APIs to HTTP v1
The Legacy FCM APIs that used an API Key have been deprecated by Google, and will be removed in June 2024. The Admin Portal will continue supporting the legacy FCM API Keys until Google removes those deprecated APIs entirely. Still, it is strongly recommended that you configure FCM HTTP v1 following the steps described in this section.
Step 2: Request Device Token
To add Cloud Messaging support to your application, open your Android Studio project and switch to the Project
view. Then, add the google-services.json
(downloaded during step 1) into your Android app module root directory.
Step 2a: Configure Gradle
Refer to the Google documentation for the steps to configure your Gradle files to use the google-services
Gradle plugin and add the dependencies for the Firebase products that you want to use.
Step 2b: Firebase Registration
The following code shows how to create a Firebase Installations ID from FirebaseMessaging
. This can be done anywhere inside the app, but for this example, it's in the onCreate
method of the AppCompatActivity
to ensure it's always executed.
Read Firebase Docs for more details.
- Java
- Kotlin
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferencesManager.init(getApplicationContext())
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(new OnCompleteListener<String>() {
@Override public void onComplete(@NonNull Task<String> task) {
if (!task.isSuccessful()) {
Log.w(TAG, "Fetching FCM registration token failed", task.getException());
return;
}
Log.d(TAG, getString(R.string.msg_token_fmt, task.getResult()));
}
});
}
show all 16 linesclass MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Firebase.messaging.getToken().addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
return@OnCompleteListener
}
Log.d(TAG, getString(R.string.msg_token_fmt, task.result))
})
}
}
Step 3: Receive & Monitor Device Token
To receive the Device Token (and updates to the token value) and mobile push notifications, you must create a custom class that extends FirebaseMessagingService
.
The onNewToken
callback fires whenever a new token is generated. After the initial FCM registration and device token has been delivered, new tokens will be delivered to this delegate method and your PubNub channel registrations will need to be updated.
- Java
- Kotlin
public class MyFirebaseMessagingService extends FirebaseMessagingService {
@Override public void onNewToken(String token) {
String oldToken = SharedPreferencesManager.readDeviceToken();
if (token.equals(oldToken)) { return; }
SharedPreferencesManager.write(token);
updatePushNotificationsOnChannels(SharedPreferencesManager.readChannelList(), token, oldToken);
}
}
override fun onNewToken(token: String) {
val oldToken = SharedPreferencesManager.getInstance(applicationContext).readDeviceToken()
if (token == oldToken) { return }
SharedPreferencesManager.getInstance(applicationContext).writeDeviceToken(token)
updatePushNotificationsOnChannels(
SharedPreferencesManager.getInstance(applicationContext).readChannelList(),
token,
oldToken
)
}
Step 3a: Cache Device Token & Registered channels
To ensure that an application can properly handle adding, updating, and removing registered push channels, there are two pieces of information that should be cached to avoid race-conditions: the Device Token and the list of registered channels. Not only will properly caching allow for easy access from anywhere inside the application, it will also prevent against race-conditions when multiple registration operations are queued at the same time.
SharedPreferences
provides basic persistent storage, but can be replaced by more sophisticated storage as your use-case requires. The following code will ensure the accessing of the cached information will be thread-safe regardless of storage choice.
A new Device Token can be provided from the system at anytime, and should be stored whenever it's received. The list of registered channels should also be cached in a similar manner as the Device Token, and should be updated whenever registered channels are added or removed. Set is used for the convenience of ensuring there are no duplicate channels.
- Java
- Kotlin
public class SharedPreferencesManager {
private static SharedPreferences sharedPref;
private SharedPreferencesManager() { }
public static void init(Context context) {
if(sharedPref == null)
sharedPref = context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
}
public static final String FCM_DEVICE_TOKEN = "PUBNUB_FCM_DEVICE_TOKEN";
public static @Nullable String readDeviceToken() {
return sharedPref.getString(FCM_DEVICE_TOKEN, null)
}
public static void writeDeviceToken(String value) {
SharedPreferences.Editor prefsEditor = sharedPref.edit();
show all 29 linesclass SharedPreferencesManager private constructor() {
companion object {
private val sharePref = SharedPreferencesManager()
private lateinit var sharedPreferences: SharedPreferences
private val PLACE_OBJ = "place_obj"
private val FCM_DEVICE_TOKEN = "PUBNUB_FCM_DEVICE_TOKEN"
private val FCM_CHANNEL_LIST = "PUBNUB_FCM_CHANNEL_LIST"
fun getInstance(context: Context): SharedPreferencesManager {
if (!::sharedPreferences.isInitialized) {
synchronized(SharedPreferencesManager::class.java) {
if (!::sharedPreferences.isInitialized) {
sharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
show all 28 linesStep 3b: Update Existing Registrations
A simple helper method can be created to consolidate the remove-then-add functionality when updating your existing registered channels.
- Java
- Kotlin
public void updatePushNotificationsOnChannels(String[] channels, String deviceToken, String oldToken) {
if (oldToken != null) {
pubnub.removeAllPushNotificationsFromDeviceWithPushToken()
.pushType(PNPushType.FCM)
.deviceId(oldToken)
.async(result -> { /* check result */ });
}
pubnub.addPushNotificationsOnChannels()
.pushType(PNPushType.FCM)
.deviceId(deviceToken)
.channels(channels)
.async(result -> { /* check result */ });
}
fun updatePushNotificationsOnChannels(channels: List<String>, deviceToken: String, oldToken: String?) {
oldToken?.also { token ->
pubnub.removeAllPushNotificationsFromDeviceWithPushToken(
pushType = PNPushType.FCM,
deviceId = token
).async { result, status -> }
}
pubnub.addPushNotificationsOnChannels(
pushType = PNPushType.FCM,
deviceId = deviceToken,
channels = channels
).async { result, status ->
// Handle Response
}
show all 16 linesStep 4: Manage Device Registrations
Once a Device Token is obtained, it can be registered with list of channels to allow Mobile Push Notifications to be send to the device. Channels can be dynamically added and removed based on the use cases of the application, and the current registrations for a Device Token can also be viewed.
Step 4a: Register New Channels
When adding channels, it's recommended to obtain the Device Token and List of Registered Channels from a cached source. After successfully registering channels, the newly registered channels should be added to the cached list.
- Java
- Kotlin
String cachedToken = SharedPreferencesManager.readDeviceToken();
pubnub.addPushNotificationsOnChannels()
.pushType(PNPushType.FCM)
.deviceId(cachedToken)
.channels(Arrays.asList("ch1", "ch2", "ch3"))
.async(result -> { /* check result */ });
SharedPreferencesManager.getInstance(applicationContext).readDeviceToken()?.also { deviceToken ->
pubnub.addPushNotificationsOnChannels(
pushType = PNPushType.FCM,
deviceId = deviceToken,
channels = listOf("ch1", "ch2", "ch3")
).async { result, status ->
// Handle Response
}
}
Step 4b: List Registered Channels
Once device registrations are added, you can confirm the APNs registrations for the device by listing all channels that the device is registered with. Since the list on the server is the source-of-truth, we will update our cached list to reflect the channels currently registered on the server.
- Java
- Kotlin
String cachedToken = SharedPreferencesManager.readDeviceToken();
pubnub.auditPushChannelProvisions()
.pushType(PNPushType.FCM)
.deviceId(cachedToken)
.async(result -> { /* check result */ });
SharedPreferencesManager.getInstance(applicationContext).readDeviceToken()?.also { deviceToken ->
pubnub.auditPushChannelProvisions(
pushType = PNPushType.FCM,
deviceId = deviceToken
).async { result, status ->
// Handle Response
}
}
Step 4c: Remove Existing Registrations
When removing channels it's recommended to obtain the Device Token and List of Registered Channels from a cached source. After removing registering channels, the channels that were removed should be also removed from the cached source.
- Java
- Kotlin
String cachedToken = SharedPreferencesManager.readDeviceToken();
pubnub.removePushNotificationsFromChannels()
.pushType(PNPushType.FCM)
.deviceId(cachedToken)
.channels(Arrays.asList("ch1", "ch2", "ch3"))
.async(result -> { /* check result */ });
SharedPreferencesManager.getInstance(applicationContext).readDeviceToken()?.also { deviceToken ->
pubnub.removePushNotificationsFromChannels(
pushType = PNPushType.FCM,
deviceId = deviceToken,
channels = listOf("ch1", "ch2", "ch3")
).async { result, status ->
// Handle Response
}
}
Step 5: Construct the Push Payload
To send a push notification, include the appropriate push notification payload for FCM when you publish a message and PubNub will appropriately parse the message.
Possible payload formatting issues
You can still format your payload data with the deprecated pn_gcm
object instead of the new pn_fcm
, and PubNub push server will attempt to remap the old format to the new one. The new spec is more restrictive in what is allowed, so it’s possible that the remapping process is insufficient (you can get FCM JSON payload
errors when debugging your push payload). In such cases, modify your payload to use pn_fcm
that is compliant with the FCM specification.
To avoid further issues after migrating to pn_fcm
, make sure the data
object accepts only data of string
type and that you don't include the topic
field in your payload (PubNub automatically sets the topic
field in the pn_fcm
payload).
For more information on moving from the legacy HTTP FCM to FCM HTTP v1, refer to the migration guide.
The structure of the pn_fcm
payload is as follows:
{
"pn_debug": true,
"pn_fcm": {
"notification": {
"body": "message notification text"
},
"android": {
"collapse_key": "group",
"data": {
"age": "10"
},
"ttl": "30s"
}
}
}
Field | Type | Required | Description |
---|---|---|---|
pn_debug | Boolean | No | PubNub debugging option. |
pn_fcm | Object | Yes | Container for Firebase Cloud Messaging (FCM) payload. The specification is compatible with FCM message payload structure to ensure PubNub processes the message correctly when delivering it to FCM. |
→ notification | Object | No | Basic notification template to use across all platforms. This part is platform-agnostic, but within the context of pn_fcm , it's being prepared for FCM parsing. It can take several optional parameters, like title , body , or image . Check official Firebase docs for the full list of available parameters. |
→ android | Object | No | Android-specific options for messages sent through FCM connection server. Specifies how messages should be handled on Android devices. It can take several optional parameters, like collapse_key , data , or ttl .Check official Firebase docs for the full list of available parameters. |
Topic field
The FCM push notification can target devices either through token
, topic
, or condition
. However, when using FCM through PubNub, don't specify them on your own, as PubNub already provides a mechanism to target devices through PubNub channels (and devices that are registered to those channels).
If you include a topic
field in your payload, FCM will throw an error when spotting more than one target
field.
FCM JSON payload error: Invalid value at 'message.data[0].value' (TYPE_STRING), 1234\nInvalid JSON payload received. Unknown name \"nick\" at 'message.data[1].value': Cannot find field.\nInvalid value at 'message' (oneof), oneof field 'target' is already set. Cannot set 'token'"
Step 5: Construct the Push Payload (deprecated)
To send a push notification, include the appropriate push notification payload for FCM when you publish a message and PubNub will appropriately parse the message.
The structure of the pn_gcm
payload is as follows:
Field | Type | Required | Description |
---|---|---|---|
notification | JSON | No | The dictionary sent to the Android device specify the type of interactions that you want the system to use when alerting the user. These messages are displayed automatically on behalf of the client app. Refer to the Firebase documentation for more information. |
data | JSON | No | Arbitrary key/value payload the client app is responsible for processing. Refer to the Firebase documentation for more information. |
pn_exceptions | [String] | No | A list of Device Tokens that should be excluded from receiving the notification. |
pn_collapse_id | String | No | An identifier to join multiple notifications into a single notification. You can only add the custom pn_collapse_id to data messages which are non-collapsible by default.It is not possible to set a user-specified collapse_id for notification messages payloads that have a notification key. These messages are always collapsible together using the package name. |
priority | enum | No | Determines the priority level for delivering a push notification to mobile device. Takes either normal or high value.Refer to the Firebase documentation. for more information. |
You may identify the target of the notification. For more information refer to Firebase Documentation.
You can also add the following options as part of the root payload (not nested under pn_gcm
), like so:
{
"pn_gcm": {
"notification": {
"title": "Chat Invitation",
"body": "John invited you to chat"
}
// or alternatively
//"data": {
// "title": "Chat Invitation",
// "body": "John invited you to chat"
//}
}
// other optional attributes
"pn_debug": true,
"pn_dry_run": true,
show all 17 linesField | Type | Required | Description |
---|---|---|---|
pn_debug | boolean | No | A flag that enables push debugging info to the pndebug channel. For more information, refer to Mobile Push Troubleshooting. |
pn_dry_run | boolean | No | A flag that allows developers to test a request without actually sending a message. |
pn_ttl | int | No | Time in seconds after which the notification expires. |
If you want to make sure that your push notifications are delivered despite the target device being in Doze mode you can use the priority
parameter, like so:
{
"pn_gcm": {
"notification": {
"title": "Chat Invitation",
"body": "John invited you to chat"
},
"priority": "high"
// other optional attributes
},
"pn_debug": true,
"pn_dry_run": true,
"pn_ttl": 60
}
- JSON
- Java
- Kotlin
Read Firebase Docs for more details.
{
"text": "John invited you to chat",
"pn_gcm": {
"notification": {
"title": "Chat Invitation",
"body": "John invited you to chat"
}
},
"pn_exceptions" : ["device-token1", "device-token2"]
}
}
PushPayloadHelper pushPayloadHelper = new PushPayloadHelper();
PushPayloadHelper.FCMPayload fcmPayload = new PushPayloadHelper.FCMPayload();
PushPayloadHelper.FCMPayload.Notification fcmNotification =
new PushPayloadHelper.FCMPayload.Notification()
.setTitle("Chat Invitation")
.setBody("John invited you to chat");
fcmPayload.setNotification(fcmNotification);
pushPayloadHelper.setFcmPayload(fcmPayload);
Map<String, Object> commonPayload = new HashMap<>();
commonPayload.put("text", "John invited you to chat");
pushPayloadHelper.setCommonPayload(commonPayload);
Map<String, Object> pushPayload = pushPayloadHelper.build();
val pushPayloadHelper = PushPayloadHelper()
val fcmPayload = FCMPayload().apply {
notification = FCMPayload.Notification().apply {
title = "Chat Invitation"
body = "John invited you to chat"
}
custom = mapOf(
"pn_exceptions" to arrayOf("device-token1", "device-token2")
)
}
pushPayloadHelper.fcmPayload = fcmPayload
pushPayloadHelper.commonPayload = mapOf(
"John invited you to chat" to "text"
show all 18 linesStep 6: Publish the Push Notification
Once the push payload is ready use the publish
method to publish the message on a channel. When PubNub finds the pn_fcm
payload, it will retrieve all device tokens that are associated for push on the target channel and forward a push notification request to the appropriate push service for those associated devices.
- Java
- Kotlin
- JavaScript
pubnub.publish()
.channel("ch1")
.message(pushPayload)
.async(result -> { /* check result */ });
pubnub.publish(
channel = "ch1",
message = pushPayload
).async { result ->
// Handle Response
}
//publish on channel
pubnub.publish(
{
channel: "ch1"
message: pushPayload
},
function (status, response) {
// Handle Response
}
);
For more information about push troubleshooting, refer to Mobile Push Troubleshooting.