Android Push Notifications
PubNub Push Notifications gateway bridges native message publishing with 3rd-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 Gateway 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: Create a Firebase Server Key
Log into your Firebase console console and create a new project.
Click Add Firebase to your Android app and follow the steps there.
Go to Project settings icon at the top left, next to Project overview. Click the Cloud messaging tab and find the Server key there.
Step 1b: Upload APNs Authentication Token to Your Admin Portal
On the Admin Portal, go to the Mobile Push Notifications section from the Keys page. Paste the Firebase Server Key into the field labeled FCM API Key, and click the Save Changes button that appears.
Step 2: Requesting 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
that was downloaded during step 1 into your Android app module root directory.
Step 2a: Configure Gradle
The Google Services plugin will load the google-services.json
file. To include this plugin add the following to the project-level build.gradle
file (
buildscript {
repositories {
// Check that you have the following line (if not, add it):
google() // Google's Maven repository
}
dependencies {
...
// Include this line
classpath 'com.google.gms:google-services:4.3.5'
}
}
allprojects {
...
repositories {
// Check that you have the following line (if not, add it):
google() // Google's Maven repository
...
}
}
App-level build.gradle
(
plugins {
id 'com.android.application'
// Include this line
id 'com.google.gms.google-services'
}
dependencies {
// Include this line
implementation platform('com.google.firebase:firebase-bom:26.5.0')
}
After detecting the changes made inside your Grande files, Android Studio should provide a prompt to Sync Now
to pull in the new plugins.
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.
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()));
}
});
}
}
class 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: Receiving & Monitoring Device Token
To receive the Device Token (and updates to the token value) and 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.
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: Caching 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. A Set is used for the convenience of ensuring there are no duplicate channels.
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();
prefsEditor.putString(FCM_DEVICE_TOKEN, value);
prefsEditor.apply();
}
public static final String FCM_CHANNEL_LIST = "PUBNUB_FCM_CHANNEL_LIST";
public static @Nullable String[] readChannelList() {
return (String[]) sharedPref.getStringSet(FCM_CHANNEL_LIST, null).toArray();
}
public static void writeChannelList(Set<String> value) {
SharedPreferences.Editor prefsEditor = sharedPref.edit();
prefsEditor.putStringSet(FCM_CHANNEL_LIST, value);
prefsEditor.apply();
}
}
class 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)
}
}
}
return sharePref
}
}
fun readDeviceToken(): String? = sharedPreferences.getString(FCM_DEVICE_TOKEN, null)
fun writeDeviceToken(value: String?) = sharedPreferences.edit().putString(FCM_DEVICE_TOKEN, value).apply()
fun readChannelList(): List<String> = sharedPreferences.getStringSet(FCM_CHANNEL_LIST, null)?.toList().orEmpty()
fun writeChannelList(value: Set<String>?) = sharedPreferences.edit().putStringSet(FCM_CHANNEL_LIST, value).apply()
}
Step 3b: Updating Existing Registrations
A simple helper method can be created to consolidate the remove-then-add functionality when updating your existing registered channels.
public void updatePushNotificationsOnChannels(String[] channels, String deviceToken, String oldToken) {
if (oldToken != null) {
pubnub.removeAllPushNotificationsFromDeviceWithPushToken()
.pushType(PNPushType.FCM)
.deviceId(oldToken)
.async(new PNCallback<PNPushRemoveAllChannelsResult>() {
@Override public void onResponse(PNPushRemoveAllChannelsResult result, PNStatus status) {
// Handle Response
}
});
}
pubnub.addPushNotificationsOnChannels()
.pushType(PNPushType.FCM)
.deviceId(deviceToken)
.channels(channels)
.async(new PNCallback<PNPushAddChannelResult>() {
@Override public void onResponse(PNPushAddChannelResult result, PNStatus status) {
// Handle Response
}
});
}
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
}
}
Step 4: Managing Device Registrations
Once a Device Token is obtained it can be registered with list of channels to allow 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.
String cachedToken = SharedPreferencesManager.readDeviceToken();
pubnub.addPushNotificationsOnChannels()
.pushType(PNPushType.FCM)
.deviceId(cachedToken)
.channels(Arrays.asList("ch1", "ch2", "ch3"))
.async(new PNCallback<PNPushAddChannelResult>() {
@Override
public void onResponse(PNPushAddChannelResult result, PNStatus status) {
// Handle Response
}
});
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.
String cachedToken = SharedPreferencesManager.readDeviceToken();
pubnub.auditPushChannelProvisions()
.pushType(PNPushType.FCM)
.deviceId(cachedToken)
.async(new PNCallback<PNPushListProvisionsResult>() {
@Override
public void onResponse(PNPushListProvisionsResult result, PNStatus status) {
// handle response.
}
});
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.
String cachedToken = SharedPreferencesManager.readDeviceToken();
pubnub.removePushNotificationsFromChannels()
.pushType(PNPushType.FCM)
.deviceId(cachedToken)
.channels(Arrays.asList("ch1", "ch2", "ch3"))
.async(new PNCallback<PNPushRemoveChannelResult>() {
@Override
public void onResponse(PNPushRemoveChannelResult result, PNStatus status) {
// handle response.
}
});
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.
The structure of the pn_gcm
payload specific to APNs notifications is as follows:
Field | Type | Required | Description |
---|---|---|---|
android | String | No | Android specific options for messages sent through FCM. Refer to the Firebase documentation |
android:notification | JSON | Yes | The dictionary sent to the Android device specify the type of interactions that you want the system to use when alerting the user. 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. |
One of the following fields are required to identify the target of the notification:
Field | Type | Description |
---|---|---|
token | String | Registration token to send a message to |
topic | String | Topic name to send a message to, e.g. "weather". Note: "/topics/" prefix should not be provided. |
condition | String | Condition to send a message to, e.g. "'foo' in topics && 'bar' in topics" |
{
"text": "John invited you to chat",
"pn_gcm": {
"topic": "invitations",
"android": {
"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(
"topic" to "invitations",
"pn_exceptions" to arrayOf("device-token1", "device-token2")
)
}
pushPayloadHelper.fcmPayload = fcmPayload
pushPayloadHelper.commonPayload = mapOf(
"John invited you to chat" to "text"
)
val pushPayload = pushPayloadHelper.build()
Step 6: Publishing 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_gcm
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.
pubnub.publish()
.channel("ch1")
.message(pushPayload)
.async(new PNCallback<PNPublishResult>() {
@Override
public void onResponse(PNPublishResult result, PNStatus status) {
// Handle Response
}
});
pubnub.publish(
channel = "ch1",
message = pushPayload
).async { result, status ->
// 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.