Using the Android App Messaging SDK

A tutorial for adding the Android App Messaging SDK code to your app to allow your app users to receive push notifications from your Dotdigital account

Our Android App Messaging SDK uses Firebase Cloud Messaging (FCM) to send push notifications to your Android app users. If you haven't already configured a Firebase project then please follow the instructions here first.

Our Android SDK is open source and can be found on Github here:

Basic sample apps can be found in Github here:

To embed in your native Android apps, complete the following tasks:

  1. Install the SDK
  2. Configure the SDK
  3. Initialise the SDK
  4. Handle push message callbacks
  5. Optional: Change the icon or colour of your push notifications
  6. Starting a session

📘

Handling asynchronous requests

You can use either standard callbacks or observable streams to handle asynchronous requests. If you want to use observable streams, you need to add the RxJava library to your app. This tutorial explains how to use standard callbacks, but you can find a full sample of the RxJava code in the 'Android sample code' section.

Installing the SDK

  1. Add the App Messaging SDK to your module-level build.gradle file and change the minSDKVersion attribute to 21 using the snippet below:
defaultConfig {
...
minSdkVersion 21
...
}
Dependencies {
...
implementation 'com.comapi:foundation:1.5.0'
}

🚧

Seeing a 'Manifest merger' error?

The SDK has a minSDKVersion attribute of 21, your attribute must have a value of at least 21.

  1. Add the following dependencies to your top level build.gradle file:
buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.3.10'
    }
}
  1. Add the following plugins to your module level build.gradle file:
plugins {
    id 'com.google.gms.google-services'
}
  1. Copy the google-services.json file from your Firebase project into your apps root folder; this is used to configure your app to work with Firebase.

👍

Sync Now

Remember to synchronise build.gradle files after making any changes.

Configuring the Android SDK

