Tutorials

Android Push Notifications tutorial - Device registration via server

Ably can deliver native push notifications to devices using Google’s Firebase Cloud Messaging service. Native push notifications, unlike Ably’s 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, native 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 you can setup and send push notifications to Android your app using the Ably Push Notification service. You will also learn how to test this integration by sending notifications to an Android device.

Ably supports two types of client permissions for access to the Push Notification service:

  • push-subscribe: A client with this permission is a push target device, and it can manage its registration and any subscription 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.

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

By default, the Push Notification service provides the AblyRealtime.push.activate method for the device to register itself with FCM directly, but you can instead delegate the device registration and subscription to your server. In this tutorial we will look at registration via server. You can jump to the direct registration tutorial if you’d rather have the device directly register itself with FCM.

Step 1 – Set up a free account with Ably

In order to run these tutorials locally, you will need an Ably API key. If you are not already signed up, you should sign up now for a free Ably account. Once you have an Ably account:

  1. Log into your app dashboard
  2. Under “Your apps”, click on “Manage app” for any app you wish to use for this tutorial, or create a new one with the “Create New App” button
  3. Click on the “API Keys” tab
  4. Copy the secret “API Key” value from your Root key and store it so that you can use it later in this tutorial

    Copy API Key screenshot

Step 2 – Enabling Push in your Ably app

Create a sandbox application as shown below and make sure that both the Push Admin and Push Subscribe permissions are checked, for your API key. You can do this under the settings tab of your app in the developer dashboard.


Setting up a Sandbox app


Adding Push capabilities

Next, add a channel rule for your chosen channel namespace by checking the ‘Push notifications enabled’


Adding channel rules

Step 3 – Registering your app with FCM

In order to be able to use the Firebase Cloud Messaging service, you’ll need to register your app with FCM. To do this, first log into the Firebase developer console, create a new project or open an existing one, then register your app as shown below:


Adding Firebase to your app

Under your Firebase project settings, click on the Cloud Messaging tab. You should be able to see your FCM server key as shown below.


FCM server key

Copy this key and paste it in ‘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

Step 4 – Setting up a Node.js server for authentication

In this step, we’ll setup an authentication server that’ll authenticate the clients with Ably using the Token Authentication strategy.

When a client is authenticated and connected to Ably, they are considered to be an authenticated client. Whilst an authenticated client has a verifiable means to authenticate with Ably, they do not necessarily have an identity. When using the Token Authentication, you can have your sever assign the IDs to all the clients at the time of authentication. Unlike in Basic Authentication, you’ll not be using a direct API key on the client side. Instead, you’ll use an authUrl or authCallback to authenticate the clients. This is a recommended strategy to perform authentication on the client side as illustrated in our Best Practice Guide

We’ll use Node to implement the server. Install node.js if you do not have it by downloading it from here.

Next, create a folder and change to this directory. We’ll be using express.js in this tutorial. It’s a JavaScript framework that makes it easy to accept REST request and send responses back, without needing to worry about the protocol level details. More information on express.js can be found on the website.

Run the following command in terminal:

npm init

You will be prompted to provide some information, just hit return in order to accept the defaults and then run the following commands to install our dependencies:

npm install express --save
npm install ably --save

Create a file called ‘index.js’ and add the following code for authentication.

const express = require('express')
const Ably = require('Ably');
var realtime = new Ably.Realtime({key: "YOUR_API_KEY" });
const app = express()
const port = 3000
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

app.get('/auth', function (req, res) {
  var tokenParams = {
    'clientId': /* Assign a random id */
  }; 
  realtime.auth.createTokenRequest(tokenParams, function(err, tokenRequest) {
    if (err) {
      res.status(500).send('Error requesting token: ' + JSON.stringify(err));
    } else {
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify(tokenRequest));
    }
  });
});

You can return to your terminal and run your node server using the following command:

$ node index.js

If you navigate to http://localhost:3000/auth on your web browser, you should see the following response, indicating authentication was successful:

{
  "keyName": /* your key name */,
  "ttl": 3600000,
  "timestamp": 1473894038255,
  "capability": "{\"*\":[\"*\"]}",
  "nonce": "f6799a8e7fa6f77b8e1dac55314789b1",
  "mac": "35nifY9SRZ8KRDfKOPIS1qYWGP16r2lD59zJo9TH8pA="
}

The above is a trivial auth example as anybody accessing the auth endpoint is automatically given a token. If you would like more information on auth and on it’s implementation head over to the Token Authentication tutorial.

