From f0f289175a93d07646ed697b5175a1e7ef1cae3c Mon Sep 17 00:00:00 2001 From: vannt1991 Date: Thu, 1 Oct 2020 02:57:20 +0700 Subject: [PATCH] fix(android, messaging): store notifications for initial/open attribution (#4317) Use a 100-notification limited ring buffer in prefs to store them --- .../firebase/messaging/JsonConvert.java | 129 ++++++++++++++++++ .../ReactNativeFirebaseMessagingModule.java | 6 +- .../ReactNativeFirebaseMessagingReceiver.java | 6 +- .../ReactNativeFirebaseMessagingStore.java | 11 ++ ...actNativeFirebaseMessagingStoreHelper.java | 23 ++++ ...ReactNativeFirebaseMessagingStoreImpl.java | 86 ++++++++++++ 6 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/messaging/JsonConvert.java create mode 100644 android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStore.java create mode 100644 android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStoreHelper.java create mode 100644 android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStoreImpl.java diff --git a/android/src/main/java/io/invertase/firebase/messaging/JsonConvert.java b/android/src/main/java/io/invertase/firebase/messaging/JsonConvert.java new file mode 100644 index 0000000000..8bef978a35 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/messaging/JsonConvert.java @@ -0,0 +1,129 @@ +package io.invertase.firebase.messaging; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; + +public abstract class JsonConvert { + public static JSONObject reactToJSON(ReadableMap readableMap) throws JSONException { + JSONObject jsonObject = new JSONObject(); + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + while(iterator.hasNextKey()){ + String key = iterator.nextKey(); + ReadableType valueType = readableMap.getType(key); + switch (valueType){ + case Null: + jsonObject.put(key,JSONObject.NULL); + break; + case Boolean: + jsonObject.put(key, readableMap.getBoolean(key)); + break; + case Number: + try { + jsonObject.put(key, readableMap.getInt(key)); + } catch(Exception e) { + jsonObject.put(key, readableMap.getDouble(key)); + } + break; + case String: + jsonObject.put(key, readableMap.getString(key)); + break; + case Map: + jsonObject.put(key, reactToJSON(readableMap.getMap(key))); + break; + case Array: + jsonObject.put(key, reactToJSON(readableMap.getArray(key))); + break; + } + } + + return jsonObject; + } + + public static JSONArray reactToJSON(ReadableArray readableArray) throws JSONException { + JSONArray jsonArray = new JSONArray(); + for(int i=0; i < readableArray.size(); i++) { + ReadableType valueType = readableArray.getType(i); + switch (valueType){ + case Null: + jsonArray.put(JSONObject.NULL); + break; + case Boolean: + jsonArray.put(readableArray.getBoolean(i)); + break; + case Number: + try { + jsonArray.put(readableArray.getInt(i)); + } catch(Exception e) { + jsonArray.put(readableArray.getDouble(i)); + } + break; + case String: + jsonArray.put(readableArray.getString(i)); + break; + case Map: + jsonArray.put(reactToJSON(readableArray.getMap(i))); + break; + case Array: + jsonArray.put(reactToJSON(readableArray.getArray(i))); + break; + } + } + return jsonArray; + } + + public static WritableMap jsonToReact(JSONObject jsonObject) throws JSONException { + WritableMap writableMap = Arguments.createMap(); + Iterator iterator = jsonObject.keys(); + while(iterator.hasNext()) { + String key = (String) iterator.next(); + Object value = jsonObject.get(key); + if (value instanceof Float || value instanceof Double) { + writableMap.putDouble(key, jsonObject.getDouble(key)); + } else if (value instanceof Number) { + writableMap.putInt(key, jsonObject.getInt(key)); + } else if (value instanceof String) { + writableMap.putString(key, jsonObject.getString(key)); + } else if (value instanceof JSONObject) { + writableMap.putMap(key,jsonToReact(jsonObject.getJSONObject(key))); + } else if (value instanceof JSONArray){ + writableMap.putArray(key, jsonToReact(jsonObject.getJSONArray(key))); + } else if (value == JSONObject.NULL){ + writableMap.putNull(key); + } + } + + return writableMap; + } + + public static WritableArray jsonToReact(JSONArray jsonArray) throws JSONException { + WritableArray writableArray = Arguments.createArray(); + for(int i=0; i < jsonArray.length(); i++) { + Object value = jsonArray.get(i); + if (value instanceof Float || value instanceof Double) { + writableArray.pushDouble(jsonArray.getDouble(i)); + } else if (value instanceof Number) { + writableArray.pushInt(jsonArray.getInt(i)); + } else if (value instanceof String) { + writableArray.pushString(jsonArray.getString(i)); + } else if (value instanceof JSONObject) { + writableArray.pushMap(jsonToReact(jsonArray.getJSONObject(i))); + } else if (value instanceof JSONArray){ + writableArray.pushArray(jsonToReact(jsonArray.getJSONArray(i))); + } else if (value == JSONObject.NULL){ + writableArray.pushNull(); + } + } + return writableArray; + } +} diff --git a/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingModule.java b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingModule.java index 64b86ceb26..efe88d29cc 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingModule.java +++ b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingModule.java @@ -69,7 +69,11 @@ public void getInitialNotification(Promise promise) { // only handle non-consumed initial notifications if (messageId != null && initialNotificationMap.get(messageId) == null) { RemoteMessage remoteMessage = ReactNativeFirebaseMessagingReceiver.notifications.get(messageId); - + if (remoteMessage == null) { + ReactNativeFirebaseMessagingStore messagingStore = ReactNativeFirebaseMessagingStoreHelper.getInstance().getMessagingStore(); + remoteMessage = messagingStore.getFirebaseMessage(messageId); + messagingStore.clearFirebaseMessage(messageId); + } if (remoteMessage != null) { promise.resolve(ReactNativeFirebaseMessagingSerializer.remoteMessageToWritableMap(remoteMessage)); initialNotificationMap.put(messageId, true); diff --git a/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingReceiver.java b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingReceiver.java index c014b26326..9e391ce0bf 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingReceiver.java +++ b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingReceiver.java @@ -11,6 +11,7 @@ import java.util.HashMap; +import io.invertase.firebase.app.ReactNativeFirebaseApp; import io.invertase.firebase.common.ReactNativeFirebaseEventEmitter; import io.invertase.firebase.common.SharedUtils; @@ -22,13 +23,16 @@ public class ReactNativeFirebaseMessagingReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "broadcast received for message"); - + if (ReactNativeFirebaseApp.getApplicationContext() == null) { + ReactNativeFirebaseApp.setApplicationContext(context.getApplicationContext()); + } RemoteMessage remoteMessage = new RemoteMessage(intent.getExtras()); ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance(); // Add a RemoteMessage if the message contains a notification payload if (remoteMessage.getNotification() != null) { notifications.put(remoteMessage.getMessageId(), remoteMessage); + ReactNativeFirebaseMessagingStoreHelper.getInstance().getMessagingStore().storeFirebaseMessage(remoteMessage); } // |-> --------------------- diff --git a/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStore.java b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStore.java new file mode 100644 index 0000000000..c7effba925 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStore.java @@ -0,0 +1,11 @@ +package io.invertase.firebase.messaging; + +import com.google.firebase.messaging.RemoteMessage; + +public interface ReactNativeFirebaseMessagingStore { + void storeFirebaseMessage(RemoteMessage remoteMessage); + + RemoteMessage getFirebaseMessage(String remoteMessageId); + + void clearFirebaseMessage(String remoteMessageId); +} diff --git a/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStoreHelper.java b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStoreHelper.java new file mode 100644 index 0000000000..2a15d13a92 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStoreHelper.java @@ -0,0 +1,23 @@ +package io.invertase.firebase.messaging; + +public class ReactNativeFirebaseMessagingStoreHelper { + + private ReactNativeFirebaseMessagingStore messagingStore; + + private ReactNativeFirebaseMessagingStoreHelper() { + messagingStore = new ReactNativeFirebaseMessagingStoreImpl(); + } + + private static ReactNativeFirebaseMessagingStoreHelper _instance; + + public static ReactNativeFirebaseMessagingStoreHelper getInstance() { + if (_instance == null) { + _instance = new ReactNativeFirebaseMessagingStoreHelper(); + } + return _instance; + } + + public ReactNativeFirebaseMessagingStore getMessagingStore() { + return messagingStore; + } +} diff --git a/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStoreImpl.java b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStoreImpl.java new file mode 100644 index 0000000000..94c2b16e12 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/messaging/ReactNativeFirebaseMessagingStoreImpl.java @@ -0,0 +1,86 @@ +package io.invertase.firebase.messaging; + +import com.facebook.react.bridge.WritableMap; +import com.google.firebase.messaging.RemoteMessage; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.invertase.firebase.common.UniversalFirebasePreferences; + +import static io.invertase.firebase.messaging.JsonConvert.jsonToReact; +import static io.invertase.firebase.messaging.JsonConvert.reactToJSON; +import static io.invertase.firebase.messaging.ReactNativeFirebaseMessagingSerializer.remoteMessageFromReadableMap; +import static io.invertase.firebase.messaging.ReactNativeFirebaseMessagingSerializer.remoteMessageToWritableMap; + +public class ReactNativeFirebaseMessagingStoreImpl implements ReactNativeFirebaseMessagingStore { + + private static final String S_KEY_ALL_NOTIFICATION_IDS = "all_notification_ids"; + private static final int MAX_SIZE_NOTIFICATIONS = 100; + private final String DELIMITER = ","; + + @Override + public void storeFirebaseMessage(RemoteMessage remoteMessage) { + try { + String remoteMessageString = reactToJSON(remoteMessageToWritableMap(remoteMessage)).toString(); +// Log.d("storeFirebaseMessage", remoteMessageString); + UniversalFirebasePreferences preferences = UniversalFirebasePreferences.getSharedInstance(); + preferences.setStringValue(remoteMessage.getMessageId(), remoteMessageString); + // save new notification id + String notifications = preferences.getStringValue(S_KEY_ALL_NOTIFICATION_IDS, ""); + notifications += remoteMessage.getMessageId() + DELIMITER; // append to last + + // check and remove old notifications message + List allNotificationList = convertToArray(notifications); + if (allNotificationList.size() > MAX_SIZE_NOTIFICATIONS) { + String firstRemoteMessageId = allNotificationList.get(0); + preferences.remove(firstRemoteMessageId); + notifications = removeRemoteMessage(firstRemoteMessageId, notifications); + } + preferences.setStringValue(S_KEY_ALL_NOTIFICATION_IDS, notifications); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @Override + public RemoteMessage getFirebaseMessage(String remoteMessageId) { + String remoteMessageString = UniversalFirebasePreferences.getSharedInstance().getStringValue(remoteMessageId, null); + if (remoteMessageString != null) { +// Log.d("getFirebaseMessage", remoteMessageString); + try { + WritableMap readableMap = jsonToReact(new JSONObject(remoteMessageString)); + readableMap.putString("to", remoteMessageId);//fake to + return remoteMessageFromReadableMap(readableMap); + } catch (JSONException e) { + e.printStackTrace(); + } + } + return null; + } + + @Override + public void clearFirebaseMessage(String remoteMessageId) { + UniversalFirebasePreferences preferences = UniversalFirebasePreferences.getSharedInstance(); + preferences.remove(remoteMessageId); + // check and remove old notifications message + String notifications = preferences.getStringValue(S_KEY_ALL_NOTIFICATION_IDS, ""); + if (!notifications.isEmpty()) { + notifications = removeRemoteMessage(remoteMessageId, notifications); // remove from list + preferences.setStringValue(S_KEY_ALL_NOTIFICATION_IDS, notifications); + } + } + + private String removeRemoteMessage(String remoteMessageId, String notifications) { + return notifications.replace(remoteMessageId + DELIMITER, ""); + } + + private List convertToArray(String string) { + return new ArrayList<>(Arrays.asList(string.split(DELIMITER))); + } + +}