Building a Firebase Notification service with gRPC and Ktor (Part 2)

Welcome back!

If you haven’t checked out Part 1 already, I highly recommend it before coming here as it goes over the structure of the projects as well as the technologies used in this project. In this part, we will configure the Firebase service’s gRPC server to receive the notification requests.

Seeing as this is a Firebase service you need to ensure your Firebase service is configured with the google-services.json file. You can get this from the Firebase console in your project. Once the file is downloaded, you can place this in the resources directory of your project where the server configuration file lives. You can read the google-services.json file with the following piece of code in your Application module file. In this example I’ve created an extension function on the Application class that helps configure this:

fun Application.configureFirebase() {
    val firebaseConfig = environment.classLoader.getResourceAsStream("yourfirebasejsonfile.json")

    val firebaseOptions = FirebaseOptions.builder()
        .setCredentials(GoogleCredentials.fromStream(firebaseConfig))
        .setDatabaseUrl("your-database-url-here")
        .build()

    FirebaseApp.initializeApp(firebaseOptions)
}

Once Firebase is configured, lets hop through to the gRPC config again and continue finishing that off 🙂

The Protobuf Configuration File

If you recall from Part 1, we had a Protobuf configuration file that looked like this:

syntax = "proto3";
package dev.vusi.trainline.notification;

message NotificationRequest {
string userid = 1; //first argument in the method definition
string title = 2; //second, argument, etc
string message = 3;
string imageurl = 4;
}

// we also need to define what response to expect here, in this case a successResponse.
message NotificationResponse {
boolean isSuccess = 1;
}

// we define the method the client and server will use here.
service NotificationService {
rpc SendNotification(NotificationRequest) returns (NotificationResponse){}
}

So we need the same file here in our proto directory in the project:

Once that’s done we can build the project. The gRPC compiler should generate the necessary configuration files based on this Protobuf file.

Configuring the Server

Quick note regarding the FCM tokens before we continue – recall that tokens are dished out to devices on their first connection to the service (or upon request). How you store these tokens is completely up to you (For example you could make use of MongoDB atlas). However for the sake of the tutorial you could just log the token to LogCat in Android Studio so that it’s easier to retrieve and use immediately.

Now that we’ve generated the required configuration file from the build process, we can start the gRPC server configuration as follows:

Setup the Notification Service

So we have two parts to this. The first will be defining our Firebase service that will actively send the notifications through to the user using their FCM token.

The second part will be defining the gRPC service, passing in our Firebase service that will handle the notifications and returning and acknowledgement of whether sending the notification was successful or not.

So let’s go over the first part, configuration of the Firebase Service. Again simplicity is the goal here so we just create a class that has our required Firebase methods:

import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingException
import com.google.firebase.messaging.Message
import com.google.firebase.messaging.Notification

class SampleFirebaseService {

fun sendNotificationToFirebase(fcmToken: String, title: String, message: String, imageUrl: String): Boolean {
try {
val notification = Notification.builder()
.setTitle(title)
.setBody(message)
.setImage(imageUrl)
.build()

val serviceMessage = Message.builder()
.setNotification(notification)
.setToken(fcmToken)
.build()

// the message ID is returned as the response of a successful message
val response = FirebaseMessaging.getInstance().send(serviceMessage)

return response.isNotEmpty()
} catch (e: FirebaseMessagingException) {
e.printStackTrace()
return false
}
}
}

Great! We have a method that can successfully send messages through to Firebase!

Next we configure the server that will listen for requests. We start by defining the interface:

interface NotificationGrpcService {

fun shutdownClient()

fun startService()
}

The implement the interface. Here we can define things like port the service should run on as well as inject the repository where the Firebase notifications methods have been defined:

class NotificationGrpcServiceImpl(private val port: Int = 10000, private val notificationRepository: NotificationRepository): NotificationGrpcService {

private val server: Server = ServerBuilder
.forPort(port)
.addService(NotificationService())
.build()

override fun startService() {
server.start()
println("GRPC Server started, listening on $port")
server.awaitTermination()
}

override fun shutdownClient() {
server.shutdown()
}

private inner class NotificationService : NotificationServiceGrpcKt.NotificationServiceCoroutineImplBase() {
override suspend fun sendNotification(request: Notification.NotificationRequest): Notification.NotificationResponse {
val firebaseNotificationRequest = FirebaseNotificationRequest(
userId = request.userid,
notificationTitle = request.title,
notificationMessage = request.message,
notificationImageUrl = request.imageurl
)
notificationRepository.sendEventNotification(firebaseNotificationRequest)
println("notification info: ${request.userid} ${request.title}, ${request.message}, ${request.imageurl}")
return Notification.NotificationResponse.newBuilder().setMessage("message processed").build()
}
}
}

One thing to note here is that the NotificationServiceGrpcKt implementation is generated automatically once we’ve defined our notification.proto file with the grpc service, so make sure to build the project first before trying to implement your class.

Lastly, we start the service. The service needs to run in its own CoroutineScope so as not to interfere with the ktor application’s context. So what we can do is implement it as a Ktor application extension function, injecting the gRPC service interface here so that we can start it:

fun Application.configureGrpc(notificationService: NotificationGrpcService) {

val grpcCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

environment.monitor.subscribe(ApplicationStarted) {
grpcCoroutineScope.launch { notificationService.startService() }
}

environment.monitor.subscribe(ApplicationStopping) {
grpcCoroutineScope.launch { notificationService.shutdownClient() }
grpcCoroutineScope.cancel("Main Application scope shutting down...")
}
}

Great!

Build and run. the gRPC service should be up and running with the current configuration. In part 3 we’re going to startup our Android application and ensure that the gRPC client is able to send messages to the server