Android - Implementing 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.
- Kotlin
- Java
class MainActivity : AppCompatActivity() {
private val notificationsPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (!granted) {
// User denied notifications permissions
return@registerForActivityResult
}
// You can enable remote notifications
lifecycleScope.launch {
try {
Notificare.push().enableRemoteNotifications()
} catch (e: Exception) {
// ...
}
}
}
private fun onEnableRemoteNotificationsClicked() {
// Ensure we have sufficient permissions
if (!ensureNotificationsPermission()) return
// We have sufficient permissions to enable remote notifications
lifecycleScope.launch {
try {
Notificare.push().enableRemoteNotifications()
} catch (e: Exception) {
// ...
}
}
}
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(new NotificareCallback<Unit>() {
@Override
public void onSuccess(Unit result) {
}
@Override
public void onFailure(@NonNull Exception e) {
}
});
}
);
private void onEnableRemoteNotificationsClicked() {
// Ensure we have sufficient permissions
if (!ensureNotificationsPermission()) return;
// We have sufficient permissions to enable remote notifications
NotificarePushCompat.enableRemoteNotifications(new NotificareCallback<Unit>() {
@Override
public void onSuccess(Unit result) {
}
@Override
public void onFailure(@NonNull Exception e) {
}
});
}
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.
- Kotlin
- Java
// Check if the user has previously enabled remote notifications.
Notificare.push().hasRemoteNotificationsEnabled
NotificarePushCompat.enableRemoteNotifications(new NotificareCallback<Unit>() {
@Override
public void onSuccess(Unit result) {
}
@Override
public void onFailure(@NonNull Exception e) {
}
});
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.
- Kotlin
- Java
// 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.
- Kotlin
- Java
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>
Disabling remote notifications
Disabling remote notifications can be achieved in the same fashion as enabling them.
- Kotlin
- Java
Notificare.push().disableRemoteNotifications()
NotificarePushCompat.disableRemoteNotifications(new NotificareCallback<Unit>() {
@Override
public void onSuccess(Unit result) {
}
@Override
public void onFailure(@NonNull Exception e) {
}
});
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.
- Kotlin
- Java
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.
- Kotlin
- Java
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.
- Kotlin
- Java
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.
- Kotlin
- Java
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) {
if (NotificarePushCompat.handleTrampolineIntent(intent)) return;
Notificare.push().parseNotificationOpenedIntent(intent)?.also { result ->
Notificare.pushUI().presentNotification(this, result.notification)
return
}
Notificare.push().parseNotificationActionOpenedIntent(intent)?.also { result ->
Notificare.pushUI().presentAction(this, result.notification, result.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 (NotificarePushCompat.handleTrampolineIntent(intent)) return;
NotificareNotificationOpenedIntentResult notificationOpenedIntentResult = NotificarePushCompat.parseNotificationOpenedIntent(intent);
if (notificationOpenedIntentResult != null) {
NotificarePushUICompat.presentNotification(
this,
notificationOpenedIntentResult.getNotification()
);
}
NotificareNotificationActionOpenedIntentResult notificationActionOpenedIntentResult = NotificarePushCompat.parseNotificationActionOpenedIntent(intent);
if (notificationActionOpenedIntentResult != null) {
NotificarePushUICompat.presentAction(
this,
notificationActionOpenedIntentResult.getNotification(),
notificationActionOpenedIntentResult.getAction()
);
}
// 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.
- Kotlin
- Java
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
) {
}