SDK

Remote Notifications

In this page you'll learn how notifications are handled in your app and what are all the options at your disposal to create a great messaging experience for your users.

Notificare supports several types of interactive and actionable notifications that will be handled for you without any extra development. If you are going to prevent this default behaviour, please note that you will have to either handle all the functionality yourself (metrics logging, presenting UI or collect replies) or if you don't, you understand that some features will not work as advertised.

Requesting Permission

Since Android 13, the notification permission is not granted by default and should be requested. We recommended targeting Android 13 to have more control over the request.

When running Android 13 and targeting Android 12 or lower, users will be prompted for the permission when the notification channel is created. Typically, when the application starts.

Although permission requests must follow a recommended standard, controlling the overall experience is something unique to each application. However, you can use the following code as inspiration for your implementation.

class MainActivity : AppCompatActivity() {

    private val notificationsPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (!granted) {
            // User denied notifications permissions
            return@registerForActivityResult
        }

        // You can enable remote notifications
        Notificare.push().enableRemoteNotifications()
    }

    private fun onEnableRemoteNotificationsClicked() {
        // Ensure we have sufficient permissions
        if (!ensureNotificationsPermission()) return

        // We have sufficient permissions to enable remote notifications
        Notificare.push().enableRemoteNotifications()
    }

    private fun ensureNotificationsPermission(): Boolean {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true

        val permission = android.Manifest.permission.POST_NOTIFICATIONS
        val granted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
        if (granted) return true

        if (shouldShowRequestPermissionRationale(permission)) {
            AlertDialog.Builder(this)
                .setTitle(R.string.app_name)
                .setMessage(R.string.notifications_permission_rationale)
                .setPositiveButton(android.R.string.ok) { _, _ ->
                    notificationsPermissionLauncher.launch(permission)
                }
                .show()

            return false
        }

        notificationsPermissionLauncher.launch(permission)
        return false
    }
}
public class MainActivity extends AppCompatActivity {

    private final ActivityResultLauncher<String> notificationsPermissionLauncher = registerForActivityResult(
        new ActivityResultContracts.RequestPermission(), granted -> {
            if (!granted) {
                // User denied notifications permissions
                return;
            }

            // You can enable remote notifications
            NotificarePushCompat.enableRemoteNotifications();
        }
    );

    private void onEnableRemoteNotificationsClicked() {
        // Ensure we have sufficient permissions
        if (!ensureNotificationsPermission()) return;

        // We have sufficient permissions to enable remote notifications
        NotificarePushCompat.enableRemoteNotifications();
    }

    private boolean ensureNotificationsPermission() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true;

        String permission = android.Manifest.permission.POST_NOTIFICATIONS;
        boolean granted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED;
        if (granted) return true;

        if (shouldShowRequestPermissionRationale(permission)) {
            new AlertDialog.Builder(this)
                .setTitle(R.string.app_name)
                .setMessage(R.string.notifications_permission_rationale)
                .setPositiveButton(android.R.string.ok, (dialog, which) -> {
                    notificationsPermissionLauncher.launch(permission);
                })
                .show();

            return false;
        }

        notificationsPermissionLauncher.launch(permission);
        return false;
    }
}

Enabling Notifications

In order to enable the device to receive notifications, all that you need to do is invoke a single method.

Notificare.push().enableRemoteNotifications()
NotificarePushCompat.enableRemoteNotifications();

Typically, the step above is done during some form of user onboarding. When the user already went through that flow, we automatically enable notifications when Notificare launches.

You can also check whether the user enrolled on remote notifications.

// Check if the user has previously enabled remote notifications.
Notificare.push().hasRemoteNotificationsEnabled
// Check if the user has previously enabled remote notifications.
NotificarePushCompat.getHasRemoteNotificationsEnabled();

Additionally, you can check if the user has disabled notifications in the System Settings.

Notificare.push().allowedUI
NotificarePushCompat.getAllowedUI();

If you want to also give access to your app's settings view from the notification settings in the device's settings, you can listen to the NOTIFICATION_PREFERENCES intent. This will add a link to your activity inside the device's settings.

<activity android:name=".SettingsActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.NOTIFICATION_PREFERENCES" />
    </intent-filter>
</activity>

notification settings link

Disabling remote notifications

