The use case
I am working in a company here in Bulgaria where we are building a huge android app with over 250 screens. It was time to integrate Firebase push notifications in it. Here were the requirements for them:
- When the app is open notifications should
- Appear on some screens
- Not appear on other screens
- Some notifications may be handled by fragments or activities
- Each notification has a type
- Each type of notification leads to different screens
- Notifications received when the app is open should not be visible when the app is closed
- Some notifications with a certain type will get the text from the server, others will have their own
- If a notification is received, but the user does not click on it and he manually opens the appropriate screen to which the notification should follow, the notification should disappear
The idea
Our idea was to use a method very close how to how the Touch Framework in Android works. It is implemented using inheritance. You can try a different approach by using composition.
Our main goals were:
- Not have any casting or type checking of the notifications in the app
- Ask each Activity/Fragment if it wants to handle the notification or not
So this is what we do. We ask each activity/fragment if they want to handle the notification by using our NotificationListener interface, if not – we let our default notification handling work. For different types of notifications, we have different handle methods.
interface NotificationListener { boolean handleTicketNotification(TicketNotification notification); boolean handlePaymentNotification(PaymentNotification notification); }
Overall structure
- MyFirebaseMessagingService – our service which extends FirebaseMessagingService and listens for incoming notifications. It then broadcasts them through the LocalBroadcastManager
- BaseActivity – a class extended by all activities with the common logic of handling notifications
- NotificationListener – an interface which has methods returning boolean for each type of notification. Its return type is used to determine whether an activity/fragment wants to override the display of a notification or not
- NotificationManager – small class to handle listening to the types of notification coming from the MyFirebaseMessagingService
Fighting the problems
Helper class
We extracted the notification receiving logic in a small notification Manager class. It creates an intent filter for a given activity and subscribes to the incoming notifications using the LocalBroadcastManager:
public class NotificationManager {
private final Activity activity;
private BroadcastReceiver notificationsReceiver;
public NotificationManager(Activity activity) {
this.activity = activity;
}
public void start(NotificationListener notificationListener,
UnhandledNotificationListener onNotificationNotHandled) {
notificationsReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(Constant.NOTIFICATION)
&& Constant.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) {
BaseNotification notificationData = NotificationPaser.getNotificationData(intent.getExtras());
if(!notificationData.handleNotification(notificationListener)) {
notificationData.onNotificationNotHandled(onNotificationNotHandled, intent);
}
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(Constant.ACTION_NOTIFICATION_RECEIVED);
LocalBroadcastManager.getInstance(activity).registerReceiver(notificationsReceiver, filter);
}
public void stop() {
LocalBroadcastManager.getInstance(activity).unregisterReceiver(notificationsReceiver);
}
}
Notifications should appear on some screens and not on other screens
Activities
For activities, we have the BaseActivity class which is extended from each Activity. So it implements the NotificationListener interface and each activity which extends it, decides whether to override a given ticket method or not.
abstract class BaseActivity extends Activity implements NotificationListener {
@Override
void onStart() {
// UnhandledNotificationListener is the default handling if the handleMethods return false
notificationManager.start(this, new UnhandledNotificationListener() {
@Override
public void onTicketNotHandled(TicketNotification ticketNotification, Intent intent) {
displayTicketNotification(ticketNotification, intent);
}
@Override
public void onPaymentNotHandled(PaymentNotification paymentNotification, Intent intent) {
displayPaymentNotification(paymentNotification, intent);
}
}
}
@Override
public boolean handleTicketNotification(TicketNotification ticketNotification) {
return false;
}
@Override
public boolean handlePaymentNotification(PaymentNotification ticketNotification) {
return false;
}
}
Fragments
For fragments, we didn’t have such a base class as we had in the activities so we decided to let each fragment implement the NotificationListener interface. So we get the visible fragment in the activity, check if it an instance of a NotificationListener interface and pass the data to it by casting it.
class TabsActivity extends BaseActivity {
@Override
public boolean handlePaymentNotification(PaymentNotification paymentNotification) {
if (getNotificationListenerFragment() != null) {
return getNotificationListenerFragment().handlePaymentNotification(paymentNotification);
}
return false;
}
public NotificationListener getNotificationListenerFragment() {
NavigationController controller = ((NavigationController) tabsController.getVisibleFragment());
if (controller != null && controller.getVisibleFragment() instanceof NotificationListener) {
return (NotificationListener) controller.getVisibleFragment();
}
return null;
}
}
Different text for different notifications
So on some screens user should not see the text for a notification, on other screens he should see it. So if you receive a notification for the update of your bank account number, while you are logged out, you should not see the text (you should not receive this type of notification, but that is for another post).
We user the strategy pattern for this case. We have three different ways of building notification text: VisibleTextBuilder, HiddenTextBuilder, GenericTextBuilder. All of these are called in the BaseActivity when the notification is received and they extend the NotificationTextBuilder interface. Based on the notification type, we use a different type of text builder.
So our displayTicketNotification and displayPaymentNotification methods use one of the mentioned text builders to display a given notification. Each of these methods is public, so an Activity can modify the text, notification pending activity and the data of the notification.
protected void displayTicketNotification(TicketNotification ticketNotification, Intent intent) {
// Dagger used to inject the appropriate text builder
NotificationTextBuilder ticketNotificationTextBuilder = getComponent()
.plus(new NotificationModule(ticketNotification.getMessageId(), ticketNotification.isAuthenticatedMessage()))
.getNotificationTextBuilder();
CustomNotificationBuilder builder = CustomNotificationBuilder
.builder(ticketNotification, this)
.setTicketNotificationTextBuilder(ticketNotificationTextBuilder)
.setNotificationPendingActivity(ticketNotification.isAuthenticatedMessage() ? LoginSecurityCodeActivity.class : ContactActivity.class)
.setIntent(intent);
builder.showNotification(ticketNotification.getTicketId());
}
Navigation for different types
This is accomplished by Overriding the appropriate display****Notification method in each activity which wants to modify the screen which should open when the notification is received. Each activity works with the builder and builds its own notification.
Notification Types
Hierarchy
All of the received notifications have at least 3 common fields of data. That’s why we made the BaseNotification class. And by using Jackson, we can easily tell it what instance it should create based on the type of a certain field. The code below does the following:
- If the notificationType field equals “NEW_QUEUED PAYMENT” create an object of PaymentNotification class
- If the notificationType field equals “MESSAGE_RECEIVED” create an object of TicketNotification class
- Otherwise create an object of UnknownNotification class
PaymentNotification, TicketNotification and UnknownNotification all extend from the BaseNotification class.
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
// if the field contains unknown value
defaultImpl = UnknownNotification.class,
property = "notificationType",
// add the notificationType to the class, otherwise it won't
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = PaymentNotification.class, name = "NEW_QUEUED_PAYMENT"),
@JsonSubTypes.Type(value = TicketNotification.class, name = "MESSAGE_RECEIVED")
})
public abstract class BaseNotification implements Serializable {
private NotificationType notificationType;
private String title, body, sound;
public abstract boolean handleNotification(NotificationListener notificationListener);
public abstract void onNotificationNotHandled(UnhandledNotificationListener onNotificationNotHandled, Intent intent);
}
Escaping casting
To escape the casting we let the notification objects themselves to work with the NotificationListener object. This way we can pass an object of BaseNotification to the activity and let the activity pass itself to it. So we won’t cast to Payment or Ticket notification, we just call BaseNotification.handleNotification(this) method in the activity and escape the casting problem. Each subclass of BaseNotification decides how and when to call the notification listener with the parsed data.
public class TicketNotification extends BaseNotification implements Serializable {
private boolean authenticatedMessage;
private String ticketId, messageId;
private int unreadMessageCount;
@Override
public boolean handleNotification(NotificationListener notificationListener) {
return notificationListener.handleTicketNotification(this);
}
@Override
public void onNotificationNotHandled(UnhandledNotificationListener onNotificationNotHandled, Intent intent) {
onNotificationNotHandled.onTicketNotHandled(this, intent);
}
}
Close notifications which were not clicked
So if a user opens a screen for which he sees a notification, but doesn’t tap on it, the notification should disappear. The simple solution was to set a notificationId the same as the ticketId, so when the user opens the TicketDetails screen for a certain ticket, the screen calls notificationManager.cancel(ticketId) and the notification for this ticket is removed. Simple as that.
Remove notifications when app is closed
To do that you need to register an ActivityLifecycleCallback. All you need to do is call: registerActivityLifecycleCallbacks(new LogoutLifecycleListener(this)); in your custom application class. So then:
public class LogoutLifecycleListener implements Application.ActivityLifecycleCallbacks { @Override public void onActivityPaused(Activity activity) { NotificationManager nMgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); nMgr.cancelAll(); } }
Closing words
In the current project I work on, we use inheritance a lot, which I really don’t like. But I think the approach we took to handling notifications, in this case, does its job really well. You can easily extend certain methods and now work with the notification yourself. I would prefer to use composition and an approach closer to OOP so I can delegate most of the work to the objects rather than to use inheritance, but this is how the touch framework in Android works and it does it’s job well.
I would be very happy if someone shares an alternative to our approach. I hope that this article helped someone to handle notifications in their project. Cheers!