Before you can configure the Android SDK, you need the following:

  1. Create a placeholder push handler class which implements the PushMessageListener interface to facilitate callbacks. The PushHandler is the class we passed instance of into the SDK init method. Inside we will get push messages delivered when the app is visible.
   public class PushHandler implements PushMessageListener {
       @Override
       public void onMessageReceived(RemoteMessage message) {
         // TODO : Add push message handling such as displaying messages when in foreground and
         // handling deep links and custom data
   }
  1. Create a new instance of the ComapiConfig class, pass your API Space Id to the apiSpaceId() method, and an instance of the class that creates a JWT to the authenticator() method using the snippet below:
   ComapiConfig config = new ComapiConfig() 
   // Set the id of the app space, the device belongs to
   .apiSpaceId("<API_SPACE_ID>")
   // Sets handler for authentication challenges (SDK asking for JWT token)
   .authenticator(new ChallengeHandler())
   // When we are initialising the SDK in the Application class we will need to provide pushMessageListener to listen for push notification delivered when the app is visible to the user.
   // In which case Android won't display notification in the system tray.
   .pushMessageListener(new PushHandler());

Configuring logs and proxy servers

You can set internal file logs, console logs, and network logs to the following levels. By default all log levels are set to WARNING:

  • OFF
  • FATAL
  • ERROR
  • WARNING
  • INFO
  • DEBUG

To change the log levels, add the following line to the ComapiConfig object and use the appropriate method on the LogConfig object:

.logConfig(new LogConfig().setFileLevel(LogLevel.DEBUG).setConsoleLevel(LogLevel.INFO).setNetworkLevel(LogLevel.ERROR));

You can also set a custom limit for the size of internal log files by calling the following method on the ComapiConfig object:

.logSizeLimitKilobytes(limit);

If your app connects through a proxy server, add the following line to the ComapiConfig object:

.apiConfiguration(new APIConfig().proxy("http://xx.xxx.xx.xxx:xxxx"));

Initialising the Android SDK

🚧

Where to initialise the Android SDK in the activity lifecycle

Initialisation needs to be performed in the onCreate() of the Android Application class so that your app users' profiles don't change every time they open the app. Remember to set that class in the manifest file <application android:name=".MyApplicationClass">

  1. After you've configured the SDK, import the com.google.firebase.FirebaseApp class into your java file, and initialise the Firebase SDK.
FirebaseApp.initializeApp(this);

🚧

Firebase must be initialised before the SDK

Please ensure the FirebaseApp.initializeApp(this); is called prior to initialising the SDK as it is a dependency of the SDK.

2 Pass the ComapiConfig object to one of the following initialisation methods. These methods return a ComapiClient object that you can use to access the session and profile services, which are used to create and update the user's profile.

  • The initialiseShared() method: When you use this method, the SDK stores the 'ComapiClient' object, and you can access it at any time by calling the Comapi.getShared() method (Java) or the RxComapi.getShared() method (RxJava)
  • The initialise() method: When you use this method, you need to store the ComapiClient object yourself. The getShared() method is not available when you use this method.
public class MyApplication extends Application implements Callback<ComapiClient> {

    @Override
    public void onCreate()
    {
        super.onCreate();
      
      	FirebaseApp.initializeApp(this);
      	
      	ComapiConfig config = new ComapiConfig() 
          // Set the id of the app space, the device belongs to
          .apiSpaceId("<API_SPACE_ID>")
          // Sets handler for authentication challenges (SDK asking for JWT token)
          .authenticator(new ChallengeHandler())
          // When we are initialising the SDK in the Application class we will need to provide pushMessageListener to listen for push notification delivered when the app is visible to the user.
          // In which case Android won't display notification in the system tray.
          .pushMessageListener(new PushHandler());
      
        // Asynchronously initialise Foundation SDK client (retrieve it in callback). 
      	// Shared makes ComapiClient accessible from Comapi.getShared()
      	// The last this passed requires your class to implement Callback<ComapiClient> which will be invoked when the SDK has initialised
        Comapi.initialiseShared(this, config, this);
  	}
  
  	
    // This is a callback for successful initialisation. We need to send a message to Activities  
    // that SDK is initialised and the client is available. 
    @Override
    public void success(ComapiClient client) {
        // SDK initialised
    }
  
    @Override
    public void error(Throwable t) {
			// Error when initialising SDK
    }
}

Handle push message callbacks

It is now time to flesh out the functionality of the push handler class which is called whenever the SDK receives a push message. Add the functionality as guided below:

Push handler callback

Within your push handler class that implements the pushMessageListener interface you'll need to decide what you want to do with the push notification and implement the logic by overriding the onMessageReceived() method. This method is called when a push notification is received while the app is in the foreground.

Functionality may include:

Displaying push notifications when the app is in the foreground

Push notifications are automatically displayed by Android only while the app is in the background. These notifications are sent to the system tray and launch your app when users tap them, but if you want to display the push notification message while the app is in the foregrounded, you will need to implement this.

You can extract the received push message body by calling message.getNotification().getBody() within your onMessageReceived handler.

Please see the official Android guidance for notfications to understand how to implement this, the example provided below is a basic implementation for illustrative purposes:

  1. Within your method you can retrieve the push message body by calling message.getNotification().getBody() you can then display or store this as desired e.g.
public class PushHandler implements PushMessageListener {

    @Override
    public void onMessageReceived(RemoteMessage message) {
      	try {
            PushDetails result = ComapiClient.parsePushMessage(message);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        // Create an Intent for the activity you want to start
        Intent resultIntent = new Intent(context, MainActivity.class);
      
      	// Create the TaskStackBuilder and add the intent, which inflates the back stack
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
        stackBuilder.addNextIntentWithParentStack(resultIntent);
      
				// Get the PendingIntent containing the entire back stack
        PendingIntent resultPendingIntent =
                stackBuilder.getPendingIntent(0,
                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

        // Check if message contains a notification payload.
        if (message.getNotification() != null) {
            // Do something with the message to notify the user
            // Example
            createNotificationChannel(context);
            Notification.Builder b = new Notification.Builder(context);
            b.setAutoCancel(true)
                    .setDefaults(NotificationCompat.DEFAULT_ALL)
                    .setWhen(System.currentTimeMillis())
                    .setSmallIcon(R.drawable.ic_launcher_background)
                    .setContentTitle("foreground message")
                    .setContentText(message.getNotification().getBody())
                    .setContentInfo("INFO")
                    .setContentIntent(resultPendingIntent);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                b.setChannelId("test_channel_id1");
            }
            NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            nm.notify(1, b.build());
        }
}

private void createNotificationChannel(Context context) {
        // Create the NotificationChannel, but only on devices that have the Google API 26+ because the NotificationChannel class is new and not in supported in older libraries
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            CharSequence name = "test channel";
            String description = "some description";
            int importance = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel("test_channel_id1", name, importance);
            channel.setDescription(description);
            // Register the channel with the system; you can't change the importance
            // or other notification behaviors after this
            NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }
}

Handling deep links

The SDK is capable of handling deep links for you and tracking their usage, which is our recommendation.
If you want to trigger deep links within your own app to drive functionality then you will need to implement these in your app prior to sending them in a push, however you can always use a URL or invoke another apps deep links without needing to implement your own deep link schema.

To enable deep link handling you must do the following:

  1. For the app to understand which Activity it should open when myappscheme://mycustomhost link is invoked the activity declaration in AndroidManifest should have an intent-filter:
<activity
    android:name=".MyActivity"
    android:exported="true">
    <intent-filter android:label="Open Activity in DotDigital Sample App">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- Accepts URIs that begin with "myappscheme://mycustomhost” -->
        <data
            android:host="mycustomhost"
            android:scheme="myappscheme" />
    </intent-filter>
</activity>
  1. Your apps MainActivity needs to be adapted to ensure that any intents passed (onCreate, onNewIntent) to the app are checked to see if they are push messages so that the SDK can automatically track the usage in Dotdigital's analytics and invoke any deep links that are associated with the push action. In addition you app will need to request permission to invoke deep links in later versions of Android, so this needs to be down in the onCreate. The code required is shown below:

public class MainActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      askPermission();
      ComapiClient client = Comapi.getShared();
      handlePush(client, getIntent());
  }
   
  // If you use android:launchMode="singleTop" this will be the place where new intent for an exsiting activity will be delivered
  @Override
  protected void onNewIntent(Intent intent) {
      super.onNewIntent(intent);
    	ComapiClient client = Comapi.getShared();
      handlePush(client, intent);
  }
  
  // Starting from Android 13 (API level 33) we need a runtime permission to post notifications.
  // SDK manifest already includes POST_NOTIFICATIONS permission, but we also need to trigger a popup with permission request.
  // This is done with askPermission() method. You should handle the response from the popup as we have not shown this in this basic example.
  void askPermission() {
      if (Build.VERSION.SDK_INT >= 33) {
          if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
                  != PackageManager.PERMISSION_GRANTED) {
            	// Manifest.permission.POST_NOTIFICATIONS is available only if 'compileSdk 33' or greater
              requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS},
                      1);
          }
      }
  }
 
  // Handle push notification intents
  void handlePush(ComapiClient client, Intent intent) {
      if (client != null) {
  
          // If we call client.handlePushMessage method providing the Activity context, intent that started the Activity, a boolean flag telling the SDK if it should open a deep link if it finds it and a callback
          // then the sdk will track the notification click interaction in dotdigital analitics, optionally try to start activity open the deep link and as a result will provide an object with:
          //  getUrl - dotdigital deep link url 
          //  getData - dotdigital custom data 
          //  isTrackingSuccessful - was the click recorded in analytics
          //  isStartActivitySuccessful - was the activity associated with the deep link started 
  
          client.handlePushNotification(this, intent, true, new Callback<PushHandleResult>() {
            @Override
            public void success(PushHandleResult result) {
                
            }

            @Override
            public void error(Throwable t) {
                
            }
        });
      }
  }
}