Disabling remote notifications can be achieved in the same fashion as enabling them.

Notificare.push().disableRemoteNotifications()
NotificarePushCompat.disableRemoteNotifications();

When this method is called, we will automatically register your device to never receive remote notifications, although you will still maintain the same user profile, inbox messages and enjoy all the other services your plan supports. You can at anytime request a re-register for push notifications if you wish to.

Receiving Notifications

Due to the changes in Android 12 when it comes to handling notifications, your Activity needs to become the trampoline. Assuming you will let your MainActivity receive the notification intents, you need to declare the following intent-filter it in your AndroidManifest.xml.

<activity android:name=".MainActivity" android:launchMode="singleTask">
    <intent-filter>
        <action android:name="re.notifica.intent.action.RemoteMessageOpened" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

You should configure the Activity receiving these intents with android:launchMode="singleTask" to prevent recreating it several times when processing the series of intents fired from opening a notification from the notifications drawer. You can also use android:launchMode="singleTop", but be aware the OS will recreate it when processing deep links triggered from the NotificationActivity. For more information on launch modes, refer to the Android documentation.

Additionally, in order to process those intents, you need to take care of them in your MainActivity as shown below.

override fun onCreate(savedInstanceState: Bundle?) {
    // more code ...

    if (intent != null) handleIntent(intent)
}

override fun onNewIntent(intent: Intent?) {
    // more code ...

    if (intent != null) handleIntent(intent)
}

private fun handleIntent(intent: Intent) {
    if (Notificare.push().handleTrampolineIntent(intent)) {
        Log.d(TAG, "Trampoline intent handled.")
        return
    }

    // more code ...
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    // more code ...

    if (getIntent() != null) handleIntent(getIntent());
}

@Override
protected void onNewIntent(Intent intent) {
    // more code ...

    if (intent != null) handleIntent(intent);
}

private void handleIntent(Intent intent) {
    if (NotificarePushCompat.handleTrampolineIntent(intent)) {
        Log.d(TAG, "Trampoline intent handled.");
        return;
    }

    // more code ...
}

Listening to Received Notifications

Once you're receiving notifications in your app, we can dive deeper and fully understand how they are handled. Our library takes care of placing a notification in the Notification Center so developers will not need to take any action in order to display notifications. If you want to be notified incoming notifications in your own Intent Receiver, for example to add a badge to your application launcher icon, you can leverage the NotificarePushIntentReceiver.

Start by creating a subclass of that Intent Receiver and overriding the onNotificationReceived() method.

class CustomPushIntentReceiver: NotificarePushIntentReceiver() {
    override fun onNotificationReceived(
        context: Context,
        notification: NotificareNotification,
        deliveryMechanism: NotificareNotificationDeliveryMechanism
    ) {
        // more code ...
    }
}
public class CustomPushIntentReceiver extends NotificarePushIntentReceiver {
    @Override
    protected void onNotificationReceived(
            @NonNull Context context,
            @NonNull NotificareNotification notification,
            @NonNull NotificareNotificationDeliveryMechanism deliveryMechanism
    ) {
        // more code ...
    }
}

In order to receive those intents in your Intent Receiver you need to let Notificare know about your class.

Notificare.push().intentReceiver = CustomPushIntentReceiver::class.java
NotificarePushCompat.setIntentReceiver(CustomPushIntentReceiver.class);

Lastly, declare your custom intent receiver in your AndroidManifest.xml.

<receiver
    android:name=".CustomPushIntentReceiver"
    android:exported="false" />

Presenting notifications

A notification can be opened by either tapping the actual notification or by tapping an action inside the notification. We will emit an intent in each case to an Activity that declares the appropriate intent-filters. To receive those intents, add the following to your MainActivity in the AndroidManifest.xml.

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="re.notifica.intent.action.NotificationOpened" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    <intent-filter>
        <action android:name="re.notifica.intent.action.ActionOpened" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

To handle the intents above, you can take the managed approach and use our NotificarePushUI module which takes care of all the events and UI types as well as actions, or you can fully take over and present them however you prefer. However, be aware if you take the non-managed approach as you will have to deal with all aspects and types of presenting the notifications, including the events needed to show the user's engagement in our Dashboard.

