I was trying to implement FCM notification for a chat app and I want to achieve \"WhatsApp-like\" notification where the notification will be grouped by conversation.
<You can use the following code to group the notification. But I didn't implement the reply buttons because I'm using an Android Web view application. I've used 2 types of notification grouping based on the Android versions because of android nougat and later versions automatically group the notification based on the group id but Marshmallow and earlier will not group the notifications.
My Firebase message handling service like follows:
import static com.packageName.config.AppConstant.MY_NOTIFICATION;
public class MyFireBaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "MyFireBaseService";
private static final int SUMMARY_ID = 999;
@Override
public void onNewToken(String refreshedToken) {
super.onNewToken(refreshedToken);
//Store fcm token to shared preferences
SharedPrefManager.getInstance(getApplicationContext()).setFCMToken(refreshedToken);
}
@Override
public void onCreate() {
super.onCreate();
}
/* Data messages should be in the form of
* {
* type(Required) : "NotificationDTO type"
* title(Required) : "NotificationDTO title"
* message(Required) : "Message to be displayed in the notification panel"
* notificationURL(Required) : "Url to be loaded into the web view"
* groupId(Optional) : "Based on this group id, system will group the notification"
* channelId(optional) : "This channel id will be used to send notification"
* image(optional) : "This image will be displayed on notification panel"
* label(optional) : "NotificationDTO label"
* priority(optional) : "NotificationDTO priority. If notification priority not mentioned,
* Then default priority will be assigned to the notification"
* }
*/
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
boolean isForeGround = false;
super.onMessageReceived(remoteMessage);
// Fetching data part from the notification
Map<String, String> data = remoteMessage.getData();
String message = data.get("message");
String id = data.get("notificationId");
int notificationId;
// If notification id is empty then no need to show a notification
if (id == null || id.isEmpty()) {
return;
} else {
notificationId = Integer.parseInt(id);
}
if (message == null || message.equals("")) {
message = getString(R.string.default_notification_message);
}
String notificationURL = data.get("notificationURL");
String title = data.get("title");
// Group id should be a string
String groupKey = AppConstant.GROUP_KEY_NOTIFICATION;
if (data.get("groupKey") != null) {
groupKey = data.get("groupKey");
}
// Current we have only one channel with id `general_notification_id`
String channelId = data.get("channelId");
String label = data.get("label");
String image = data.get("image");
/*
* Notification priority(String Value) should be one of the following
* PRIORITY_HIGH/PRIORITY_LOW/PRIORITY_MAX/PRIORITY_MIN
* If no priority mentioned, system will automatically assign the default priority
*/
String priority = data.get("priority");
int notificationPriority = 0;
if (priority != null && !priority.isEmpty()) {
priority = priority.toUpperCase();
switch (priority) {
case "PRIORITY_HIGH":
notificationPriority = NotificationCompat.PRIORITY_HIGH;
break;
case "PRIORITY_LOW":
notificationPriority = NotificationCompat.PRIORITY_LOW;
break;
case "PRIORITY_MAX":
notificationPriority = NotificationCompat.PRIORITY_MAX;
break;
case "PRIORITY_MIN":
notificationPriority = NotificationCompat.PRIORITY_MIN;
break;
default:
notificationPriority = NotificationCompat.PRIORITY_DEFAULT;
break;
}
}
/*
* Category should be from the following list.
* Because system will sort the notification based on the category.
*
* CATEGORY_ALARM,CATEGORY_CALL,CATEGORY_MESSAGE,CATEGORY_EMAIL,CATEGORY_EVENT,
* CATEGORY_PROMO,CATEGORY_ALARM,CATEGORY_PROGRESS,CATEGORY_SOCIAL,CATEGORY_ERROR,
* CATEGORY_TRANSPORT,CATEGORY_SYSTEM,CATEGORY_SERVICE,CATEGORY_REMINDER,
* CATEGORY_RECOMMENDATION,CATEGORY_STATUS
*/
String category = data.get("category");
String notificationCategory = "";
if (category != null && !category.isEmpty()) {
category = category.toUpperCase();
switch (category) {
case "CATEGORY_ALARM":
notificationCategory = NotificationCompat.CATEGORY_ALARM;
break;
case "CATEGORY_CALL":
notificationCategory = NotificationCompat.CATEGORY_CALL;
break;
case "CATEGORY_MESSAGE":
notificationCategory = NotificationCompat.CATEGORY_MESSAGE;
break;
case "CATEGORY_EMAIL":
notificationCategory = NotificationCompat.CATEGORY_EMAIL;
break;
case "CATEGORY_EVENT":
notificationCategory = NotificationCompat.CATEGORY_EVENT;
break;
case "CATEGORY_PROMO":
notificationCategory = NotificationCompat.CATEGORY_PROMO;
break;
case "CATEGORY_PROGRESS":
notificationCategory = NotificationCompat.CATEGORY_PROGRESS;
break;
case "CATEGORY_SOCIAL":
notificationCategory = NotificationCompat.CATEGORY_SOCIAL;
break;
case "CATEGORY_ERROR":
notificationCategory = NotificationCompat.CATEGORY_ERROR;
break;
case "CATEGORY_TRANSPORT":
notificationCategory = NotificationCompat.CATEGORY_TRANSPORT;
break;
case "CATEGORY_SYSTEM":
notificationCategory = NotificationCompat.CATEGORY_SYSTEM;
break;
case "CATEGORY_SERVICE":
notificationCategory = NotificationCompat.CATEGORY_SERVICE;
break;
case "CATEGORY_RECOMMENDATION":
notificationCategory = NotificationCompat.CATEGORY_RECOMMENDATION;
break;
case "CATEGORY_REMINDER":
notificationCategory = NotificationCompat.CATEGORY_REMINDER;
break;
case "CATEGORY_STATUS":
notificationCategory = NotificationCompat.CATEGORY_STATUS;
break;
}
}
// Default notification visibility is private
String visibility = data.get("visibility");
int notificationVisibility = 0;
if (visibility != null && !visibility.isEmpty()) {
visibility = visibility.toUpperCase();
switch (visibility) {
case "VISIBILITY_PUBLIC":
notificationVisibility = NotificationCompat.VISIBILITY_PUBLIC;
break;
case "VISIBILITY_SECRET":
notificationVisibility = NotificationCompat.VISIBILITY_SECRET;
break;
default:
notificationVisibility = NotificationCompat.VISIBILITY_PRIVATE;
break;
}
}
//creating default notification url for grouped notifications
// if notification grouped, user cannot go the url corresponding to the each notification therefore assign a common url for the notification
String defaultNotificationURL = "https://something.com"
// Creating notification object
NotificationDTO notificationDTO = new NotificationDTO(
notificationId,
groupKey,
message,
notificationURL,
channelId,
image,
label,
notificationPriority,
title,
notificationCategory,
notificationVisibility,
defaultNotificationURL);
// Checking app is in foreground or background
// if the app in the foreground this message service send a broadcast message
// else app will create a notification in notification panel
try {
isForeGround = new ForegroundCheckTask().execute(this).get();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
//Android implement new grouping and channel mechanisms after android API version 24,
//So we need to implement different notification settings for both above 24 and below 24
if (android.os.Build.VERSION.SDK_INT >= 24) {
createNotificationForAPILevelAbove24(notificationDTO, isForeGround);
} else {
createNotificationForAPILevelBelow24(notificationDTO, isForeGround);
}
}
/**
* Creating notification for api level above 24
*
* @param notificationDTO NotificationDTO
* @param isForeGround Boolean
*/
private void createNotificationForAPILevelAbove24(NotificationDTO notificationDTO, Boolean isForeGround) {
Log.d(TAG, String.valueOf(isForeGround));
if (isForeGround) {
Intent intent = new Intent(MY_NOTIFICATION);
intent.putExtra("notificationURL", notificationDTO.getNotificationURL());
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
} else {
int requestID = (int) System.currentTimeMillis();
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra("notificationURL", notificationDTO.getNotificationURL());
PendingIntent resultIntent = PendingIntent.getActivity(this, requestID, intent,
PendingIntent.FLAG_ONE_SHOT);
Uri notificationSoundURI = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
String defaultChannel = getString(R.string.general_notification_id);
NotificationCompat.Builder mNotificationBuilder = new NotificationCompat.Builder(this, defaultChannel);
mNotificationBuilder.setSmallIcon(R.drawable.ic_stat_notification);
mNotificationBuilder.setColor(getResources().getColor(R.color.colorPrimary));
mNotificationBuilder.setContentTitle(notificationDTO.getTitle());
mNotificationBuilder.setContentText(notificationDTO.getMessage());
mNotificationBuilder.setGroup(notificationDTO.getGroupKey());
mNotificationBuilder.setAutoCancel(true);
mNotificationBuilder.setSound(notificationSoundURI);
mNotificationBuilder.setPriority(notificationDTO.getPriority());
if (notificationDTO.getImage() != null) {
Bitmap bitmap = getBitmapFromUrl(notificationDTO.getImage());
mNotificationBuilder.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(bitmap));
}
mNotificationBuilder.setContentIntent(resultIntent);
if (notificationDTO.getCategory() != null) {
mNotificationBuilder.setCategory(notificationDTO.getCategory());
}
mNotificationBuilder.setVisibility(notificationDTO.getVisibility());
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
boolean areNotificationsEnabled = notificationManager.areNotificationsEnabled();
String appPushEnabled = String.valueOf(areNotificationsEnabled);
notificationManager.notify(notificationDTO.getId(), mNotificationBuilder.build());
// Creating notification summary for grouping notifications
Notification summaryNotification =
new NotificationCompat.Builder(this, defaultChannel)
.setContentTitle(getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_stat_notification)
//specify which group this notification belongs to
.setGroup(notificationDTO.getGroupKey())
//set this notification as the summary for the group
.setGroupSummary(true)
//automatically remove the notifications from the notification tray
.setAutoCancel(true)
.build();
notificationManager.notify(getString(R.string.app_name), SUMMARY_ID, summaryNotification);
}
}
/**
* Handling notification for api level below 24
*
* @param notificationDTO NotificationDTO
* @param isForeGround Boolean
*/
private void createNotificationForAPILevelBelow24(NotificationDTO notificationDTO, Boolean isForeGround) {
if (isForeGround) {
Intent intent = new Intent(MY_NOTIFICATION);
intent.putExtra("notificationURL", notificationDTO.getNotificationURL());
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
} else {
//Grouping notifications
String storedNotifications = SharedPrefManager.getInstance(this).getNotifications();
JSONArray notificationArray;
try {
boolean isDuplicateNotification = false;
JSONObject notificationObject = new JSONObject();
notificationObject.put("notificationId", notificationDTO.getId());
notificationObject.put("description", notificationDTO.getMessage());
notificationObject.put("title", notificationDTO.getTitle());
if (storedNotifications != null && !storedNotifications.equals("")) {
Log.d(TAG, storedNotifications);
notificationArray = new JSONArray(storedNotifications);
for (int i = 0; i < notificationArray.length(); i++) {
JSONObject json = notificationArray.getJSONObject(i);
if (json.getInt("notificationId") == notificationDTO.getId()) {
isDuplicateNotification = true;
break;
}
}
} else {
notificationArray = new JSONArray();
}
if (isDuplicateNotification) {
//Notification already added to the tray
return;
}
notificationArray.put(notificationObject);
SharedPrefManager.getInstance(this).setNotificationDetails(notificationArray.toString());
Uri notificationSoundURI = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder summary = new NotificationCompat.Builder(this);
summary.setSmallIcon(R.drawable.ic_stat_notification);
summary.setGroup(notificationDTO.getGroupKey());
summary.setAutoCancel(true);
summary.setPriority(notificationDTO.getPriority());
summary.setColor(ContextCompat.getColor(this, R.color.colorPrimary));
summary.setSound(notificationSoundURI);
summary.setContentTitle(notificationDTO.getTitle());
summary.setContentText(notificationDTO.getMessage());
summary.setPriority(notificationDTO.getPriority());
if (notificationDTO.getCategory() != null) {
summary.setCategory(notificationDTO.getCategory());
}
summary.setVisibility(notificationDTO.getVisibility());
if (notificationDTO.getImage() != null) {
Bitmap bitmap = getBitmapFromUrl(notificationDTO.getImage());
summary.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(bitmap));
}
/*
* This is used to pass notification url to the main class of the application.
* Based on this url MainActivity load the corresponding url into the web view
*/
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
/*
* checking more than 2 notifications received by the system,
* then this will create a summary of that notifications.
* else create a single notification
*/
if (notificationArray.length() > 1) {
summary.setGroupSummary(true);
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
inboxStyle.setBigContentTitle(getString(R.string.app_name));
summary.setStyle(inboxStyle);
int messageCount;
for (messageCount = 0; messageCount < notificationArray.length(); messageCount++) {
JSONObject json = notificationArray.getJSONObject(messageCount);
inboxStyle.addLine(json.getString("title") + " " + json.getString("description"));
}
inboxStyle.setSummaryText(String.valueOf
(messageCount) + " notifications");
summary.setNumber(messageCount);
summary.setContentText(String.valueOf(messageCount + " notifications"));
intent.putExtra("notificationURL", notificationDTO.getDefaultNotificationUrl());
} else {
intent.putExtra("notificationURL", notificationDTO.getNotificationURL());
}
PendingIntent resultIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_ONE_SHOT);
summary.setContentIntent(resultIntent);
/*
* One cancel intent is used to clear the notifications stored in
* the shared preferences when user delete the notifications.
*/
Intent onCancelIntent = new Intent(this, OnCancelBroadcastReceiver.class);
PendingIntent onDismissPendingIntent = PendingIntent.getBroadcast(this.getApplicationContext(), 0, onCancelIntent, 0);
summary.setDeleteIntent(onDismissPendingIntent);
notificationManager.notify(getString(R.string.app_name), SUMMARY_ID, summary.build());
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Used to load image from notification
*
* @param imageUrl String
* @return Bitmap
*/
public Bitmap getBitmapFromUrl(String imageUrl) {
try {
URL url = new URL(imageUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
return BitmapFactory.decodeStream(input);
} catch (Exception e) {
return null;
}
}
}
Notification object class like follows:
public class NotificationDTO {
private String groupKey, message, notificationURL, channelId;
private String image, label, title, category,defaultNotificationUrl;
private int priority, id, visibility;
public NotificationDTO(
int id,
String groupKey,
String message,
String notificationURL,
String channelId,
String image,
String label,
int priority,
String title,
String category,
int visibility,
String defaultNotificationUrl) {
this.groupKey = groupKey;
this.message = message;
this.id = id;
this.notificationURL = notificationURL;
this.channelId = channelId;
this.image = image;
this.label = label;
this.priority = priority;
this.title = title;
this.category = category;
this.visibility = visibility;
this.defaultNotificationUrl = defaultNotificationUrl;
}
public String getGroupKey() {
return groupKey;
}
public String getMessage() {
return message;
}
public String getNotificationURL() {
return notificationURL;
}
public String getChannelId() {
return channelId;
}
public String getLabel() {
return label;
}
public String getImage() {
return image;
}
public int getPriority() {
return priority;
}
public String getTitle() {
return title;
}
public String getCategory() {
return category;
}
public int getId() {
return id;
}
public int getVisibility() {
return visibility;
}
public String getDefaultNotificationUrl() {
return defaultNotificationUrl;
}
}
SharedPreference manager like follows:
public class SharedPrefManager {
private static final String KEY_FCM_TOKEN = "keyFCMToken";
private static final String KEY_NOTIFICATIONS = "keyNotifications";
private static SharedPrefManager mInstance;
private static Context mContext;
private SharedPrefManager(Context context) {
mContext = context;
}
public static synchronized SharedPrefManager getInstance(Context context) {
if (mInstance == null) {
mInstance = new SharedPrefManager(context);
}
return mInstance;
}
public void setNotificationDetails(String descriptions) {
SharedPreferences sharedPreferences = mContext.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(KEY_NOTIFICATIONS, descriptions);
editor.apply();
}
public void setFCMToken(String fcmToken) {
SharedPreferences sharedPreferences = mContext.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(KEY_FCM_TOKEN, fcmToken);
editor.apply();
}
public String getNotifications() {
SharedPreferences sharedPreferences = mContext.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
return sharedPreferences.getString(KEY_NOTIFICATIONS, null);
}
public String getFCMToken() {
SharedPreferences sharedPreferences = mContext.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
return sharedPreferences.getString(KEY_FCM_TOKEN, null);
}
}
and for creating channels for notification I'm using an app controller class that extends applciation class.
public class AppController extends Application {
public static final String TAG = AppController.class.getSimpleName();
@Override
public void onCreate() {
super.onCreate();
mInstance = this;
/*
Define notification channels here.
*/
//NotificationDTO channel is necessary
//create a notification channel id in res/values/strings.xml
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create channel to show notifications.
String defaultChannel = getString(R.string.general_notification_id);
String channelName = getString(R.string.general_notification_name);
// String miscellaneousChannel = getString(R.string.miscellaneous_notification_id);
// String miscellaneousChannelName = getString(R.string.miscellaneous_notification_name);
NotificationManager notificationManager =
getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(new NotificationChannel(defaultChannel,
channelName, NotificationManager.IMPORTANCE_DEFAULT));
}
}
}
A broadcast receiver implemented to clear the shared preferences like follows
public class OnCancelBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("ON_CANCEL","Cancelled");
SharedPrefManager.getInstance(context).setNotificationDetails("");
}
}
By grouping the notification, I'm presuming you mean stacking or bundling notifications.
This is more on how you handle the notification in your client app. You simply have to make use of the setGroup() to add all your notifications to a single group then calling notify() to let the NotificationManager of the changes.
This Add Each Notification to a Group documentation pretty much sums it all up.
You can use the 'tag' in the notification payload:
tag Optional, string
Identifier used to replace existing notifications in the notification drawer. If not specified, each request creates a new notification. If specified and a notification with the same tag is already being shown, the new notification replaces the existing one in the notification drawer.
source: https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref#notification-payload-support