Implementing Push Notifications on Android devices

Ably can deliver Push Notifications to Android devices using Firebase Cloud Messaging. Push Notifications, unlike our channel-based Pub/Sub messaging, do not require the device to maintain a connection to Ably, as the underlying platform or OS is responsible for maintaining its own battery efficient transport to receive Push Notifications. Therefore, Push Notifications are commonly used to display visual notifications to users or launch a background process for an app in a battery efficient manner.

In this tutorial, we’ll see how to set up and send Push Notifications to your Android app using Ably’s Push Notification service.

To enable Push Notifications on your device, it must be registered with FCM first. This can be done in two ways; you can either have the device register itself directly with Ably or delegate the registration work to your app server, which would then register the device with Ably on its behalf. In this tutorial we’ll demonstrate how to directly register with Ably.

Step 1 – Create your Ably app and API key

To follow this tutorial, you will need an Ably account. Sign up for a free account if you don’t already have one.

Access to the Ably global messaging platform requires an API key for authentication. API keys exist within the context of an Ably application and each application can have multiple API keys so that you can assign different capabilities and manage access to channels and queues.

You can either create a new application for this tutorial, or use an existing one.

To create a new application and generate an API key:

  1. Log in to your Ably account dashboard
  2. Click the “Create New App” button
  3. Give it a name and click “Create app”
  4. Copy your private API key and store it somewhere. You will need it for this tutorial.

To use an existing application and API key:

  1. Select an application from “Your apps” in the dashboard
  2. In the API keys tab, choose an API key to use for this tutorial. The default “Root” API key has full access to capabilities and channels.
  3. Copy the Root API key and store it somewhere. You will need it for this tutorial.

    Copy API Key screenshot

Step 2 – Enabling Push in your Ably app

Next, you’ll need to ensure your Ably app is correctly set up to use Push Notifications. Firstly, make sure the API key you’re using has both the Push Admin and Push Subscribe permissions, as seen below. If not, select the Settings option on the API key interface and tick the permissions so that they’re available.


Adding Push capabilities

These permissions correspond to the following:

  • push-subscribe: A client with this permission is a push target device, and it can manage registration and subscription to Push Notifications for itself. However, it won’t be able to manage push registrations or channel subscriptions for any other device.
  • push-admin: A client with this permission has full access to the Push Admin API and can manage registrations and subscriptions for all devices.

The push-subscribe permission will allow the Android app to subscribe to Push Notifications. push-admin is what will allow us to publish Push Notifications, and would be useful if we wanted to create a server which controlled our push devices.

Read more about permissions (also called capabilities) in our API documentation.

Next, navigate to the Ably app’s Settings tab, go down to the Channel rules section and add a new channel rule for a namespace by checking the Push notifications enabled option. In this tutorial, we’ll be eventually publishing some Push Notifications via the push:test_push_channel channel, so we’ll use the channel namespace push, as seen below.


Adding channel rules

With this done, your app now has a key with push permissions, along with a channel on which push notifications can be sent.

Step 3 – Create an Android app registered with FCM

Now that we’ve set up our permissions with Ably, it’s time to work on our new Android app. In order for the app to be able to use the Firebase Cloud Messaging service, you’ll need to create an app with FCM. To do this, follow the following steps:

Firstly make sure you have installed the latest version of Android Studio, and create a new empty project in it. In Android Studio, you should then set up a device or an emulator for testing purposes.

See this step in Github

With your empty project ready, you should log in to the Firebase developer console and create a new project by selecting Add project and following the steps provided.

Once this is done, register your app to this project as shown below, making sure to download the google-services.json and add the appropriate dependencies as described in the process.


Adding Firebase to your app

Next, go to your project’s Settings and click on the Service Accounts tab. You should be able to create a new private key as shown below.


FCM service account

Save this key and upload it in the ‘Setting up Google/Firebase Cloud Messaging’ section under the Notifications tab of your Ably app dashboard and click save.


Setting up FCM on Ably dashboard

See this step in Github

Step 4 – Adding the Ably client library to your Android app

With the groundwork done for setting up FCM, it’s time to integrate Ably into your Android app. To start using Ably, you will need to include the Ably Client library via Gradle in your app-level gradle.build file.

apply plugin: 'com.android.application'
...
dependencies {
    ...
    implementation 'io.ably:ably-android:1.1.0'
}

In the above example, a specific version of the library is referenced. You can check which is the latest stable version and should use that.

With Ably added as a dependency, you can import the AblyRealtime class into your code and initialize it as shown below:

package YOUR_PACKAGE_ID;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

import io.ably.lib.realtime.AblyRealtime;
import io.ably.lib.realtime.CompletionListener;
import io.ably.lib.realtime.ConnectionStateListener;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ClientOptions;
import io.ably.lib.types.ErrorInfo;
import io.ably.lib.util.IntentUtils;

