Many applications we create require the ability to send push notifications. Sometimes we are the ones who made the client and the backend side as well. The available tutorials are helpful but not always entirely clear; we met quite a few challenges. Let’s look at a specific example.

Now, I’m going to show you an example that has every piece in its place for us to be able to send push notifications in this scenario.

text

Tech used

First, let’s take a look at the service provider and the technologies we decided to use.

Firebase is pretty straight forward; it’s Google’s own, it is compatible with both iOS and Android, and it is capable of sending a large number of messages. Plus, many service providers use Firebase in the background.

We connected to Firebase with XMPP. It has many advantages over HTTP API, for us the most important is its ability to handle a large number of messages.

On the backend we used Akka with Java. Akka helps coping with the challenges that surface when writing multithreaded parallel applications, e.g. things that occur when resending a notification.

First, we created the mobile app. You can send test messages from the FCM console, so we can check whether receiving the messages works. After that we will write the backend according to the demands Firebase makes (look at the backend section to see more).

So we will write „Trusted environment” and the android app of this figure.

text

Firebase

If you have a Google account, you can register with it onto Firebase. It’s pretty simple and straightforward, so I’m not gonna dive into the details.

Mobile

After we’ve created a new app with Android studio, we need to add the FCM dependencies to it. Google helps us with this step as well, we only need our app’s package name. With the package name, we need to create a project in Firebase, then we need to follow the steps written in there.

  1. Dowload the google-services.json and copy it
  2. Add the dependencies, can do this in the gradle file.
dependencies {
...
    compile 'com.google.firebase:firebase-core:9.2.0'
compile 'com.google.firebase:firebase-messaging:9.2.0'                   
}

apply plugin: 'com.google.gms.google-services'
public class MyFirebaseMessagingService extends FirebaseMessagingService {
    private static final String TAG = "FCM Service";
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // TODO: Handle FCM messages here.
        
        Log.d(TAG, "From: " + remoteMessage.getFrom());
        Log.d(TAG, "Notification Message Body: " + remoteMessage.getNotification().getBody());
    }
}
<service android:name=".FirebaseIDService">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
    </intent-filter>
</service>
public class FirebaseIDService extends FirebaseInstanceIdService {
    private static final String TAG = "FirebaseIDService";
    private static Set<TokenRefreshListener> listeners = new HashSet<>();

    public static void addTokenRefreshListener(TokenRefreshListener listener) {
        listeners.add(listener);
    }

    @Override
    public void onTokenRefresh() {
        String refreshedToken = FirebaseInstanceId.getInstance().getToken();
        Log.d(TAG, "Refreshed token: " + refreshedToken); 
        for (TokenRefreshListener listener : listeners) {
            listener.onTokenRefresh(refreshedToken);
        }
    }

}

We use an observer design pattern, because our token will arrive in an asynchronous way.

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Main activity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FirebaseIDService.addTokenRefreshListener(new TokenRefreshListener() {
            @Override
            public void onTokenRefresh(final String newToken) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        EditText editText = (EditText) findViewById(R.id.editText);
                        editText.setText(newToken);
                    }
                });
            }
        });

        String token = FirebaseInstanceId.getInstance().getToken();
        if (token != null) {
            EditText editText = (EditText) findViewById(R.id.editText);
            editText.setText(token);
        }
    }
}

With that, our Android app is ready to receive the Push notification. We can try it from the FCM console: starting the Android app, it displays the token, in the FCM console, we can send a push notification by clicking on GROW / Notifications / NEW MESSAGE. Choose single device and copy the FCM token in:

Backend

There are some examples online, but these all end after the message is recieved on the Android device. Google, however, has more expectations from the backend.

Before you can write client apps that use Firebase Cloud Messaging, you must have a server environment that meets the following criteria:

There are some more limitations in the documentation:

Violating theses rules can get you excluded from FCM. Unfortunately the tutorials online do not discuss how to implement these limitations. This is what I am hoping to fix with this post, hope it will be useful!

I have mentioned in the beginning that we will be dealing with Akka. The actors that need to be created are listed in this figure:

We divide the limitations between the components. It’s their responsibility to abide by the rules.

Let’s look at the backend’s implementation. The entire implementation can be downloaded, we only look at the important or interesting points here.

Let’s create an akka project with the sbt new command. We need to add the akka and smack dependencies to the build.sbt.

name := """akka-push-notification"""

version := "1.0"

scalaVersion := "2.11.6"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor" % "2.3.11",

  "com.fasterxml.jackson.core" % "jackson-core" % "2.9.1",
  "com.fasterxml.jackson.core" % "jackson-databind" % "2.9.1",

  "org.igniterealtime.smack" % "smack-java7" % "4.2.1",
  "org.igniterealtime.smack" % "smack-tcp" % "4.2.1",
  "org.igniterealtime.smack" % "smack-extensions" % "4.2.1"
)

With the FCM, we communicate with json messages sent through the XMPP. This means that the json we want to send needs to be embedded in the XML. Creating this XML, sending it to the FCM and processing the FCM’s response is the job of CcsClient.

public class CcsClient implements PacketListener {

   private static final play.Logger.ALogger logger = play.Logger.of(Constants.LOGGER_FCM);
   private static final String LOG_TAG = "<<xmpp.server.CcsClient>>";

   private XMPPConnection connection;
   private ConnectionConfiguration config;
   private String fcmServerKey = null;
   private String fcmSenderId = null;
   private String fcmServerUsername = null;
   private long organizationId;

   public CcsClient(String fcmSenderId, String fcmServerKey) {
      this();
      this.fcmServerKey = fcmServerKey;
      this.fcmSenderId = fcmSenderId;
      this.fcmServerUsername = this.fcmSenderId + "@" + FcmConstants.FCM_SERVER_CONNECTION; 
   }

   private CcsClient() { 
      ProviderManager.getInstance().addExtensionProvider(FcmConstants.FCM_ELEMENT_NAME, FcmConstants.FCM_NAMESPACE,
            new PacketExtensionProvider() {

               @Override
               public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
                  String json = parser.nextText();
                  GcmPacketExtension packet = new GcmPacketExtension(json);
                  return packet;
               }
            });
   }

   public void connect() throws XMPPException {
      config = new ConnectionConfiguration(FcmConstants.FCM_SERVER, FcmConstants.FCM_PORT);
      config.setSecurityMode(SecurityMode.enabled);
      config.setReconnectionAllowed(true);
      config.setRosterLoadedAtLogin(false);
      config.setSendPresence(false);
      config.setSocketFactory(SSLSocketFactory.getDefault());
      config.setDebuggerEnabled(false);

      connection = new XMPPConnection(config);
      connection.connect();

      final CcsClient ccsClient = this;
      connection.addConnectionListener(new ConnectionListener() {

         @Override
         public void reconnectionSuccessful() {
            logger.info(LOG_TAG + " - Reconnection successful ...");
         }

         @Override
         public void reconnectionFailed(Exception e) {...}

         @Override
         public void reconnectingIn(int seconds) {...}

         @Override
         public void connectionClosedOnError(Exception e) {...}

         @Override
         public void connectionClosed() {...}
      });

      // Handle incoming packets (the class implements the PacketListener)
      connection.addPacketListener(this, new PacketTypeFilter(Message.class));

      // Log all outgoing packets
      connection.addPacketInterceptor(new PacketInterceptor() {
         @Override
         public void interceptPacket(Packet packet) {
            logger.debug(LOG_TAG + " - XMPP packet sent: " + packet.toXML());
         }
      }, new PacketTypeFilter(Message.class));

      connection.login(fcmServerUsername, fcmServerKey);
      logger.info(LOG_TAG + " - Logged in: " + fcmServerUsername);
   }

   @Override
   public void processPacket(Packet packet) {
      ActorRef fcmActor = FcmUtil.lookupFcmConnectionsActor(organizationId);
      CcsPacket ccsPacket = new CcsPacket();
      ccsPacket.ccsClient = this;
      ccsPacket.packet = packet;
      fcmActor.tell(ccsPacket, null);
   }

   public void send(String jsonRequest) {
      Packet request = new GcmPacketExtension(jsonRequest).toPacket();
      connection.sendPacket(request);
   }
}

