Skip to content

Commit

Permalink
Implement notification groupping on Android
Browse files Browse the repository at this point in the history
Summary: This differential implements notification grouping based on threadID on Android

Test Plan:
1. Obtain device with Android version at least 24 and build the app.
2. Send messages from different clients to the device. Ensure that for each thread there is one expandable notification that encapsulates all sent messages when expanded.
3. Read messages from one client on another device (or web). Ensure rescinding removes all notifications for this particular thread and does not break grouping for other threads - notifications for other threads are still encapsulated in expandable.

Reviewers: bartek, tomek

Reviewed By: bartek

Subscribers: atul, ashoat

Differential Revision: https://phab.comm.dev/D8182
  • Loading branch information
marcinwasowicz committed Jun 27, 2023
1 parent b0c92fe commit 597ebe4
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,23 @@ public void removeAllActiveNotificationsForThread(String threadID) {
continue;
}
String notificationThreadID = data.getString("threadID");
if (notificationThreadID != null &&
notificationThreadID.equals(threadID)) {
String notificationGroup = notification.getNotification().getGroup();
if (threadID.equals(notificationThreadID) ||
threadID.equals(notificationGroup)) {
notificationManager.cancel(notification.getTag(), notification.getId());
}
}
}

@ReactMethod
public void getInitialNotification(Promise promise) {
Bundle initialNotification =
getCurrentActivity().getIntent().getParcelableExtra("message");
if (initialNotification == null) {
public void getInitialNotificationThreadID(Promise promise) {
String initialNotificationThreadID =
getCurrentActivity().getIntent().getStringExtra("threadID");
if (initialNotificationThreadID == null) {
promise.resolve(null);
return;
}
WritableMap jsReadableNotification =
CommAndroidNotificationParser.parseRemoteMessageToJSMessage(
initialNotification);
promise.resolve(jsReadableNotification);
promise.resolve(initialNotificationThreadID);
}

@ReactMethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,13 @@ private void sendEventToJS(String eventName, Object body) {
}

private void sendInitialNotificationFromIntentToJS(Intent intent) {
Bundle initialNotification = intent.getParcelableExtra("message");
if (initialNotification == null) {
String initialNotificationThreadID = intent.getStringExtra("threadID");
if (initialNotificationThreadID == null) {
return;
}
WritableMap jsReadableNotification =
CommAndroidNotificationParser.parseRemoteMessageToJSMessage(
initialNotification);
if (jsReadableNotification != null) {
sendEventToJS(
COMM_ANDROID_NOTIFICATIONS_NOTIFICATION_OPENED,
jsReadableNotification);
}
sendEventToJS(
COMM_ANDROID_NOTIFICATIONS_NOTIFICATION_OPENED,
initialNotificationThreadID);
}

private class CommAndroidNotificationsTokenReceiver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,19 @@ private boolean isAppInForeground() {
Lifecycle.State.RESUMED;
}

private boolean notificationGroupingSupported() {
// Comm doesn't support notification grouping for clients running
// Android versions older than 23
return android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.M;
}

private void handleNotificationRescind(RemoteMessage message) {
String setUnreadStatus = message.getData().get(SET_UNREAD_STATUS_KEY);
String threadID = message.getData().get(THREAD_ID_KEY);
if ("true".equals(setUnreadStatus)) {
File sqliteFile =
this.getApplicationContext().getDatabasePath("comm.sqlite");
if (sqliteFile.exists()) {
String threadID = message.getData().get(THREAD_ID_KEY);
GlobalDBSingleton.scheduleOrRun(() -> {
ThreadOperations.updateSQLiteUnreadStatus(
sqliteFile.getPath(), threadID, false);
Expand All @@ -171,13 +177,55 @@ private void handleNotificationRescind(RemoteMessage message) {
}
}
String rescindID = message.getData().get(RESCIND_ID_KEY);
boolean groupSummaryPresent = false;
boolean threadGroupPresent = false;

for (StatusBarNotification notification :
notificationManager.getActiveNotifications()) {
String tag = notification.getTag();
boolean isGroupMember =
threadID.equals(notification.getNotification().getGroup());
boolean isGroupSummary =
(notification.getNotification().flags &
Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY;
if (tag != null && tag.equals(rescindID)) {
notificationManager.cancel(notification.getTag(), notification.getId());
} else if (isGroupMember && isGroupSummary) {
groupSummaryPresent = true;
} else if (isGroupMember) {
threadGroupPresent = true;
}
}

if (groupSummaryPresent && !threadGroupPresent) {
notificationManager.cancel(threadID, threadID.hashCode());
}
}

private void addToThreadGroupAndDisplay(
String notificationID,
NotificationCompat.Builder notificationBuilder,
String threadID) {

notificationBuilder =
notificationBuilder.setGroup(threadID).setGroupAlertBehavior(
NotificationCompat.GROUP_ALERT_CHILDREN);

NotificationCompat.Builder groupSummaryNotificationBuilder =
new NotificationCompat.Builder(this.getApplicationContext())
.setChannelId(CHANNEL_ID)
.setSmallIcon(R.drawable.notif_icon)
.setContentIntent(
this.createStartMainActivityAction(threadID, threadID))
.setGroup(threadID)
.setGroupSummary(true)
.setAutoCancel(true)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);

notificationManager.notify(
notificationID, notificationID.hashCode(), notificationBuilder.build());
notificationManager.notify(
threadID, threadID.hashCode(), groupSummaryNotificationBuilder.build());
}

private void displayNotification(RemoteMessage message) {
Expand All @@ -198,9 +246,6 @@ private void displayNotification(RemoteMessage message) {
Bundle data = new Bundle();
data.putString(THREAD_ID_KEY, threadID);

PendingIntent startMainActivityAction =
this.createStartMainActivityAction(message);

NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this.getApplicationContext())
.setDefaults(Notification.DEFAULT_ALL)
Expand All @@ -210,25 +255,35 @@ private void displayNotification(RemoteMessage message) {
.setVibrate(VIBRATION_SPEC)
.setSmallIcon(R.drawable.notif_icon)
.setLargeIcon(displayableNotificationLargeIcon)
.setAutoCancel(true)
.setContentIntent(startMainActivityAction);
.setAutoCancel(true);

if (title != null) {
notificationBuilder = notificationBuilder.setContentTitle(title);
}
notificationManager.notify(id, id.hashCode(), notificationBuilder.build());

if (threadID != null) {
notificationBuilder = notificationBuilder.setContentIntent(
this.createStartMainActivityAction(id, threadID));
}

if (!this.notificationGroupingSupported() || threadID == null) {
notificationManager.notify(
id, id.hashCode(), notificationBuilder.build());
return;
}
this.addToThreadGroupAndDisplay(id, notificationBuilder, threadID);
}

private PendingIntent createStartMainActivityAction(RemoteMessage message) {
private PendingIntent
createStartMainActivityAction(String notificationID, String threadID) {
Intent intent =
new Intent(this.getApplicationContext(), MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(
"message", serializeMessageDataForIntentAttachment(message));
intent.putExtra("threadID", threadID);

return PendingIntent.getActivity(
this.getApplicationContext(),
message.getData().get(NOTIF_ID_KEY).hashCode(),
notificationID.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
}
Expand Down
4 changes: 2 additions & 2 deletions native/push/android.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type CommAndroidNotificationsConstants = {

type CommAndroidNotificationsModuleType = {
+removeAllActiveNotificationsForThread: (threadID: string) => void,
+getInitialNotification: () => Promise<?AndroidMessage>,
+getInitialNotificationThreadID: () => Promise<?string>,
+createChannel: (
channelID: string,
name: string,
Expand Down Expand Up @@ -66,7 +66,7 @@ function handleAndroidMessage(
function getCommAndroidNotificationsEventEmitter(): NativeEventEmitter<{
commAndroidNotificationsToken: [string],
commAndroidNotificationsMessage: [AndroidMessage],
commAndroidNotificationsNotificationOpened: [AndroidMessage],
commAndroidNotificationsNotificationOpened: [string],
}> {
return new NativeEventEmitter(CommAndroidNotificationsEventEmitter);
}
Expand Down
11 changes: 5 additions & 6 deletions native/push/push-handler.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,10 @@ class PushHandler extends React.PureComponent<Props, State> {
return;
}
this.initialAndroidNotifHandled = true;
const initialNotif =
await CommAndroidNotifications.getInitialNotification();
if (initialNotif) {
await this.androidNotificationOpened(initialNotif);
const initialNotifThreadID =
await CommAndroidNotifications.getInitialNotificationThreadID();
if (initialNotifThreadID) {
await this.androidNotificationOpened(initialNotifThreadID);
}
}

Expand Down Expand Up @@ -572,9 +572,8 @@ class PushHandler extends React.PureComponent<Props, State> {
});
}

androidNotificationOpened = async (notificationOpen: AndroidMessage) => {
androidNotificationOpened = async (threadID: string) => {
this.onPushNotifBootsApp();
const { threadID } = notificationOpen;
this.onPressNotificationForThread(threadID, true);
};

Expand Down

0 comments on commit 597ebe4

Please sign in to comment.