public class MainActivity extends AppCompatActivity {
    private AblyRealtime ablyRealtime;

    private TextView rollingLogs;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        rollingLogs = findViewById(R.id.rolling_logs);
        try {
            initAblyRuntime();
        } catch (AblyException e) {
            logMessage("AblyException " + e.getMessage());
        }
    }

    /**
     * Step 1: Initialize Ably Runtime
     *
     * @throws AblyException
     */
    private void initAblyRuntime() throws AblyException {
        ClientOptions options = new ClientOptions();
        options.key = "REPLACE_WITH_YOUR_API_KEY";
        options.clientId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);

        ablyRealtime = new AblyRealtime(options);
        ablyRealtime.setAndroidContext(getApplicationContext());
        ablyRealtime.connect();
        ablyRealtime.connection.on(new ConnectionStateListener() {
            @Override
            public void onConnectionStateChanged(ConnectionStateChange state) {
                logMessage("Connection state changed to : " + state.current.name());
                switch (state.current) {
                    case connected:
                        logMessage("Connected to Ably with clientId " + ablyRealtime.auth.clientId);
                        break;
                }
            }
        });
    }

    private void logMessage(String message) {
        Log.i(MainActivity.class.getSimpleName(), message);
        rollingLogs.append(message);
        rollingLogs.append("\n");
    }

    public void activatePush(View view) {
        // We will fill this in the next step
    }

    public void deactivatePush(View view) {
        // We will fill this in the next step
    }
}

Note: Make sure to change YOUR_PACKAGE_ID to your package ID, and REPLACE_WITH_YOUR_API_KEY to your actual API key. Generally on untrusted devices such as this you would use Token authentication, which allows for the underlying credentials to never be revealed. However, for the simplicity of this tutorial we will use our API key directly.

Next, add the following code to the layout file activity_main.xml. Layout files should go in the src/main/res/layout folder within your app.

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:id="@+id/steps"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="activatePush"
            android:text="Activate device" />
        <Button
            android:id="@+id/deactivate"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="deactivatePush"
            android:text="Deactivate device" />

        <TextView
            android:id="@+id/rolling_logs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:autoLink="all" />
    </LinearLayout>
</ScrollView>

Currently the two buttons won’t do anything, however we will in the next step make them activate and deactivate the device for Push Notifications.

Finally, add the following activity inside your application section within your AndroidManifest.xml file:

<application
    ...
    >
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

If you now run this app on your emulator or device, you should be able to connect to Ably with a clientId.

See this step in Github

Step 5 – Integrating FCM into your Android app

With the app able to connect to Ably, it’s time to make use of FCM. Go ahead and add a new folder in the same directory as your MainActivity.java file, and name it receivers. In this new folder, add a new file and name it AblyPushMessagingService.java with the following code:

package YOUR.PACKAGE.NAME;

import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

import io.ably.lib.push.ActivationContext;
import io.ably.lib.types.RegistrationToken;

public class AblyPushMessagingService extends FirebaseMessagingService {
    public static final String PUSH_NOTIFICATION_ACTION = AblyPushMessagingService.class.getName() + ".PUSH_NOTIFICATION_MESSAGE";

    @Override
    public void onMessageReceived(RemoteMessage message) {
        //FCM data is received here.
        Intent intent = new Intent(PUSH_NOTIFICATION_ACTION);
        LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
    }

    @Override
    public void onNewToken(String s) {
        super.onNewToken(s);
        //Store token in Ably
        ActivationContext.getActivationContext(this).onNewRegistrationToken(RegistrationToken.Type.FCM, s);
    }
}

This class extends the base FCM functionality, allowing us to intercept events and act upon them. The onMessageReceived function will allow us to intercept and broadcast whenever a new Push message is received. onNewToken will do the same for whenever we receive a token for Push.

Next, go ahead and add this class as a service in your manifest fie. Open the AndroidManifest.xml and paste the following after <activity></activity> within the <application></application> tags.

<service android:name=".receivers.AblyPushMessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

Finally, we need to add firebase libraries to our app-level build.gradle. Add the following lines of code to your dependencies:

dependencies {
    ...
    implementation 'com.google.firebase:firebase-messaging:19.0.1'
    implementation 'com.google.firebase:firebase-core:17.0.0'
}

See this step in Github

Step 6 – Directly registering the devices with FCM

With FCM added and extended, we will need to make use of it in our code. In this step, we’ll activate the device with the FCM server, allowing the device to receive Push Notifications.

In your MainActivity.java file, import your new AblyPushMessagingService with your prior imports, in addition to LocalBroadcastManager:

import YOUR.PACKAGE.NAME.receivers.AblyPushMessagingService;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

Then, replace the activatePush and deactivatePush functions with the following:

public void activatePush(View view) {
    try {
        ablyRealtime.push.activate();
    } catch (AblyException e) {
        logMessage("AblyException activating push: " + e.getMessage());
    }
}

public void deactivatePush(View view) {
    try {
        logMessage("Deactivating Push on device");
        ablyRealtime.push.deactivate();
    } catch (AblyException e){
        logMessage("AblyException deactivating push: " + e.getMessage());
    }
}

In the above function, we’ve used the activate method to register the device with FCM via Ably. We’ve also included the deactivate method so you can easily de-register from Push notifications. These will be called from their respective buttons in the UI.

Next, add a BroadcastReceiver, which we’ll use to handle broadcasts from our AblyPushMessagingService:

private BroadcastReceiver pushReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if ("io.ably.broadcast.PUSH_ACTIVATE".equalsIgnoreCase(intent.getAction())) {
            ErrorInfo error = IntentUtils.getErrorInfo(intent);
            if (error!=null) {
                logMessage("Error activating push service: " + error);
                return;
            }
            try {
                logMessage("Device is now registered for push with deviceId " + deviceId());
                subscribeChannels();
            } catch(AblyException e) {
                logMessage("AblyException getting deviceId: " + e);
            }
            return;
        }
        if (AblyPushMessagingService.PUSH_NOTIFICATION_ACTION.equalsIgnoreCase(intent.getAction())) {
            logMessage("Received Push message");
        }
    }
};

private String deviceId() throws AblyException {
    return ablyRealtime.device().id;
}

private void subscribeChannels() {
    ablyRealtime.channels.get("push:test_push_channel").push.subscribeClientAsync(new CompletionListener() {
        @Override
        public void onSuccess() {
            logMessage("Subscribed to push for the channel");
        }

        @Override
        public void onError(ErrorInfo reason) {
            logMessage("Error subscribing to push channel " + reason.message);
            logMessage("Visit link for more details: " + reason.href);
        }
    });
}

In addition to listening to Push events, we also log our deviceId and subscribe to the Ably channel push:test_push_channel for Push Notifications.

Finally, register these receivers in your onCreate function:

LocalBroadcastManager.getInstance(this).registerReceiver(pushReceiver, new IntentFilter("io.ably.broadcast.PUSH_ACTIVATE"));
LocalBroadcastManager.getInstance(this).registerReceiver(pushReceiver, new IntentFilter(AblyPushMessagingService.PUSH_NOTIFICATION_ACTION));

See this step in Github

Step 7 – Testing the app

Now that our app is ready, let’s test it out. Before using any of these methods make sure you have built and run the app, and have successfully activated it for Push Notifications. We’ll be using curl commands to publish messages for simplicity, however our libraries have all the functionality for publishing used here as well.

a. Testing using Device ID

Using the Device ID logged when activating the device, run the following curl:

curl -X POST https://rest.ably.io/push/publish \
 -u "YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 --data \
 '
{
  "recipient": {
    "deviceId": "YOUR_DEVICE_ID"
  },
  "notification": {
    "title": "Ably Push Tutorial",
    "body": "Push notification from Ably"
  }
}'

Make sure to replace YOUR_API_KEY with your API Key, and YOUR_DEVICE_ID with your Device ID.

If you had the app open, you should see a new line in the log letting you know a message was received. If you had closed the app, you should see a Push Notification like below:


Android Push Notification Screenshot

b. Testing using Client ID

Using the Client ID logged when connecting to Ably, run the following curl:

curl -X POST https://rest.ably.io/push/publish \
 -u "YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 --data \
 '
{
  "recipient": {
    "clientId": "YOUR_CLIENT_ID"
  },
  "notification": {
    "title": "Hello from Ably via Client ID!",
    "body": "Example push notification from Ably."
  }
}'

Make sure to replace YOUR_API_KEY with your API Key, and YOUR_CLIENT_ID with your Client ID.

c. Testing using Channels

You can also publish messages using Ably channels, by adding a push object to a message sent on it. Publishing via a channel is the easiest way to publish a Push Notification to a collection of devices with different client and device IDs.

curl -X POST https://rest.ably.io/channels/MY_PUSH_CHANNEL/messages \
 -u "YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 --data \
 '
{ "name": "myMessage",
  "extras": {
    "push": {
      "notification": {
        "title": "Hello from Ably!",
        "body": "Example push notification from Ably."
      }
    }
  }
}'

Make sure to replace YOUR_API_KEY with your API key, and MY_PUSH_CHANNEL with the Ably channel your app is subscribed to, in this tutorial being push:test_push_channel.

Step 8 – Start using your new app

With your app successfully able to subscribe to Push Notifications, and you being able to send Notifications, you can start expanding on it. A few ideas would be:

Further Reading