Send push notifications in a social network Android app - Part 2
You should first complete part one of the series. You will need some familiarity with Kotlin, and appropriate IDEs. IntelliJ IDEA and Android Studio are recommended.
This is part 2 of a 2 part tutorial. You can find part 1 here.
Introduction
We previously created a simple social events platform with an Android application to interact with it. Now we are going to expand this by adding support for push notifications for a variety of occurrences in the application, bringing a whole new level of interactivity for the uses.
Setting up push notifications can be confusing and time-consuming. However, with Pusher’s Beams API, the process is a lot easier and faster.
Prerequisites
In order to follow along, you will need some experience with the Kotlin programming language, which we are going to use for both the backend and frontend of our application.
You will also need appropriate IDEs. We suggest IntelliJ IDEA and Android Studio. Finally, you will need a free Pusher Account . Sign up now if you haven’t already done so.
Setting up your Pusher account
In order to use the Beams API and SDKs from Pusher, you need to create a new Beams instance in the Pusher Beta Dashboard.
Next, on your Overview for your Beams instance, click Get Started to add your Firebase Cloud Messaging (FCM) Server Key to the Beams instance. When setting this up, you must ensure you use the exact same package name as was used to create the Android application in the previous article.
After saving your FCM key, you can finish the Get Started wizard by yourself to send your first push notification, or just continue as we’ll cover this below.
It’s important to make sure that you download and keep the google-services.json
file from the Firebase Console as we are going to need this later on.
Once you have created your Beams instance, you will also need to note down your “Instance ID” and “Secret Key” from the Pusher Dashboard, found under the “Keys” section of your instance settings.
Sending push notifications
In the previous article we created some REST handlers for our backend. We now want to trigger push notifications from these handlers. These push notifications will all use the Event ID as the Interest, and the payload will contain the event data and an action key indicating what has happened to the event:
- POST /events/{id} - Event created
- PUT /events/{id} - Event details updated
- DELETE /events/{id} - Event deleted
- PUT /events/{id}/interest/{user} - User subscribed to an event
- DELETE /events/{id}/interest/{user} - User unsubscribed from an event
- POST /events/{id}/share - The event that was shared, targeted only at the one user
Note that we are not going to be emitting any push notifications from the GET handlers because they are used only for reading data. We are going to emit a notification from the POST handler, but it will be a global one that every user gets regardless and not an event-specific one since it’s not possible for anyone to have subscribed to the event at this point.
Firstly, we need a way of emitting our push notifications. For this we will create a new EventNotifier
class as follows:
@Component
class EventNotifier(
@Value("\${pusher.instanceId}") private val instanceId: String,
@Value("\${pusher.secretKey}") private val secretKey: String
) {
private val pusher = PushNotifications(instanceId, secretKey)
fun emitGlobal(action: String, event: Event) {
pusher.publish(
listOf(action),
mapOf(
"fcm" to mapOf(
"data" to mapOf(
"action" to action,
"id" to event.id,
"name" to event.name,
"description" to event.description,
"start" to event.start
)
)
)
)
}
fun emitForEvent(action: String, event: Event) {
pusher.publish(
listOf("EVENT_" + event.id!!),
mapOf(
"fcm" to mapOf(
"data" to mapOf(
"action" to action,
"id" to event.id,
"name" to event.name,
"description" to event.description,
"start" to event.start
)
)
)
)
}
fun emitForUsers(action: String, users: List<String>, event: Event) {
pusher.publish(
users.map { "USER_$it" },
mapOf(
"fcm" to mapOf(
"data" to mapOf(
"action" to action,
"id" to event.id,
"name" to event.name,
"description" to event.description,
"start" to event.start
)
)
)
)
}
fun emitFromUser(action: String, user: String, event: Event) {
pusher.publish(
listOf("EVENT_" + event.id!!),
mapOf(
"fcm" to mapOf(
"data" to mapOf(
"user" to user,
"action" to action,
"id" to event.id,
"name" to event.name,
"description" to event.description,
"start" to event.start
)
)
)
)
}
}
The @Component
annotation indicates that this class is a part of the application and that Spring Boot should automatically construct it and make it available elsewhere.
The @Value
annotations are used to provide property values from the configuration. These can come from a number of sources, but for now we will simply use the already-present application.properties
file. Update this to add two keys - pusher.instanceId
and pusher.secretKey
containing your Instance ID and secret key from earlier.
Note that these use the data
form of notifications. These provide a payload to the Android application to deal with it as it desires, giving complete freedom over what to do with it. There is an alternative form that can be used, providing some details as a notification
instead, but this is a lot more restricted in what you can achieve with it.
Next we need to make use of this new class. We can autowire this into our controller to make it automatically available, and then simply call it from the appropriate handler methods.
Update the constructor of our controller as follows:
class EventController(@Autowired private val eventNotifier: EventNotifier) {
And then add the following lines to the appropriate handler methods:
// createEvent
eventNotifier.emitGlobal("CREATED", newEvent)
// deleteEvent
events.find { it.id == id }
?.let { eventNotifier.emitForEvent("DELETED", it) }
// updateEvent
eventNotifier.emitForEvent("UPDATED", newEvent)
// registerInterest
events.find { it.id == event }
?.let { eventNotifier.emitFromUser("SUBSCRIBED", user, it) }
// unregisterInterest
events.find { it.id == event }
?.let { eventNotifier.emitFromUser("UNSUBSCRIBED", user, it) }
// shareEvent
events.find { it.id == event }
?.let { eventNotifier.emitForUsers("RECOMMENDED", friends, it) }
At this point, our backend application is fully able to send push notifications. If you were to trigger the REST endpoints then the push notifications will be sent from our application to the Pusher server, and from there on to any devices that are listening for them.
Receiving push notifications
Now that we can send push notifications, we need to be able to receive them as well. The Android application that was built in the previous article will be extended to support this.
First we need to add some dependencies to our project to support Pusher. Add the following to the project level build.gradle
, in the existing dependencies
section:
classpath 'com.google.gms:google-services:3.1.0'
Then add the following to the dependencies
section of the App level build.gradle
:
implementation 'com.google.firebase:firebase-messaging:11.8.0'
implementation 'com.pusher:push-notifications-android:0.9.12'
And this to bottom of the App level build.gradle
:
apply plugin: 'com.google.gms.google-services'
Next, copy the google-services.json
file we downloaded earlier into the app
directory under your project. We are now ready to actually develop our specific application using these dependencies.
Now that we can interact with events in our application, we’d like to get notifications when things happen whilst we’re not looking. This means setting up to receive push notifications on certain things happening.
In order to handle push notifications, we are going to use the PushNotifications
class provided by the Pusher push-notifications-android
dependency. This makes receiving these really simple and flexible.
To enable push notifications, we need to add the following to the onCreate
method of EventsListActivity
:
override fun onCreate() {
super.onCreate()
PushNotifications.start(getApplicationContext(), "YOUR_INSTANCE_ID");
}
Ensure that YOUR_INSTANCE_ID
is replaced with the instance ID that you got from your Pusher dashboard, and must be exactly the same as used in the backend application.
In order to receive push notifications, we need to subscribe to them for updates. These are all done by subscribing to an Interest, which our server will send out. In our application, these come in three forms:
- Global events that everyone gets. This is the
CREATED
event - Events that are targeted to a particular user - This is our
RECOMMENDED
event, and has an Interest ofUSER_<user>
- Events that are targeted to a particular event. There are several of these, but the Interest is always
EVENT_<event>
Let’s first subscribe to the ones we always want - the global and user ones. Add this to the onCreate
method of EventsListActivity
:
PushNotifications.subscribe("CREATED");
PushNotifications.subscribe("USER_" + (application as EventsApplication).username);
Next we want to subscribe to events that we are interested in, but not to ones we are no longer interested in.
Update the ViewEventActivity
to add the following to the appropriate onSuccess
handlers:
// onClickInterested
PushNotifications.subscribe("EVENT_" + eventId);
// onClickDisinterested
PushNotifications.unsubscribe("EVENT_" + eventId);
At this point, we can now receive the notifications. We just can’t react to them. For this we need to register a listener on the FCMMessagingService
. Update EventsApplication
to add the following:
override fun onCreate() {
super.onCreate()
FCMMessagingService.setOnMessageReceivedListener(object : PushNotificationReceivedListener {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val action = remoteMessage["action"]
if (action == "CREATED") {
showCreatedNotification(remoteMessage.data)
} else if (action == "SUBSCRIBED") {
showSubscribedNotification(remoteMessage.data)
} else if (action == "UNSUBSCRIBED") {
showUnsubscribedNotification(remoteMessage.data)
} else if (action == "RECOMMENDED") {
showRecommendedNotification(remoteMessage.data)
}
}
})
}
private fun showRecommendedNotification(data: Map<String, String>) {
Log.v("EventsApplication", "Received Recommended Notification: " + data.toString())
}
private fun showUnsubscribedNotification(data: Map<String, String>) {
Log.v("EventsApplication", "Received Unsubscribed Notification: " + data.toString())
}
private fun showSubscribedNotification(data: Map<String, String>) {
Log.v("EventsApplication", "Received Subscribed Notification: " + data.toString())
}
private fun showCreatedNotification(data: Map<String, String>) {
Log.v("EventsApplication", "Received Created Notification: " + data.toString())
}
This will now generate log messages for our push notifications, but not anything useful. Let’s change that.
Note: this doesn’t cover all of the possible actions that we emit. The rest are left as an exercise for the reader.
Before we can display any notifications, we need to set things up. In the EventsApplication
class, add the following field:
private lateinit var notificationManager: NotificationManager
And then add the following to the top of the onCreated
method:
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel("events",
"Pusher Events",
NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(channel)
}
This is needed to display notifications on newer versions of Android, otherwise they just silently vanish.
Now we can actually display them. The simple one first is the CREATED
notification. Add the following to the showCreatedNotification
method:
val intent = Intent(applicationContext, EventsListActivity::class.java)
val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "events")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("New event: " + data["name"])
.setContentText(data["description"])
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
notificationManager.notify(0, notification.build())
This displays a simple notification, and clicking on it takes you to the Events List.
Next we’ll show notifications for subscribed and unsubscribed. Update as follows:
// showSubscribedNotification
val intent = Intent(this, ViewEventActivity::class.java)
intent.putExtra("event", data["id"])
val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "events")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(data["user"] + " is interested in " + data["name"])
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
notificationManager.notify(0, notification.build())
// showUnsubscribedNotification
val intent = Intent(this, ViewEventActivity::class.java)
intent.putExtra("event", data["id"])
val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "events")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(data["user"] + " is no longer interested in " + data["name"])
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
notificationManager.notify(0, notification.build())
The difference is that this time we are going to direct the user to the actual View Event page when they click on the notification, and so we are passing the event ID through in the Intent.
Finally, we are going to display a notification for when the event was recommended to us. This time we are going to have two explicitly named actions that each do slightly different things. Update showRecommendedNotifiction
as follows:
val viewIntent = Intent(this, ViewEventActivity::class.java)
viewIntent.putExtra("event", data["id"])
val pendingViewIntent = PendingIntent.getActivity(applicationContext, 0, viewIntent, 0)
val interestedIntent = Intent(this, ViewEventActivity::class.java)
interestedIntent.putExtra("event", data["id"])
interestedIntent.putExtra("trigger", "interested")
val pendingInterestedIntent = PendingIntent.getActivity(applicationContext, 1, interestedIntent, 0)
val notification = NotificationCompat.Builder(this, "events")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Event " + data["name"] + " has been recommended to you")
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.addAction(NotificationCompat.Action.Builder(R.mipmap.ic_launcher, "View", pendingViewIntent).build())
.addAction(NotificationCompat.Action.Builder(R.mipmap.ic_launcher, "Interested", pendingInterestedIntent).build())
notificationManager.notify(0, notification.build())
Note that one of the Intent’s that we are using has an additional property - “trigger” - that we are using to indicate to the target Activity that something should happen. Now we need to make a slight change to the ViewEventActivity
to handle this extra action. Update the onCreate
method to add the following:
val trigger = intent.getStringExtra("trigger")
if (trigger == "interested") {
onClickInterested(null)
}
This will cause the onClickInterested
method to be called immediately on displaying the activity if the user came here from the “Interested” action on our notification.
Conclusion
This article shows how simple it is to add push notifications using Pusher’s Beams API and SDKs to your already-existing application, and how powerful such functionality can be.
The full source for the entire application is available on Github. Why not add some more features to it for yourself, and have a fully-functional event management application to share with friends.
This is part 2 of a 2 part tutorial. You can find part 1 here.
12 April 2018
by Graham Cox