The code below illustrates how this works when using the managed approach.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // more code ...

    if (intent != null) handleIntent(intent)
}

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    // more code ...

    if (intent != null) handleIntent(intent)
}

private fun handleIntent(intent: Intent) {
    when (intent.action) {
        Notificare.INTENT_ACTION_NOTIFICATION_OPENED -> {
            val notification: NotificareNotification = requireNotNull(
                intent.getParcelableExtra(Notificare.INTENT_EXTRA_NOTIFICATION)
            )

            Notificare.pushUI().presentNotification(this, notification)
            return
        }
        Notificare.INTENT_ACTION_ACTION_OPENED -> {
            val notification: NotificareNotification = requireNotNull(
                intent.getParcelableExtra(Notificare.INTENT_EXTRA_NOTIFICATION)
            )

            val action: NotificareNotification.Action = requireNotNull(
                intent.getParcelableExtra(Notificare.INTENT_EXTRA_ACTION)
            )

            Notificare.pushUI().presentAction(this, notification, action)
            return
        }
    }

    // more code ...
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // more code ...

    if (getIntent() != null) handleIntent(getIntent());
}

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    // more code ...

    if (intent != null) handleIntent(intent);
}

private void handleIntent(Intent intent) {
    if (intent.getAction().equals(NotificarePushCompat.INTENT_ACTION_NOTIFICATION_OPENED)) {
        NotificareNotification notification = Objects.requireNonNull(
                intent.getParcelableExtra(Notificare.INTENT_EXTRA_NOTIFICATION)
        );

        NotificarePushUICompat.presentNotification(this, notification);
    } else if (intent.getAction().equals(NotificarePushCompat.INTENT_ACTION_ACTION_OPENED)) {
        NotificareNotification notification = Objects.requireNonNull(
                intent.getParcelableExtra(Notificare.INTENT_EXTRA_NOTIFICATION)
        );

        NotificareNotification.Action action = Objects.requireNonNull(
                intent.getParcelableExtra(Notificare.INTENT_EXTRA_ACTION)
        );

        NotificarePushUICompat.presentAction(this, notification, action);
    }

    // more code ...
}

Additionally, when using the managed approach, you can listen to Notification lifecycle events and perform any additional steps you may require. Let your Activity implement the NotificarePushUI.NotificationLifecycleListener and add following methods as needed.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // more code ...

    Notificare.pushUI().addLifecycleListener(this)
}

override fun onDestroy() {
    super.onDestroy()
    // more code ...

    Notificare.pushUI().removeLifecycleListener(this)
}

override fun onNotificationWillPresent(notification: NotificareNotification) {

}

override fun onNotificationPresented(notification: NotificareNotification) {

}

override fun onNotificationFinishedPresenting(notification: NotificareNotification) {

}

override fun onNotificationFailedToPresent(notification: NotificareNotification) {

}

override fun onNotificationUrlClicked(notification: NotificareNotification, uri: Uri) {

}

override fun onActionWillExecute(notification: NotificareNotification, action: NotificareNotification.Action) {

}

override fun onActionExecuted(notification: NotificareNotification, action: NotificareNotification.Action) {

}

override fun onActionFailedToExecute(
    notification: NotificareNotification,
    action: NotificareNotification.Action,
    error: Exception?
) {

}

override fun onCustomActionReceived(
    notification: NotificareNotification,
    action: NotificareNotification.Action,
    uri: Uri,
) {

}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // more code ...

    NotificarePushUICompat.addLifecycleListener(this);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    // more code ...

    NotificarePushUICompat.removeLifecycleListener(this);
}

@Override
public void onPointerCaptureChanged(boolean hasCapture) {

}

@Override
public void onNotificationWillPresent(@NonNull NotificareNotification notification) {

}

@Override
public void onNotificationPresented(@NonNull NotificareNotification notification) {

}

@Override
public void onNotificationFinishedPresenting(@NonNull NotificareNotification notification) {

}

@Override
public void onNotificationFailedToPresent(@NonNull NotificareNotification notification) {

}

@Override
public void onNotificationUrlClicked(@NonNull NotificareNotification notification, @NonNull Uri uri) {

}

@Override
public void onActionWillExecute(@NonNull NotificareNotification notification, @NonNull NotificareNotification.Action action) {

}

