The purpose of this tutorial is to help you understand what each part of the Beams API does in a client Android app. Consider this a supplement to the Beams documentation if you need a little extra explanation. I'll take you through code examples and explain what each method does. The whole demo project is available on GitHub so that you can see the code in context.
This is more of a reference material rather than a step-by-step tutorial, but since it does contain an Android project demonstrating the full API of the Android Beams SDK, I’ll give you some pointers for how to set the demo up and how to send push notifications to test it.
If you running the demo project, these are the prerequisites:
This project was tested with Pusher Beams Android SDK version 1.4.2.
The demo app I’ll be using has a layout with buttons to show each part of the Beams SDK.
To set up this demo app, perform the following tasks:
com.example.beamsapidemo
. Enter the FCM Server Key and download the google-service.json
file. At that point you can quit the quick start wizard. Go to your Pusher dashboard, open your new instance, and go to the Credentials tab. You will find your Instance ID and Secret Key there.app/google-services.json
with the one you downloaded from the FCM setup.MainActivity.kt
file, set the INSTANCE_ID
constant to your Instance ID.The interesting parts of the repo (with code comments) are here:
I will describe the aspects related to sending push notifications below.
In the descriptions throughout this article I will direct you to send push notifications to test the app. You could do that from a server, but for simplicity we'll use curl. (Alternatively, you could use Postman if you are more comfortable with that.)
Below are some curl commands that you will find useful. Replace the SSSSSSSSSSSSSSSSS
with your Beams Secret Key and replace IDIDIDIDIDIDID
with your Beams Instance ID, both of which you can find in your Pusher dashboard under the Credentials tab for your instance.
Sending an FCM push notification for the device interest apple
:
1curl -H "Content-Type: application/json" \ 2 -H "Authorization: Bearer SSSSSSSSSSSSSSSSS" \ 3 -X POST "https://IDIDIDIDIDIDID.pushnotifications.pusher.com/publish_api/v1/instances/IDIDIDIDIDIDID/publishes/interests" \ 4 -d '{"interests":["apple"],"fcm":{"notification":{"title":"My title","body":"Body message"}}}'
You can change apple to something else or include multiple interests. That is, replace ["apple"]
with ["apple","pear"]
.
Sending an FCM push notification to an authenticated user Mary
:
1curl -H "Content-Type: application/json" \ 2 -H "Authorization: Bearer SSSSSSSSSSSSSSSSS" \ 3 -X POST "https://IDIDIDIDIDIDID.pushnotifications.pusher.com/publish_api/v1/instances/IDIDIDIDIDIDID/publishes/users" \ 4 -d '{"users":["Mary"],"fcm":{"notification":{"title":"My title","body":"Hey, Mary"}}}'
This one is the same as the interests request, except that the string interests
is replaced with users
(in the path and in the JSON key), and the JSON value for users
includes "Mary"
in the array. I changed the message body a little, too.
In each section below I’ll describe what the methods of the SDK do.
You always need to call the start()
method first. If you try to run other methods before calling this one, your app will crash. Normally you would call it in your activity's onCreate()
method, but in the demo app I put it in a button clicked method. This lets you see the app crash if you are so inclined.
PushNotifications.start(this, INSTANCE_ID)
The start method takes two parameters. The first is the context. Since we are in an Activity, I used this
. The second is the instance ID. I added the instance ID as a constant at the top of the class:
1companion object { 2 // replace this with your Beams instance ID 3 const val INSTANCE_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 4 }
My instance ID is different than yours will be. You can get it from your Beams dashboard.
Calling stop()
means that you won't get any more notifications. The state (device interests and Beams token) is deleted on the local device and on the remote Pusher server.
PushNotifications.stop()
If you want to start getting messages again, you will need to call start()
and resubscribe to any interests that you want (since the state was deleted). Calling another method without calling start()
first may cause the app to crash (Although, in my tests I could manipulate the device interests without causing a crash. At best, the state should be considered undefined.)
One use case for stop
would be if a user wanted to opt out of receiving all notifications.
When a user logs out, prefer using clearAllState()
. See the “User” section below.
Start must be called first:
Stopping clears the state:
Stopping prevents notifications from being delivered:
Interests are topics that a device can subscribe to. A device can subscribe to multiple topics in the form of a list of strings. When the server sends a push notification for a string that is in the device's list of interests, that device will receive a notification.
This method returns Set<String>
, which is a list of all of the interests that the device is subscribed to.
val interests = PushNotifications.getDeviceInterests()
As long as you don't call stop()
, clearInterests()
, or clearAllState()
, the interests will still be available between runs of the app, even if the device powers down and restarts.
With setDeviceInterests
you can choose a new list of interests to subscribe to. This replaces any old list that the device might have had. The device will receive push notifications for all interests in this list, but not for any interests outside of this list.
PushNotifications.setDeviceInterests(interests)
The type of the parameter interests
is Set<String>
. In the case of the demo app, this Set is obtained from a CheckBox alert dialog.
There is nothing difficult about clearDeviceInterests()
. It does what it says and removes any interests that the device is subscribed to. The device will no longer receive push notifications for any interests.
PushNotifications.clearDeviceInterests()
This method adds an interest to the list of interests that the device is already subscribed to. The device will receive push notifications for this interest.
PushNotifications.addDeviceInterest(interest)
The type for the parameter interest
is a String
.
Adding the same interest twice does not cause a crash. It's the same as adding it once.
This method removes an interest from the list of interests that the device is already subscribed to. The device will no longer receive push notifications for this interest.
PushNotifications.removeDeviceInterest(interest)
The type for the parameter interest
is a String
.
Trying to remove an interest that the device is not subscribed to does not cause a crash.
These tests assume that you have already clicked the Start button to start the SDK.
Adding an interest allows the device to receive a notification
Removing an interest causes the device to not receive notification:
Setting a list of interests replaces the old list:
While interests are for sending push notifications to a large numbers devices by topic, user notifications can be sent to specific authenticated users. The notification will go to every device on which the user is logged in.
Of all the methods in the Beams SDK, this one is the most complex. Part of the reason is security. You have to prove who you are before Pusher will send you user specific notifications. Otherwise you could just say you're me and then get all of my private notifications.
The way it works is like this. First of all, a user who wants to receive private notifications gets a Beams token from the app server (not Pusher). This could be a one-step process using basic authentication with a username and password. Or it could be a two-step process of first logging in to get a session token and then using it to get a Pusher token. The server creates the Pusher token by signing a JWT payload with the instance's secret key. After the user has the Pusher token, it is sent to Pusher to prove they are who they say are. Only then will Pusher send them personal notifications.
Here is the sequence diagram taken from the Beams documentation:
The process above is somewhat hidden by the Beams Android client API. The setUserId
method takes three parameters:
1PushNotifications.setUserId( 2 userId: String, 3 tokenProvider: TokenProvider, 4 callback: BeamsCallback<Void, PusherCallbackError> 5 )
userId
is the username that the app server knows the user as, and also what Pusher will use. When the app server wants to send a notification to a user, it will use this ID.TokenProvider
is an interface with a single method, that is, fetchToken(userId: String)
. You pass in the user ID and get back the Beams token. The Android SDK uses this method to get the Beams token from your server whenever it needs it. You could write your own implementation of the TokenProvider
interface, but the SDK already has one called BeamsTokenProvider
. The source code is here if you want to see what it does.callback
allows you to handle the success or failure of the setUserId
request.Since setUserId
requires a TokenProvider
and most people will use the BeamsTokenProvider
, lets take a look at it. It has two parameters:
1BeamsTokenProvider( 2 authUrl: String, 3 authDataGetter: AuthDataGetter 4 )
authUrl
is the endpoint on your server where you request the Beams token. It could be something like https://www.example.com/beamstoken
.AuthDataGetter
comes in. It’s an interface with a single method getAuthData()
, which returns an AuthData
instance. AuthData
is data class to hold the headers and query parameters.1AuthData( 2 headers: Map<String, String>, 3 queryParams: Map<String, String> 4 )
headers
are a map of whatever auth data your server uses to authenticate a user. For example, the key would probably be Authorization
and the value might be Bearer sometoken
.queryParams
. If you are not using it, though, you can leave this parameter out.So after all that explanation, here is what setting the user ID looks like in the demo app:
1// basic authentication credentials 2 val userId = "Mary" 3 val password = "mypassword" 4 val text = "$userId:$password" 5 val data = text.toByteArray() 6 val base64 = Base64.encodeToString(data, Base64.NO_WRAP) 7 8 // Token Provider 9 val serverUrl = "http://10.0.2.2:8888/token" 10 val tokenProvider = BeamsTokenProvider( 11 serverUrl, 12 object: AuthDataGetter { 13 override fun getAuthData(): AuthData { 14 return AuthData( 15 headers = hashMapOf( 16 "Authorization" to "Basic $base64" 17 ) 18 ) 19 } 20 } 21 ) 22 23 // Get the Beams token and send it to Pusher 24 PushNotifications.setUserId( 25 userId, 26 tokenProvider, 27 object : BeamsCallback<Void, PusherCallbackError> { 28 override fun onFailure(error: PusherCallbackError) { 29 Log.e("BeamsAuth", 30 "Could not login to Beams: ${error.message}") 31 } 32 override fun onSuccess(vararg values: Void) { 33 Log.i("BeamsAuth", "Beams login success") 34 } 35 } 36 )
I'm just using basic authentication to simplify the code.
Before publishing this article, I reached out to the Pusher team (at support@pusher.com) for advice. Here is one of their comments:
One final thing to explain in the
setUserId
method is that you should always call it whenever the app starts and you know the user is logged in.For example, consider the Facebook app. When you open it, it will check if the previous logged in user still has a valid session, and if so, proceed to display the news feed. It is at this point that
setUserId
should be called again. If at this point, the app realises the user is no longer logged in, thenclearAllState
should be called.This keeps the device in sync with the server and deletes with cases such as server-side user deletion.
I could have put this method with the other SDK methods because internally it basically just calls stop()
and then start()
. However, the main use for clearAllState()
is for when a user is logging out.
PushNotifications.clearAllState()
The Beams token (and any interests) will be deleted and the user will not receive any personal notifications on this device. In my tests, calling clearAllState
did not remove the listeners (see next section).
With how the Android SDK is set up, there isn't really a way to use curl or Postman alone. You need to have a backend server to provide Beams tokens. Doing that is beyond the scope of this article, but you can find documentation for many server SDKs in the Beams documentation. Make special note of the generateToken()
method. If you want to use a Dart server that is already configured for this demo, check out my previous tutorial. You will need to replace the Secrert Key and Instance ID with your own.
These tests assume that you have already clicked the Start button to start the SDK. Make sure your server is running, too.
Setting the user ID allows user to receive personal notification
Clearing the state prevents the device from receiving a notification:
You have a few options for getting updates about incoming messages and changes in the SDK.
Setting this listener allows you to handle changes to the list of interests that the device is subscribed to. This method is Activity specific. That is, you should set the listener in each activity that you need it.
1PushNotifications.setOnDeviceInterestsChangedListener(object : SubscriptionsChangedListener { 2 override fun onSubscriptionsChanged(interests: Set<String>) { 3 Toast.makeText(applicationContext, 4 "interests changed to: $interests", 5 Toast.LENGTH_SHORT).show() 6 } 7 })
The SubscriptionsChangedListener
is an interface with a single method onSubscriptionsChanged
, which provides you with the new list of interests. Subscriptions is the old way to refer to device interests. For example, the deprecated version of this method is called setOnSubscriptionsChangedListener
.
The thing about push notifications is that they only appear when your app is in the background. So if a user gets one while they are using your app, they won't see it. You can overcome this problem by setting a listener to tell you when a message comes in.
You should set this listener in your activity's onResume()
method. Like the previous listener, this listener must be set in every activity where you want to handle it.
1override fun onResume() { 2 super.onResume() 3 4 PushNotifications.setOnMessageReceivedListenerForVisibleActivity(this, object : 5 PushNotificationReceivedListener { 6 override fun onMessageReceived(remoteMessage: RemoteMessage) { 7 showInSnackBar(rootView, 8 "Message received: " + 9 "Title: \"${remoteMessage.notification?.title}\"" + 10 "Body \"${remoteMessage.notification?.body}\"" 11 ) 12 } 13 }) 14 }
PushNotificationReceivedListener
is an interface with one method, onMessageReceived
, which provides you with a RemoteMessage
. This is a Firebase object that you can read more about here.
By extracting the data from the RemoteMessage
, you are able to update the UI or make a custom notification.
The two previous listeners were activity specific. If you want to make a general listener that works no matter what activity you are in and even while the app is in the background, you can create a MessagingService
subclass.
To do that, as I noted earlier, you need to register the service in your AndroidManifest
:
1<service android:name=".NotificationsMessagingService"> 2 <intent-filter android:priority="1"> 3 <action android:name="com.google.firebase.MESSAGING_EVENT" /> 4 </intent-filter> 5 </service>
where NotificationsMessagingService
is a Kotlin class:
1// app/src/main/java/com/example/beamsapidemo/NotificationsMessagingService.kt 2 3 package com.example.beamsapidemo 4 5 import android.util.Log 6 import com.google.firebase.messaging.RemoteMessage 7 import com.pusher.pushnotifications.fcm.MessagingService 8 9 class NotificationsMessagingService : MessagingService() { 10 override fun onMessageReceived(remoteMessage: RemoteMessage) { 11 Log.i("MessagingService", "Remote message was received") 12 } 13 // This method is only for integrating with other 3rd party services. 14 // For most use cases you can omit it. 15 override fun onNewToken(token: String) { 16 Log.i("MessagingService", "FCM token was changed") 17 } 18 }
MessagingService
is a wrapper (with some extra processing) around FirebaseMessagingService
. It exposes onMessageReceived
and onNewToken
.
In the demo app I just print messages to the log output, but you could use this service to create a custom notification in onMessageReceived
. Here is a related example for Firebase. Note that if the app is in the background, this method will not get called unless the notification has a data-only payload. By "data" I mean a custom JSON structure for the data
key in the FCM.
1{ 2 "data": { 3 "key": "value", 4 }, 5 }
This one is not a data-only FCM because there is a notification
key:
1{ 2 "notification": { 3 "title": "Hello", 4 "body": "Hello, World!", 5 } 6 "data": { 7 "key": "value", 8 }, 9 }
The MessagingService
listener would not respond for that FCM while the app is in the background. It would display as a normal notification. While the app is in the foreground, though, the message would be caught.
Overriding onNewToken
provides you with an FCM device token. If you don’t know what that is for, then you probably don’t need it. It’s not necessary to override this method for the majority of use cases. Pusher already takes care of handling the FCM token for you behind the scenes. However, if you need the token to integrate with other 3rd party push notification services, you can get it here.
These tests assume that you have already clicked the Start button to start the SDK.
Activity listener handles changed interests:
Activity listener handles received messages while in foreground:
Messaging service handles received messages in foreground app:
Messaging service handles data only payload notifications while app in background:
Messaging service does not handle notifications while app in background when the FCM has a notification
payload:
notification
key (See the “MessagingService” section above.)I hope this explanation of the full Android client API for the Beams SDK has helped you to understand it better. Bookmark this page for future reference.
The demo app is available on GitHub.