Send push notifications in a social network Android app - Part 2

Introduction

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 Beams API, the process is a lot easier and faster.

android-social-network-push-notifications-app-preview

Prerequisites

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.

Then create a free sandbox Pusher account or sign in.

Setting up your Pusher account

To use the Beams API and SDKs, you need to create a new Beams instance in the 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:

1@Component
2    class EventNotifier(
3            @Value("\${pusher.instanceId}") private val instanceId: String,
4            @Value("\${pusher.secretKey}") private val secretKey: String
5    ) {
6        private val pusher = PushNotifications(instanceId, secretKey)
7    
8        fun emitGlobal(action: String, event: Event) {
9            pusher.publish(
10                    listOf(action),
11                    mapOf(
12                            "fcm" to mapOf(
13                                    "data" to mapOf(
14                                            "action" to action,
15                                            "id" to event.id,
16                                            "name" to event.name,
17                                            "description" to event.description,
18                                            "start" to event.start
19                                    )
20                            )
21                    )
22            )
23        }
24    
25        fun emitForEvent(action: String, event: Event) {
26            pusher.publish(
27                    listOf("EVENT_" + event.id!!),
28                    mapOf(
29                            "fcm" to mapOf(
30                                    "data" to mapOf(
31                                            "action" to action,
32                                            "id" to event.id,
33                                            "name" to event.name,
34                                            "description" to event.description,
35                                            "start" to event.start
36                                    )
37                            )
38                    )
39            )
40        }
41    
42        fun emitForUsers(action: String, users: List<String>, event: Event) {
43            pusher.publish(
44                    users.map { "USER_$it" },
45                    mapOf(
46                            "fcm" to mapOf(
47                                    "data" to mapOf(
48                                            "action" to action,
49                                            "id" to event.id,
50                                            "name" to event.name,
51                                            "description" to event.description,
52                                            "start" to event.start
53                                    )
54                            )
55                    )
56            )
57        }
58        
59        fun emitFromUser(action: String, user: String, event: Event) {
60            pusher.publish(
61                    listOf("EVENT_" + event.id!!),
62                    mapOf(
63                            "fcm" to mapOf(
64                                    "data" to mapOf(
65                                            "user" to user,
66                                            "action" to action,
67                                            "id" to event.id,
68                                            "name" to event.name,
69                                            "description" to event.description,
70                                            "start" to event.start
71                                    )
72                            )
73                    )
74            )
75        }
76    }

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:

1// createEvent
2    eventNotifier.emitGlobal("CREATED", newEvent)
3    
4    // deleteEvent
5    events.find { it.id == id }
6            ?.let { eventNotifier.emitForEvent("DELETED", it) }
7    
8    // updateEvent
9    eventNotifier.emitForEvent("UPDATED", newEvent)
10    
11    // registerInterest
12    events.find { it.id == event }
13            ?.let { eventNotifier.emitFromUser("SUBSCRIBED", user, it) }
14    
15    // unregisterInterest
16    events.find { it.id == event }
17            ?.let { eventNotifier.emitFromUser("UNSUBSCRIBED", user, it) }
18            
19    // shareEvent
20        events.find { it.id == event }
21                ?.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:

1implementation 'com.google.firebase:firebase-messaging:11.8.0'
2    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:

1override fun onCreate() {
2        super.onCreate()
3        PushNotifications.start(getApplicationContext(), "YOUR_INSTANCE_ID");
4    }

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 of USER_<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:

1PushNotifications.subscribe("CREATED");
2    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:

1// onClickInterested
2    PushNotifications.subscribe("EVENT_" + eventId);
3    
4    // onClickDisinterested
5    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:

1override fun onCreate() {
2        super.onCreate()
3        FCMMessagingService.setOnMessageReceivedListener(object : PushNotificationReceivedListener {
4            override fun onMessageReceived(remoteMessage: RemoteMessage) {
5                val action = remoteMessage["action"]
6    
7                if (action == "CREATED") {
8                    showCreatedNotification(remoteMessage.data)
9                } else if (action == "SUBSCRIBED") {
10                    showSubscribedNotification(remoteMessage.data)
11                } else if (action == "UNSUBSCRIBED") {
12                    showUnsubscribedNotification(remoteMessage.data)
13                } else if (action == "RECOMMENDED") {
14                    showRecommendedNotification(remoteMessage.data)
15                }
16            }
17        })
18    }
19    
20    private fun showRecommendedNotification(data: Map<String, String>) {
21        Log.v("EventsApplication", "Received Recommended Notification: " + data.toString())
22    }
23    
24    private fun showUnsubscribedNotification(data: Map<String, String>) {
25        Log.v("EventsApplication", "Received Unsubscribed Notification: " + data.toString())
26    }
27    
28    private fun showSubscribedNotification(data: Map<String, String>) {
29        Log.v("EventsApplication", "Received Subscribed Notification: " + data.toString())
30    }
31    
32    private fun showCreatedNotification(data: Map<String, String>) {
33        Log.v("EventsApplication", "Received Created Notification: " + data.toString())
34    }

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:

1notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
2    
3    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
4        val channel = NotificationChannel("events",
5                "Pusher Events",
6                NotificationManager.IMPORTANCE_DEFAULT)
7        notificationManager.createNotificationChannel(channel)
8    }

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:

1val intent = Intent(applicationContext, EventsListActivity::class.java)
2    val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, 0)
3    
4    val notification = NotificationCompat.Builder(this, "events")
5            .setSmallIcon(R.mipmap.ic_launcher)
6            .setContentTitle("New event: " + data["name"])
7            .setContentText(data["description"])
8            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
9            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
10            .setContentIntent(pendingIntent)
11    
12    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:

1// showSubscribedNotification
2    val intent = Intent(this, ViewEventActivity::class.java)
3    intent.putExtra("event", data["id"])
4    val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, 0)
5    
6    val notification = NotificationCompat.Builder(this, "events")
7            .setSmallIcon(R.mipmap.ic_launcher)
8            .setContentTitle(data["user"] + " is interested in " + data["name"])
9            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
10            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
11            .setContentIntent(pendingIntent)
12    
13    notificationManager.notify(0, notification.build())
14    
15    // showUnsubscribedNotification
16    val intent = Intent(this, ViewEventActivity::class.java)
17    intent.putExtra("event", data["id"])
18    val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, 0)
19    
20    val notification = NotificationCompat.Builder(this, "events")
21            .setSmallIcon(R.mipmap.ic_launcher)
22            .setContentTitle(data["user"] + " is no longer interested in " + data["name"])
23            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
24            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
25            .setContentIntent(pendingIntent)
26    
27    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:

1val viewIntent = Intent(this, ViewEventActivity::class.java)
2    viewIntent.putExtra("event", data["id"])
3    val pendingViewIntent = PendingIntent.getActivity(applicationContext, 0, viewIntent, 0)
4    
5    val interestedIntent = Intent(this, ViewEventActivity::class.java)
6    interestedIntent.putExtra("event", data["id"])
7    interestedIntent.putExtra("trigger", "interested")
8    val pendingInterestedIntent = PendingIntent.getActivity(applicationContext, 1, interestedIntent, 0)
9    
10    val notification = NotificationCompat.Builder(this, "events")
11            .setSmallIcon(R.mipmap.ic_launcher)
12            .setContentTitle("Event " + data["name"] + " has been recommended to you")
13            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
14            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
15            .addAction(NotificationCompat.Action.Builder(R.mipmap.ic_launcher, "View", pendingViewIntent).build())
16            .addAction(NotificationCompat.Action.Builder(R.mipmap.ic_launcher, "Interested", pendingInterestedIntent).build())
17    
18    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:

1val trigger = intent.getStringExtra("trigger")
2    if (trigger == "interested") {
3        onClickInterested(null)
4    }

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.

android-social-network-push-notifications-app-preview

Conclusion

This article shows how simple it is to add push notifications using Pusher 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.