@Override
public void onActionExecuted(@NonNull NotificareNotification notification, @NonNull NotificareNotification.Action action) {

}

@Override
public void onActionFailedToExecute(
        @NonNull NotificareNotification notification,
        @NonNull NotificareNotification.Action action,
        @Nullable Exception error
) {

}

@Override
public void onCustomActionReceived(
        @NonNull NotificareNotification notification,
        @NonNull NotificareNotification.Action action,
        @NonNull Uri uri
) {

}

In modern apps, this is a great way of creating interactions between notifications, and your own app content, allowing you to send messages that can eventually drive the user to open content hidden deeper in your app.

To prepare your app to handle deep links is not complicated and will allow you to handle not only Deep Link notification types, but also any link made from a web page. In order to indicate your app that you will handle a custom URL scheme you simply have to declare the following in the AndroidManifest.xml and be sure to update the scheme to match yours:

<activity android:name=".DeeplinkActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="com.example" />
    </intent-filter>
</activity>

By simply providing a data entry in your activity's intent-filter element, any click in a link from a web page using com.example://example.com/product?id=1 as the href value, would simply trigger your app to open.

If you are planning to handle Dynamic Links, you must also add all the domain prefixes you've created:

<activity android:name=".DeeplinkActivity">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:host="example.ntc.re" android:scheme="https"/>
    </intent-filter>
</activity>

But of course you will want to show something to the user based on that URL. That is also easily done by implementing the following in the activity you've declared to use a URL scheme:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // more code ...

    if (intent != null) handleIntent(intent)
}

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    // more code ...

    if (intent != null) handleIntent(intent)
}

private fun handleIntent(intent: Intent) {
    // more code ...

    if (Notificare.handleDynamicLinkIntent(this, intent)) {
        Log.d(TAG, "Dynamic link handled.")
        return
    }

    val uri = intent.data
    if (uri != null) {
        // Open a view based on the received URI.
    }
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // more code ...

    if (getIntent() != null) handleIntent(getIntent());
}

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    // more code ...

    if (intent != null) handleIntent(intent);
}

private void handleIntent(Intent intent) {
    // more code ...

    if (Notificare.handleDynamicLinkIntent(this, intent)) {
        Log.d(TAG, "Dynamic link handled.");
        return;
    }

    Uri uri = intent.getData();
    if (uri != null) {
        // Open a view based on the received URI.
    }
}

If you need to open external deep links in your notifications, you need to add the appropriate entries to your AndroidManifest.xml for those links to work in Android 11, for example to handle HTTP and HTTPS links, you would need to add:

<queries>
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" />
    </intent>
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="http" />
    </intent>
</queries>

On top of Dynamic Links, you can add support for deferred links. This type of dynamic link survives the App Store installation process and can be handled the first time the application is opened. Since this is an opt-in feature, you have to make changes to your application as described below. Once the deferred link is evaluated, Notificare will trigger an intent to your application with the resolved deep link.

class DeeplinkActivity : AppCompatActivity()
    override fun onCreate(savedInstanceState: Bundle?) {
        // more code ...

        lifecycleScope.launch {
            try {
                val canEvaluateDeferredLink = Notificare.canEvaluateDeferredLink()
                if (!canEvaluateDeferredLink) return@launch

                val evaluated = Notificare.evaluateDeferredLink()
                // The deferred link was successfully handled.
            } catch (e: Exception) {
                // Something went wrong.
            }
        }
    }
}
public class DeeplinkActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // more code ...

        Notificare.canEvaluateDeferredLink(new NotificareCallback<Boolean>() {
            @Override
            public void onSuccess(Boolean canEvaluateDeferredLink) {
                if (!canEvaluateDeferredLink) return;

                Notificare.evaluateDeferredLink(new NotificareCallback<Boolean>() {
                    @Override
                    public void onSuccess(Boolean evaluated) {
                        // The deferred link was successfully handled.
                    }

                    @Override
                    public void onFailure(@NonNull Exception e) {
                        // Something went wrong.
                    }
                });
            }

            @Override
            public void onFailure(@NonNull Exception e) {
                // Something went wrong.
            }
        });
    }
}

Another common example where you will make use of deep links, would be to take proper action whenever users click in hypertext links in a Notificare's HTML or Web Page notification type. This is done by first declaring all the URL schemes that you want to handle in your AndroidManifest.xml:

<meta-data
    android:name="re.notifica.push.ui.notification_url_schemes"
    android:resource="@array/notification_url_schemes" />

This entry will require you to create a app/res/values/notification_url_schemes.xml file with the following content:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="notification_url_schemes">
        <item>com.example</item>
        <item>com.example2</item>
        <item>com.example3</item>
    </string-array>
</resources>

Any click in an HTML or Web Page notification type, would be intercepted by our library and trigger the method onNotificationUrlClicked() from NotificarePushUI.NotificationLifecycleListener.

Notifications from Unknown Sources

In some apps it is possible you're also using other providers to send remote notifications, when that is the case Notificare will recognize an unknown notification and trigger an intent that you can use to further handle that notification. To be notified whenever that happens, implement the following method of the NotificarePushIntentReceiver.

class CustomPushIntentReceiver: NotificarePushIntentReceiver() {
    override fun onUnknownNotificationReceived(context: Context, notification: NotificareUnknownNotification) {
        // Handle the unknown notification.
    }
}
public class CustomPushIntentReceiver extends NotificarePushIntentReceiver {
    @Override
    protected void onUnknownNotificationReceived(@NonNull Context context, @NonNull NotificareUnknownNotification notification) {
        // Handle the unknown notification.
    }
}

In order to receive the events in your class, make sure you declare your custom receiver as explained the listening to received notifications section.

Live Activities

The Live Activity utilities provide you a way to emulate the iOS functionality. You can build any kind of UI (Widget, notification view, etc.), register that activity in Notificare and receive remote content updates.

The Live Activity functionality is only available from version 3.5.0 and up.

Registering an activity

You can register an activity by calling registerLiveActivity(). An activityId uniquely identifies an on-going activity, while the optional topics act as a tagging mechanism. You can target the activities based on both properties through our API.

Notificare.push().registerLiveActivity(
    activityId = "order-tracker",
    topics = listOf("sample"),
    callback = object : NotificareCallback<Unit> {
        override fun onSuccess(result: Unit) {

        }

        override fun onFailure(e: Exception) {

        }
    }
)
NotificarePushCompat.registerLiveActivity("order-tracker", Arrays.asList("sample"), new NotificareCallback<Unit>() {
    @Override
    public void onSuccess(Unit result) {

    }

    @Override
    public void onFailure(@NonNull Exception e) {

    }
});

Ending an activity

It's part of your app's responsibility to notify Notificare that a given activity has ended. When you decide to stop it or it has been dismissed by the user, you should call endLiveActivity().

Notificare.push().endLiveActivity(
    activityId = "order-tracker",
    callback = object : NotificareCallback<Unit> {
        override fun onSuccess(result: Unit) {

        }

        override fun onFailure(e: Exception) {

        }
    }
)
NotificarePushCompat.endLiveActivity("order-tracker", new NotificareCallback<Unit>() {
    @Override
    public void onSuccess(Unit result) {

    }

    @Override
    public void onFailure(@NonNull Exception e) {

    }
});

Listening to activity updates

While iOS automatically updates the associated UI, we must do this manually since there is no dedicated OS mechanism.

Once you have configured the Notificare.push().intentReceiver to your instance, you can listen to the onLiveActivityUpdate() event. At this point, you can act on the update and adjust your UI accordingly.

class CustomPushReceiver : NotificarePushIntentReceiver() {

    override fun onLiveActivityUpdate(context: Context, update: NotificareLiveActivityUpdate) {
        // update your UI
    }

}
public class CustomPushReceiver extends NotificarePushIntentReceiver {

    @Override
    protected void onLiveActivityUpdate(@NonNull Context context, @NonNull NotificareLiveActivityUpdate update) {
        // update your UI
    }

}

Listening to token changes

Every now and then, Firebase will refresh the push token associated with your device. At this point, you must re-register any on-going Live Activities to continue receiving updates.

class CustomPushReceiver : NotificarePushIntentReceiver() {

    override fun onTokenChanged(context: Context, token: String) {
        // register the on-going activities again
    }

}
public class CustomPushReceiver extends NotificarePushIntentReceiver {

    @Override
    protected void onTokenChanged(@NonNull Context context, @NonNull String token) {
        // register the on-going activities again
    }

}