Step 5 – Using ngrok to make the server public

We’ll need to install ngrok to expose our local server to the internet. Follow the instructions on their ngrok’s website to install it.

Once you have it installed run the following command in terminal:

ngrok http 3000

You may find the above command not work, depending upon where you have ngrok installed on your drive. In such a case, try the following command instead:

$ ./ngrok http 3000

If everything goes as expected, you should see the following (or similar) within terminal:


Ngrok running example

It means that ngrok has opened up your localhost port 3000 to the internet and provided you with both a HTTP and HTTPS URLs to reach it.

Note: From this point on, you should use the https URL provided by ngrok and replace any existing or future http://localhost:3000 with that.

Step 6 – Adding the Google Services Gradle plugin

To build your own Android Project, please visit the Android Developers website and get familiar with steps necessary to set up your own application.

As part of enabling Google APIs or Firebase services in your Android application you may have to add the google-services plugin to your build.gradle file:

dependencies {
    classpath 'com.google.gms:google-services:4.2.0'
    // ...
}

The google-services plugin has two main functions:

  • Process the google-services.json file and produce Android resources that can be used in your application’s code.
  • Add dependencies for basic libraries required for the services you have enabled. This step requires that the apply plugin: ‘com.google.gms.google-services’ line be at the bottom of your app/build.gradle file so that no dependency collisions are introduced. You can see the result of this step by running ./gradlew :app:dependencies.

Download and add the google-services.json file to your Android app module’s root folder.


Downloading and installing the Google Services JSON file

Step 7 – Adding the Ably client library to your Android project

To start using Ably in your Android app, you need to include the Ably Client library. We recommend that you include the latest client library via Gradle in your module-level gradle.build file.

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

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

After you add the necessary dependencies, you can import the AblyRealtime class into your code and initialize it as shown below:

import io.ably.lib.realtime.AblyRealtime;

public class MainActivity extends AppCompatActivity {

    public static final int SUCCESS = 0;
    public static final int FAILURE = 1;
    public static final int UPDATE_LOGS = 2;

    public static final String STEP_1 = "Initialize Ably";
    public static final String STEP_2 = "Activate Push";
    public static final String STEP_3 = "Subscribe Channels";
    public static final String STEP_4 = "Send Test Push";

    public static final String TEST_PUSH_CHANNEL_NAME = "test_push_channel";
    //Ensure that ngrok is setup, or modify xml/network_security_config.xml accordingly.
    private static final String PRIVATE_SERVER_AUTH_URL = 'YOUR SERVER URL' + "/auth";
    private AblyRealtime ablyRealtime;

    private String getClientId() {
        String clientId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
        return clientId;
    }

    private void initAblyRuntime() throws AblyException {
        ClientOptions options = new ClientOptions();
        options.environment = BuildConfig.ABLY_ENV;
        options.key = BuildConfig.ABLY_KEY;
        //Token auth via server
        options.authUrl = PRIVATE_SERVER_AUTH_URL;
        options.authParams = new Param[]{new Param("clientId", getClientId())};

        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:
                        /* Perform device registration and subscription, this is described later in the tutorial */
                        break;
                    case disconnected:
                    case failed:
                        //handle failure
                        break;
                }
            }
        });
    }
}

Alternatively, you can add your API key in the local.properties file in order to prevent the secret keys from being accidentally committed to the version control system that you use. Further, assign the ably.env variable to either ‘sandbox’ or ‘production’ accordingly. However, storing API keys on the client side makes it prone to compromise and hence is not recommended.


Setting local properties in the android app

Step 8 – Adding device registration capability to the Node.js server and subscribing to the Push Notifications channel via server

In this step, we’ll setup the device registration and subscription for the Push Notifications to be received for messages sent on the push channel.

Add the following code to your index.js file:

app.get('/subscribe', (req, res) => {
  console.log('Registering device')
  var deviceId = req.query.deviceId
  console.log('Device Id ' + deviceId)
  var registrationToken = req.query.registrationToken
  var recipientDetails = {
            transportType: 'fcm',
            registrationToken: registrationToken
  }
  var device = {
    id: deviceId,
    formFactor: 'phone',
    metadata: 'PST',
          platform: 'android',
          push: {
              recipient: recipientDetails
          }
  }
  //device registration
  realtime.push.admin.deviceRegistrations.save(device, (err, device) => {
      if(err){
          console.log('Error: ' + err)
          res.status(500).send('Error registering: ' + JSON.stringify(err));
      } else{
          console.log('Device registered:' + device.id)
          subscribeDevice(device)
      }
      res.send('Complete')
  })
})

