You can use the following code to group notifications. I use the application to browse the Internet via Android, so I pass the URL as intentional content. I used 2 types of grouping notifications based on Android versions, because Android nougat and later versions automatically group notifications based on the group ID, but Marshmallow and earlier will not group notifications. Remember one thing: you must send your notifications as data notifications. because you can handle data notifications inside onMessageReceived even if the application is in the background or onMessageReceived state.
My Firebase messaging service looks like this:
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; } } }
The notification object class is as 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; } }
The SharedPreference manager is as 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 to create channels for notification, I use the application controller class, which extends the application class.
public class AppController extends Application { public static final String TAG = AppController.class.getSimpleName(); @Override public void onCreate() { super.onCreate(); mInstance = this;
The broadcast receiver is implemented to clear the general settings as shown below
public class OnCancelBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.d("ON_CANCEL","Cancelled"); SharedPrefManager.getInstance(context).setNotificationDetails(""); } }