Then follows the implementation of the FcmActor. The important method here is handleControlMessage(), this handles CONNECTION_DRAINING. In case of this message, we open a new active FCM connection.

public class FcmConnectionsActor extends UntypedActor {

    private CcsClient activeCcsClient;
    private Set<CcsClient> closingCcsClients = new HashSet<>();

    private final FcmOrganizationBean fcmOrganizationBean;

    ...
    public FcmConnectionsActor(FcmOrganizationBean fcmOrganizationBean) {
        this.fcmOrganizationBean = fcmOrganizationBean;
    }

    @Override
    public void onReceive(Object message) throws Exception {
        if(message instanceof PushNotificationBean) {
            sendPushNotification((PushNotificationBean) message);
        } else if(message instanceof CcsPacket) {
            processIncomingMessage((CcsPacket) message);
        } else if(message instanceof CcsConnectionClosed) {
            removeFromPool(((CcsConnectionClosed) message).ccsClient);
        } else {
            unhandled(message);
        }
    }

    private void sendPushNotification(PushNotificationBean pushNotificationBean) {...}

    private CcsOutMessage buildCcsMessage(PushNotificationBean pushNotificationBean) {...}

    private void processIncomingMessage(CcsPacket ccsPacket) {...}


    private void handleControlMessage(CcsClient ccsClient, Map<String, Object> jsonMap) {
        String controlType = (String) jsonMap.get("control_type");

        if(controlType.equals("CONNECTION_DRAINING")) {
            handleConnectionDrainingFailure(ccsClient);
        } else {
            logger.info(LOG_TAG + " - Received unknown FCM Control message: " + controlType);
        }
    }

    private void handleConnectionDrainingFailure(CcsClient drainingCcsClient) {
        if(drainingCcsClient.equals(activeCcsClient)) {
            closingCcsClients.add(activeCcsClient);
            activeCcsClient = null;
        } else {
            logger.info(LOG_TAG + " - Inactive FCM connection sent CONNECTION_DRAINING. We've already handled the connection draining for this connection");
        }
    }

    private CcsClient activeConnection() throws XMPPException {
        if(activeCcsClient == null) {
            activeCcsClient = new CcsClient(fcmOrganizationBean.fcmSenderId, fcmOrganizationBean.fcmServerKey);
            activeCcsClient.connect();
        }
        return activeCcsClient;
    }
}

In CcsMessageStagingActor we collect the push notifications that are waiting to be sent or are pending. We keep sending them until we reach 100 pending ones.

public class CcsMessageStagingActor extends UntypedActor {
    private static final int MAX_PENDING_NOTIFICATIONS = 100; 

    private Queue<PushNotificationBean> queuedNotifications = new LinkedList<>();
    private Map<UUID, PushNotificationBean> pendingNotifications = new HashMap<>(MAX_PENDING_NOTIFICATIONS);
    private SortedMap<Date, UUID> pendingNotificationSentAt = new TreeMap<>();

    private ActorRef errorHandlerActor;
    private ActorRef stepDoneActor;

    public CcsMessageStagingActor() {...}

    @Override
    public void onReceive(Object message) throws Exception {
        if(message instanceof PushNotificationBean) {
            processNotification((PushNotificationBean) message);
        } else if(message instanceof Ack) {
            handleAck((Ack) message);
        } else if(message instanceof Nack) {
            handleNack((Nack) message);
        } else if(message instanceof FailedPushNotificationBean) {
            handleFailed((FailedPushNotificationBean) message);
        } else if(message instanceof CleanUpStuckMessages) {
            cleanUpStuckMessages();
        } else {
            unhandled(message);
        }
    }

    private void cleanUpStuckMessages() {...}

    private void processNotification(PushNotificationBean pushNotificationBean) {...}

    private void handleAck(Ack ack) {...}

    private void handleNack(Nack nack) {...}

    private void handleFailed(FailedPushNotificationBean failedPushNotificationBean) {...}

    private void queuePushNotification(PushNotificationBean pushNotificationBean) {
        queuedNotifications.add(pushNotificationBean);
    }

