From 1777a4f7504271fccf123e50589a5edc6ffb8ea3 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 20 Mar 2019 16:39:36 -0300 Subject: [PATCH] MM-14316 Android Notifications and reply to notifications from native code (#2653) * MM-14316 Android Notifications and reply to notifications from native code * Feedback review * Fix typo * setBadgeIconType only for Android 8+ --- android/app/src/main/AndroidManifest.xml | 2 +- .../rnbeta/CustomPushNotification.java | 150 ++++++++--------- .../rnbeta/NotificationDismissService.java | 2 +- .../NotificationReplyBroadcastReceiver.java | 156 ++++++++++++++++++ .../rnbeta/NotificationReplyService.java | 81 --------- 5 files changed, 228 insertions(+), 163 deletions(-) create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java delete mode 100644 android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2be9a6329..fe28b85ee 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -37,7 +37,7 @@ - = Build.VERSION_CODES.O) { + String CHANNEL_ID = "channel_01"; + String CHANNEL_NAME = "Mattermost notifications"; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH); + channel.setShowBadge(true); + final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(channel); notification.setChannelId(CHANNEL_ID); } + Bundle bundle = mNotificationProps.asBundle(); String version = bundle.getString("version"); @@ -185,12 +194,10 @@ public class CustomPushNotification extends PushNotification { String channelId = bundle.getString("channel_id"); String postId = bundle.getString("post_id"); - int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID; - String message = bundle.getString("message"); - String subText = bundle.getString("subText"); - String numberString = bundle.getString("badge"); + String badge = bundle.getString("badge"); String smallIcon = bundle.getString("smallIcon"); String largeIcon = bundle.getString("largeIcon"); + int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID; Bundle b = bundle.getBundle("userInfo"); if (b == null) { @@ -222,67 +229,47 @@ public class CustomPushNotification extends PushNotification { largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName); } - if (numberString != null) { - CustomPushNotification.badgeCount = Integer.parseInt(numberString); + if (badge != null) { + CustomPushNotification.badgeCount = Integer.parseInt(badge); ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount); + } else { + // HERE ADD THE DOT INDICATOR STUFF } - int numMessages = getMessageCountInChannel(channelId); + Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("You"); + if (title != null && !title.startsWith("@")) { + messagingStyle + .setConversationTitle(title); + } + + List bundleArray = channelIdToNotification.get(channelId); + List list; + if (bundleArray != null) { + list = new ArrayList(bundleArray); + } else { + list = new ArrayList(); + } + + for (Bundle data : list) { + String message = data.getString("message"); + if (title == null || !title.startsWith("@")) { + message = removeSenderFromMessage(message); + } + messagingStyle.addMessage(message, data.getLong("time"), data.getString("sender_name")); + } notification .setContentIntent(intent) .setGroupSummary(true) + .setStyle(messagingStyle) .setSmallIcon(smallIconResId) + .setNumber(Integer.parseInt(badge)) .setVisibility(Notification.VISIBILITY_PRIVATE) .setPriority(Notification.PRIORITY_HIGH) .setAutoCancel(true); - if (numMessages == 1) { - notification - .setContentTitle(title) - .setContentText(message) - .setStyle(new Notification.BigTextStyle() - .bigText(message)); - } else { - String summaryTitle = null; - - if (version != null && version.equals("v2")) { - summaryTitle = String.format("(%d) %s", numMessages, title); - } else { - summaryTitle = String.format("%s (%d)", title, numMessages); - } - - Notification.InboxStyle style = new Notification.InboxStyle(); - List bundleArray = channelIdToNotification.get(channelId); - List list; - if (bundleArray != null) { - list = new ArrayList(bundleArray); - } else { - list = new ArrayList(); - } - - if (version != null && version.equals("v2")) { - style.addLine(message); - } - - for (Bundle data : list) { - String msg = data.getString("message"); - if (msg != message) { - style.addLine(data.getString("message")); - } - } - - if (version != null && version.equals("v2")) { - notification - .setContentTitle(summaryTitle) - .setContentText(message) - .setStyle(style); - } else { - style.setBigContentTitle(message) - .setSummaryText(String.format("+%d more", (numMessages - 1))); - notification.setStyle(style) - .setContentTitle(summaryTitle); - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notification.setBadgeIconType(Notification.BADGE_ICON_SMALL); } // Let's add a delete intent when the notification is dismissed @@ -296,17 +283,11 @@ public class CustomPushNotification extends PushNotification { } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && postId != null) { - Intent replyIntent = new Intent(mContext, NotificationReplyService.class); + Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class); replyIntent.setAction(KEY_TEXT_REPLY); replyIntent.putExtra(NOTIFICATION_ID, notificationId); replyIntent.putExtra("pushNotification", bundle); - PendingIntent replyPendingIntent; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - replyPendingIntent = PendingIntent.getForegroundService(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); - } else { - replyPendingIntent = PendingIntent.getService(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); - } + PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY) .setLabel("Reply") @@ -328,10 +309,6 @@ public class CustomPushNotification extends PushNotification { notification.setLargeIcon(largeIconBitmap); } - if (subText != null) { - notification.setSubText(subText); - } - String soundUri = notificationPreferences.getNotificationSound(); if (soundUri != null) { if (soundUri != "none") { @@ -378,4 +355,17 @@ public class CustomPushNotification extends PushNotification { ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount); } + + private String getSenderName(String channelName, String message) { + if (channelName != null && channelName.startsWith("@")) { + return channelName; + } + + return message.split(":")[0]; + } + + private String removeSenderFromMessage(String message) { + String sender = String.format("%s: ", getSenderName("", message)); + return message.replaceFirst(sender, ""); + } } diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java index 8aeefb704..6fb1598a4 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java @@ -11,7 +11,7 @@ import com.wix.reactnativenotifications.core.NotificationIntentAdapter; public class NotificationDismissService extends IntentService { private Context mContext; public NotificationDismissService() { - super("notificationDismissService"); + super("notificationDismissService"); } @Override diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java new file mode 100644 index 000000000..0df1a9a9c --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java @@ -0,0 +1,156 @@ +package com.mattermost.rnbeta; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.RemoteInput; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.util.Log; +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import com.mattermost.react_native_interface.ResolvePromise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.WritableMap; +import com.oblador.keychain.KeychainModule; + +import com.wix.reactnativenotifications.core.NotificationIntentAdapter; + +public class NotificationReplyBroadcastReceiver extends BroadcastReceiver { + private Context mContext; + private Bundle bundle; + private NotificationManager notificationManager; + + @Override + public void onReceive(Context context, Intent intent) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + mContext = context; + bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context); + final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1); + final CharSequence message = getReplyMessage(intent); + final KeychainModule keychainModule = new KeychainModule(reactApplicationContext); + + keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() { + @Override + public void resolve(@Nullable Object value) { + if (value instanceof Boolean && !(Boolean)value) { + String channelId = bundle.getString("channel_id"); + onReplyFailed(notificationManager, notificationId, channelId); + return; + } + + WritableMap map = (WritableMap) value; + if (map != null) { + String[] credentials = map.getString("password").split(",[ ]*"); + String token = null; + String serverUrl = null; + if (credentials.length == 2) { + token = credentials[0]; + serverUrl = credentials[1]; + + } + + Log.i("ReactNative", String.format("URL=%s TOKEN=%s", serverUrl, token)); + replyToMessage(serverUrl, token, notificationId, message); + } + } + }); + } + } + + protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) { + final String channelId = bundle.getString("channel_id"); + final String rootId = bundle.getString("post_id"); + + if (token == null || serverUrl == null) { + onReplyFailed(notificationManager, notificationId, channelId); + return; + } + + final OkHttpClient client = new OkHttpClient(); + final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + String json = buildReplyPost(channelId, rootId, message.toString()); + Log.i("ReactNative", String.format("JSON STRING %s", json)); + RequestBody body = RequestBody.create(JSON, json); + Request request = new Request.Builder() + .header("Authorization", String.format("Bearer %s", token)) + .header("Content-Type", "application/json") + .url(String.format("%s/api/v4/posts", serverUrl.replaceAll("/$", ""))) + .post(body) + .build(); + + client.newCall(request).enqueue(new okhttp3.Callback() { + @Override + public void onFailure(Call call, IOException e) { + Log.i("ReactNative", String.format("Reply with message %s FAILED exception %s", message, e.getMessage())); + onReplyFailed(notificationManager, notificationId, channelId); + } + + @Override + public void onResponse(Call call, final Response response) throws IOException { + if (response.isSuccessful()) { + onReplySuccess(notificationManager, notificationId, channelId); + Log.i("ReactNative", String.format("Reply with message %s", message)); + } else { + Log.i("ReactNative", String.format("Reply with message %s FAILED status %s BODY %s", message, response.code(), response.body().string())); + onReplyFailed(notificationManager, notificationId, channelId); + } + } + }); + } + + protected String buildReplyPost(String channelId, String rootId, String message) { + return "{" + + "\"channel_id\": \"" + channelId + "\"," + + "\"message\": \"" + message + "\"," + + "\"root_id\": \"" + rootId + "\"" + + "}"; + } + + protected void onReplyFailed(NotificationManager notificationManager, int notificationId, String channelId) { + String CHANNEL_ID = "Reply job"; + Resources res = mContext.getResources(); + String packageName = mContext.getPackageName(); + int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName); + + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW); + notificationManager.createNotificationChannel(channel); + Notification notification = + new Notification.Builder(mContext, CHANNEL_ID) + .setContentTitle("Message failed to send.") + .setContentText(packageName) + .setSmallIcon(smallIconResId) + .build(); + + CustomPushNotification.clearNotification(mContext, notificationId, channelId); + notificationManager.notify(notificationId, notification); + } + + protected void onReplySuccess(NotificationManager notificationManager, int notificationId, String channelId) { + notificationManager.cancel(notificationId); + CustomPushNotification.clearNotification(mContext, notificationId, channelId); + } + + private CharSequence getReplyMessage(Intent intent) { + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY); + } + return null; + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java deleted file mode 100644 index 3978284fd..000000000 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.mattermost.rnbeta; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.RemoteInput; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.os.Build; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.util.Log; - -import com.facebook.react.HeadlessJsTaskService; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.jstasks.HeadlessJsTaskConfig; - - -import com.wix.reactnativenotifications.core.NotificationIntentAdapter; - -public class NotificationReplyService extends HeadlessJsTaskService { - private Context mContext; - - @Override - public void onCreate() { - super.onCreate(); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - Context mContext = this.getApplicationContext(); - final Resources res = mContext.getResources(); - String packageName = mContext.getPackageName(); - int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName); - String CHANNEL_ID = "Reply job"; - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW); - ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel); - Notification notification = - new Notification.Builder(mContext, CHANNEL_ID) - .setContentTitle("Replying to message") - .setContentText(packageName) - .setSmallIcon(smallIconResId) - .build(); - startForeground(1, notification); - } - - } - - @Override - protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) { - mContext = getApplicationContext(); - if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) { - CharSequence message = getReplyMessage(intent); - - Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); - String channelId = bundle.getString("channel_id"); - bundle.putCharSequence("text", message); - bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId)); - - int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1); - CustomPushNotification.clearNotification(mContext, notificationId, channelId); - - MainApplication app = (MainApplication) this.getApplication(); - app.replyFromPushNotification = true; - Log.i("ReactNative", "Replying service"); - return new HeadlessJsTaskConfig( - "notificationReplied", - Arguments.fromBundle(bundle), - 5000); - - } - - return null; - } - - private CharSequence getReplyMessage(Intent intent) { - Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); - if (remoteInput != null) { - return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY); - } - return null; - } -}