In the above code we use the save() method on the realtime.push.admin.deviceRegistrations to register the device with Ably, using its device ID, that was sent as part of the request. If the registration is successful, we go on to subscribe the device (or client) to the push channel over which we intend to send the required messages.

This function is shown below:

//device subscription
function subscribeDevice (device){
  var channelSub = {
      channel: 'push',
      deviceId: device.id
  }
  realtime.push.admin.channelSubscriptions.save(channelSub, (err, channelSub) => {
      if(err){
          console.log('Error: ' + err)
      } else{
          console.log('Device subscribed to push channel with deviceId' + device.id)
      }
  })
}

If there’s no error thrown, your device is successfully subscribed to the push channel.

Our server is all set to register the device and subscribe it to the push channel. Next, let’s have the device send the required identifiers to the server for performing the actual registration and subscription.

Step 9 – Sending device details to the server for registration

Next, you’ll need to use FirebaseInstanceIdService to generate the registrationToken for the device, then send this token along with other identifiers like deviceId, clientId etc to the server, in order for it to be able to register the device with FCM and subscribe it to the push channel using this info.

Go ahead and add two new folders in the same directory as your MainActivity.java file, namely ‘receivers’ and ‘server’. In the ‘receivers’ folder, add a new file and name it AblyPushMessagingService.java. In this file, we’ll add code to show a Push Notification on the device when it receives one. Paste the following into this file:

package YOUR-PACKAGE-NAME;

import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;

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

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);
    }

}

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>

Now, in your MainActivity class, add the following below the initAblyRuntime() function. In this function we send various device identifiers like deviceId, clientId and registrationToken to the server, so the device registration can be performed there.

private BroadcastReceiver pushReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (ABLY_PUSH_ACTIVATE_ACTION.equalsIgnoreCase(intent.getAction())) {
            ErrorInfo error = IntentUtils.getErrorInfo(intent);
            if (error != null) {
                logMessage("Error activating push service: " + error);
                handler.sendMessage(handler.obtainMessage(FAILURE));
                return;
            }
            logMessage("Device is now registered for push");
            handler.sendMessage(handler.obtainMessage(SUCCESS, STEP_3));
            return;
        }

        if (AblyPushMessagingService.PUSH_NOTIFICATION_ACTION.equalsIgnoreCase(intent.getAction())) {
            logMessage("Received Push message");
        }
    }
};

private void initAblyPush() throws AblyException {
    LocalDevice device = ablyRealtime.push.getActivationContext().getLocalDevice();
    if (device.push.recipient == null) {
        logMessage("Push not initialized. Please check Firebase settings");
        return;
    }
    String registrationToken = device.push.recipient.get("registrationToken").getAsString();
    if (registrationToken == null || registrationToken.length() == 0) {
        logMessage("Registration token cannot be null. Please check Firebase settings");
        return;
    }
    String deviceId = device.id;
    String clientId = getClientId();

    ServerAPI.getInstance().api().register(deviceId, registrationToken, clientId).enqueue(new Callback<NetResponse>() {
        @Override
        public void onResponse(Call<NetResponse> call, Response<NetResponse> response) {
            logMessage("Successfully registered: " + new Gson().toJson(response.body()));
            handler.sendMessage(handler.obtainMessage(SUCCESS, STEP_3));
        }

        @Override
        public void onFailure(Call<NetResponse> call, Throwable t) {
            logMessage("Error registering with server: " + t.getMessage());
            handler.sendMessage(handler.obtainMessage(FAILURE));
        }
    });
}

In the ‘server’ folder that you created before, add a new file and name it ServerAPI.java. Here, we’ll define the ServerAPI class that’ll send a request to the server with the device registration info. Paste the following in the this file:

package YOUR-PACKAGE-NAME;

import io.ably.lib.push.AblyFirebaseInstanceIdService;
/**
 * Leave this empty as the base class AblyFirebaseInstanceIdService does the FCM token registration.
 * In case your app requires access to FCM token as well, then override io.ably.lib.push.AblyFirebaseInstanceIdService#onTokenRefresh()
 */
public class AblyPushRegistrationService extends AblyFirebaseInstanceIdService {
    @Override
    public void onTokenRefresh() {
        //Make sure to call super.onTokenRefresh to initialize Ably push environment.
        super.onTokenRefresh();
    }
}