    private void dequeueNext() {
        PushNotificationBean pushNotificationBean = queuedNotifications.poll();
        if(pushNotificationBean != null) {
            sendPushNotification(pushNotificationBean);
        }
    }

    private void sendPushNotification(PushNotificationBean pushNotificationBean) {
        updateLastRetry(pushNotificationBean.id);
        pendingNotifications.put(pushNotificationBean.id, pushNotificationBean);
        pendingNotificationSentAt.put(new Date(), pushNotificationBean.id);
        ActorRef fcmConnectionsActor = FcmUtil.lookupFcmConnectionsActor(pushNotificationBean.organizationId);
        fcmConnectionsActor.tell(pushNotificationBean, getSelf());
    }

}

And finally, let’s look at the code handling the exponential backoff resend function. This can be found in PushNotificationErrorHandlerActor. So we know when to try to resend a message, we need to take note of how many times we’ve already tried to send it.

public class PushNotificationErrorHandlerActor extends UntypedActor {
    @Override
    public void onReceive(Object message) throws Exception {
        if(message instanceof ActorRef) {
            stepDoneActor = (ActorRef) message;
        } else if(message instanceof FailedPushNotificationBean) {
            handleFailedPushNotification((FailedPushNotificationBean) message);
        } else if(message instanceof Nack) {
            handleNackMessage((Nack) message);
        } else {
            unhandled(message);
        }
    }

    private void handleFailedPushNotification(FailedPushNotificationBean failedPushNotificationBean) {...}

    private void handleNackMessage(Nack nack) {
            if(recoverable(nack)) {
                handleRecoverableFailure(nack) {
            } else {
                handleUnrecoverableFailure(nack);
          }
        }

    }

    private void handleRecoverableFailure(final Nack nack) {
        JPA.withTransaction(new F.Callback0() {
            @Override
            public void invoke() throws Throwable {
                PushNotification pushNotification = PushNotification.findById(nack.messageId);
                if(pushNotification.retries < MAX_RETRIES) {
                    long delay = calculatePushNotificationRetryDelay(pushNotification);
                    pushNotification.retries++;
                    pushNotification.lastRetry = new Date();
                    pushNotification.pushNotificationState = PushNotificationState.WAITING_FOR_RESEND;

                    Akka.system().scheduler().scheduleOnce(
                        Duration.create(delay, TimeUnit.MILLISECONDS),
                        ccsMessageStagingActor,
                        new PushNotificationBean(pushNotification),
                        Akka.system().dispatcher(),
                        getSelf());
                } else {
                    sendToStepDoneAsFailed(new PushNotificationBean(pushNotification));
                }
            }
        });
    }

    private void handleUnrecoverableFailure(final Nack nack) {...}

    private void sendToStepDoneAsFailed(PushNotificationBean pushNotificationBean) {...}

    private long calculatePushNotificationRetryDelay(PushNotification pushNotification) {
        long now = System.currentTimeMillis();

        long lastTry = pushNotification.lastRetry.getTime();
        long secondsBetweenTries = (long) Math.pow(2, pushNotification.retries);

        long nextTry = lastTry + (secondsBetweenTries * 1000);

        long delay = nextTry - now;
        return delay > 0 ? delay : 0L;
    }

}

It is not advised to choose a number greater than 15 for MAX_RETRIES, because 2^15 sec is about 18 hours. If this is not enough time for the FCM server to accept the message, there is a good chance it won’t ever do it.

My Github links:

https://github.com/papgaabor/fcm-push-example-android

https://github.com/papgaabor/fcm-push-example-backend

References:

Similar, though a bit outdated blog post:

https://www.grokkingandroid.com/xmpp-server-google-cloud-messaging/

Concrete backend implementation, though not as comprehensive (e.g missing „resend with exponential back-off”)

https://github.com/carlosCharz/fcmxmppserverv2

member photo

Papi is an excellent resource when it comes to Java, Akka, and Play framework questions. His sense of humor also makes him a delight to ask. 😉

Latest post by Gábor Pap

Push Notifications – a bigger picture