Extracting a deep link

If you wish to extract a deep link URL so that you can choose how to handle it, you can use the parsePushMessage function to parse Dotdigital data for the URL or Firebase RemoteMessage (a helper method).

public class PushHandler implements PushMessageListener {
  @Override
  public void onMessageReceived(RemoteMessage message) {
      try {
        	// Let the SDK parse the push payload
          PushDetails result = ComapiClient.parsePushMessage(message);
          // Get the deep link URL if available
          Log.i("TEST", "url = " + result.getUrl());
      } catch (JSONException e) {
          e.printStackTrace();
      }
  }
}

Handling custom data payloads

The SDK can deliver custom data payloads via the push messages which your app can interpret to drive functionality. To access this custom data you will need to call the SDKs parsePushMessage() method with the contents of the push message. Any custom data can be extracted for the returned object by calling getData(). Add this code to your onMessageRecevied() implementation in your push handler class. e.g.

public class PushHandler implements PushMessageListener {
  @Override
  public void onMessageReceived(RemoteMessage message) {
      try {
        	// Let the SDK parse the push payload
        	try {
            PushDetails result = ComapiClient.parsePushMessage(message);
          } catch (JSONException e) {
              e.printStackTrace();
          }

        	// Get the custom data using getData()
          Log.i("TEST", "data = " + (result.getData() != null ? result.getData().toString() : "null"));

      } catch (JSONException e) {
          e.printStackTrace();
      }
  }
}

Changing the icon or colour of push notification

Our Android SDK uses Firebase Cloud Messaging (FCM) to send push notifications to your Android app users. As such, you can use the Android <meta-data> tag in your AndroidManifest.xml file to change the icon and colour of your push notifications.

<!-- Set icon used with incoming notification messages. This is used when no icon is set for the incoming notification message. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@mipmap/ic_launcher_round" />
<!-- Set color used with incoming notification messages. This is used when no color is set for the incoming notification message. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/colorAccent" />

Sessions

Starting a session

To receive push messages the SDK requires an active session.

To create a session you must have successfully initialized and retrieved a client object, you need to be able to identify the app user so that the sub claim in the JWT can be populated for the user; when the SDK requests the JWT in order to start the session. To create a session call:

client.service().session().startSession(new Callback<Session>(){/* implement */});
rxClient.service().session().startSession()
   .subscribe(new Observer<Session>(){/* implement */});

Ending a session

You only have to end a session if you want to stop the app receiving push notifications, or you want to change users on the app.

To end the current session, call:

client.service().session().endSession(
   new Callback<ComapiResult<Void>>(){/* implement */});
rxClient.service().session().endSession()
   .subscribe(new Observer<ComapiResult<Void>>(){/* implement */});

👍

Next steps

Now ensure your app passes an email address to the SDK for the app user to ensure they get a contact created in Dotdigital by following these instructions

📘

Want to know more about the SDK?

To find out more about the SDK and it features and functions please go here