Push messages

The SDK automatically registers your app to receive push messages through FCM.

Should you need access to the raw push messages — for example to run custom logic over the data payload — you can obtain them by either registering a PushMessageListener when initialising the SDK, or by reading the extras placed on the launcher Activity's intent when the user taps a system-tray notification.

App stateNotificationDataBoth
ForegroundPushMessageListenerPushMessageListenerPushMessageListener
BackgroundSystem trayPushMessageListenerNotification: system tray; Data: in extras of the intent.

PushMessageListener redirects messages from the FirebaseMessagingService that the SDK already registers internally — you do not need to (and should not) declare your own FirebaseMessagingService for the same intent filter, as that would replace the SDK's one.

Learn more in the Firebase documentation.

🚧

Android 13+ (API 33) requires POST_NOTIFICATIONS

The SDK declares android.permission.POST_NOTIFICATIONS in its manifest, but on Android 13 and above your app must also request it at runtime before any notification will be shown. The OS silently drops notifications for apps that have not been granted the permission.

📘

Deep links

If a push notification is delivered while the app is in the foreground, you can build your own deep link in the onMessageReceived() method that creates a link to a particular activity in your app.

Implementing PushMessageListener

public class PushHandler implements PushMessageListener {

    private static final String CHANNEL_ID = "comapi_default_channel";

    private final Context context;

    public PushHandler(Context context) {
        this.context = context.getApplicationContext();
    }

    @Override
    public void onMessageReceived(RemoteMessage message) {
        RemoteMessage.Notification notification = message.getNotification();
        if (notification == null) {
            // Data-only payload: handle silently or post your own notification.
            return;
        }

        String title = notification.getTitle();
        String body = notification.getBody();
        Log.i("ComapiPush", "Push notification: " + body);

        createNotificationChannel();

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
                .setAutoCancel(true)
                .setDefaults(NotificationCompat.DEFAULT_ALL)
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle(title)
                .setContentText(body);

        NotificationManagerCompat manager = NotificationManagerCompat.from(context);
        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
                == PackageManager.PERMISSION_GRANTED) {
            manager.notify(1, builder.build());
        }
    }

    private void createNotificationChannel() {
        // Notification channels were introduced in API 26. The SDK's minSdk is 16,
        // so the version check is still required.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_ID,
                    "Default",
                    NotificationManager.IMPORTANCE_DEFAULT);
            channel.setDescription("Default push notifications");

            NotificationManager nm = context.getSystemService(NotificationManager.class);
            if (nm != null) {
                nm.createNotificationChannel(channel);
            }
        }
    }
}
class PushHandler(context: Context) : PushMessageListener {

    private val appContext = context.applicationContext

    override fun onMessageReceived(message: RemoteMessage) {
        val notification = message.notification ?: return // data-only payload

        Log.i("ComapiPush", "Push notification: ${notification.body}")

        createNotificationChannel()

        val builder = NotificationCompat.Builder(appContext, CHANNEL_ID)
            .setAutoCancel(true)
            .setDefaults(NotificationCompat.DEFAULT_ALL)
            .setWhen(System.currentTimeMillis())
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(notification.title)
            .setContentText(notification.body)

        if (ActivityCompat.checkSelfPermission(appContext, Manifest.permission.POST_NOTIFICATIONS)
            == PackageManager.PERMISSION_GRANTED
        ) {
            NotificationManagerCompat.from(appContext).notify(1, builder.build())
        }
    }

    private fun createNotificationChannel() {
        // Notification channels were introduced in API 26. The SDK's minSdk is 16,
        // so the version check is still required.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "Default",
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply { description = "Default push notifications" }

            appContext.getSystemService(NotificationManager::class.java)
                ?.createNotificationChannel(channel)
        }
    }

    companion object {
        private const val CHANNEL_ID = "comapi_default_channel"
    }
}

Pass your class to pushMessageListener() on the ComapiConfig object:

config.pushMessageListener(new PushHandler(getApplicationContext()));
config.pushMessageListener(PushHandler(applicationContext))

Requesting the runtime notification permission (Android 13+)

private final ActivityResultLauncher<String> requestNotificationPermission =
        registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
            // granted == true means the user accepted the prompt
        });

private void ensureNotificationPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
            && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
                != PackageManager.PERMISSION_GRANTED) {
        requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS);
    }
}
private val requestNotificationPermission =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { /* granted */ }

private fun ensureNotificationPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
        ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
        != PackageManager.PERMISSION_GRANTED
    ) {
        requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
    }
}

Reading the data when the user taps a system-tray notification

When the app is in the background and a notification payload is shown in the system tray, FCM doesn't deliver the message to PushMessageListener — it opens the launcher Activity and places the data fields in the Activity intent's extras. The SDK exposes a helper, handlePushNotification(...), which reads those extras, optionally records a click for analytics, and optionally launches the deep link.

Call it from Activity.onCreate(...) (and onNewIntent(...) if the Activity uses singleTop/singleTask) on the intent supplied by the system.

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    handleIfFromPush(getIntent());
}

@Override
protected void onNewIntent(@NonNull Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent);
    handleIfFromPush(intent);
}

private void handleIfFromPush(@NonNull Intent intent) {
    ComapiClient client = /* the client instance returned at init */;
    client.handlePushNotification(this, intent, /* startActivity = */ true, result -> {
        if (result == null) return;
        String deepLinkUrl = result.getUrl();          // null if the payload had no deep link
        JSONObject customData = result.getData();      // null if the payload had no `dd_data`
        boolean clickTracked = result.isClickRecorded();
        boolean deepLinkLaunched = result.isDeepLinkCalled();
        // Update UI / route within the app as appropriate
    });
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    handleIfFromPush(intent)
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)
    handleIfFromPush(intent)
}

private fun handleIfFromPush(intent: Intent) {
    val client: ComapiClient = /* the client instance returned at init */
    client.handlePushNotification(this, intent, /* startActivity = */ true) { result ->
        result ?: return@handlePushNotification
        val deepLinkUrl: String? = result.url       // null if no deep link
        val customData: JSONObject? = result.data   // null if no `dd_data`
        val clickTracked: Boolean = result.isClickRecorded
        val deepLinkLaunched: Boolean = result.isDeepLinkCalled
        // Update UI / route within the app as appropriate
    }
}
📘

What handlePushNotification does

The helper checks the launcher intent for the SDK's two well-known extras — dd_deepLink (containing a url and optional trackingUrl) and dd_data. If a deep link is found and startActivity is true, it fires an ACTION_VIEW intent for that URL. If a trackingUrl is present it also records the click against the Dotdigital analytics endpoint.

Parsing a RemoteMessage directly

If you receive the message in the foreground via PushMessageListener and want the same deep-link / custom-data extraction without firing any intents, use the static helper:

try {
    PushDetails details = ComapiClient.parsePushMessage(message);
    String deepLinkUrl = details.getUrl();
    JSONObject customData = details.getData();
} catch (JSONException e) {
    // payload was malformed
}
try {
    val details = ComapiClient.parsePushMessage(message)
    val deepLinkUrl: String? = details.url
    val customData: JSONObject? = details.data
} catch (e: JSONException) {
    // payload was malformed
}