Next, in the same folder, that is ‘server’, add a new file and name it AblyPushMessagingService.java and paste the following into it.

package YOUR-PACKAGE-NAME;

import io.ably.tutorial.push_tutorial_two.BuildConfig;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;
import retrofit2.http.Query;

public class ServerAPI {
    public interface API {
        @GET("/register")
        Call<NetResponse> register(@Query("deviceId") String deviceId, @Query("registrationToken") String registrationToken, @Query("clientId") String clientId);
    }

    private static ServerAPI instance;

    public static ServerAPI getInstance() {
        if (instance == null) {
            instance = new ServerAPI();
        }
        return instance;
    }

    private final API api;

    private ServerAPI() {
        Retrofit.Builder builder = new Retrofit.Builder();
        builder.baseUrl(BuildConfig.ABLY_URL);
        builder.addConverterFactory(GsonConverterFactory.create());

        Retrofit retrofit = builder.build();
        api = retrofit.create(API.class);
    }

    public API api() {
        return api;
    }
}

This makes use of the NetResponse class. Let’s go ahead and define this next, in the same file, i.e, ‘server’. Create a new file named NetResponse.java and paste the following into it:

package YOUR-PACKAGE-NAME;

import io.ably.lib.rest.DeviceDetails;

public class NetResponse {
    public String id;
    public String platform;
    public String formFactor;
    public String clientId;
    DeviceDetails.Push push;
}


This class defines the data objects required to be sent to the server.

Step 10 – 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 on a real Android device and accepted push notifications.

a. Testing using Device ID

Add the following method to your index.js file and replace “YOUR_DEVICE_ID” with the id you just retrieved from your console:

app.get('/push/device', function (req, res) {
  var recipient = {
    deviceId: 'YOUR_DEVICE_ID'
  };
  var notification = {
    notification: {
      title: 'Hello from Ably!'
    }
  };
  realtime.push.publish(recipient, notification, function(err) {
    if (err) {
      console.log('Unable to publish push notification; err = ' + err.message);
      return;
    }
    console.log('Push notification published');
    res.send("Push Sent");
  });
})

Restart the server and navigate to YOUR-SERVER-URL/push/device in your browser. You should be able to receive a notification on your device as shown the image below.


Android Push Notification Screenshot

b. Testing using Client ID

We can test sending push notifications using the client ID. This allows us to push all devices with that client ID by adding the following code to your index.js file:

app.get('/push/client', function (req, res) {
  var recipient = {
    clientId: 'iosPushClient'
  };
  var notification = {
    notification: {
      title: 'Hello from Ably!'
    }
  };
  realtime.push.publish(recipient, notification, function(err) {
    if (err) {
      console.log('Unable to publish push notification; err = ' + err.message);
      return;
    }
    console.log('Push notification published');
    res.send("Push Sent");
  });
})

Note: If your clientId is different make sure to replace the clientId specified in the recipient object above with that.

Restart the server and go to YOUR-SERVER-URL/push/client you should receive a notification on your device as shown in the image below.


Android Push Notification Screenshot

c. Testing using Channels

Channel based Push Notifications allow us to push messages to a channel that the client on the device has subscribed to. To test this, add the following code to our index.js file:

app.get('/push/channel', function (req, res) {
  var extras = {
    push: {
      notification: {
        title: 'Hello from Ably!',
        body: 'Example push notification from Ably.'
      },
      data: {
        foo: 'bar',
        baz: 'qux'
      }
    }
  };

  var channel = realtime.channels.get('push');
  channel.publish({ name: 'example', data: 'data', extras: extras }, function(err) {
    if (err) {
      console.log('Unable to publish message with push notification; err = ' + err.message);
      return;
    }
    console.log('Message with push notification published');
    res.send("Push Sent");
  });
})

Restart the server and go to YOUR-SERVER-URL/push/channel. You should receive a notification on your device. Notice how this notification has both a title and a body. You can do this on all three types of push notifications mentioned earlier, if you wish. Simply add a body object to the notification part of your push payload. See the above code for an example of this.


Android Push Notification Screenshot

Further Reading

  • FCM documentation is a good place to find more information on the use of Push Notifications in Android.
  • You may also wish to deregister devices or get all of the stats related to your application implementing Push Notifications, such as the number and type of device registrations, etc. More information on how to do this can be found in the Push Admin API docs.

Ready to get started?

Our free plan includes 3m messages per month, 100 peak connections, 100 peak channels, and loads of features.