Differential D8566 Diff 38941 native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java
Changeset View
Changeset View
Standalone View
Standalone View
native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java
Show All 12 Lines | |||||
import android.util.Log; | import android.util.Log; | ||||
import androidx.core.app.NotificationCompat; | import androidx.core.app.NotificationCompat; | ||||
import androidx.lifecycle.Lifecycle; | import androidx.lifecycle.Lifecycle; | ||||
import androidx.lifecycle.ProcessLifecycleOwner; | import androidx.lifecycle.ProcessLifecycleOwner; | ||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager; | import androidx.localbroadcastmanager.content.LocalBroadcastManager; | ||||
import app.comm.android.ExpoUtils; | import app.comm.android.ExpoUtils; | ||||
import app.comm.android.MainActivity; | import app.comm.android.MainActivity; | ||||
import app.comm.android.R; | import app.comm.android.R; | ||||
import app.comm.android.aescrypto.AESCryptoModuleCompat; | |||||
import app.comm.android.commservices.CommAndroidBlobClient; | |||||
import app.comm.android.fbjni.CommMMKV; | import app.comm.android.fbjni.CommMMKV; | ||||
import app.comm.android.fbjni.CommSecureStore; | import app.comm.android.fbjni.CommSecureStore; | ||||
import app.comm.android.fbjni.GlobalDBSingleton; | import app.comm.android.fbjni.GlobalDBSingleton; | ||||
import app.comm.android.fbjni.MessageOperationsUtilities; | import app.comm.android.fbjni.MessageOperationsUtilities; | ||||
import app.comm.android.fbjni.NetworkModule; | import app.comm.android.fbjni.NetworkModule; | ||||
import app.comm.android.fbjni.NotificationsCryptoModule; | import app.comm.android.fbjni.NotificationsCryptoModule; | ||||
import app.comm.android.fbjni.StaffUtils; | import app.comm.android.fbjni.StaffUtils; | ||||
import app.comm.android.fbjni.ThreadOperations; | import app.comm.android.fbjni.ThreadOperations; | ||||
import com.google.firebase.messaging.FirebaseMessagingService; | import com.google.firebase.messaging.FirebaseMessagingService; | ||||
import com.google.firebase.messaging.RemoteMessage; | import com.google.firebase.messaging.RemoteMessage; | ||||
import java.io.File; | import java.io.File; | ||||
import java.io.IOException; | |||||
import java.lang.OutOfMemoryError; | |||||
import java.lang.StringBuilder; | import java.lang.StringBuilder; | ||||
import java.nio.charset.StandardCharsets; | |||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.Base64; | |||||
import me.leolin.shortcutbadger.ShortcutBadger; | import me.leolin.shortcutbadger.ShortcutBadger; | ||||
import org.json.JSONException; | import org.json.JSONException; | ||||
import org.json.JSONObject; | import org.json.JSONObject; | ||||
public class CommNotificationsHandler extends FirebaseMessagingService { | public class CommNotificationsHandler extends FirebaseMessagingService { | ||||
private static final String BADGE_KEY = "badge"; | private static final String BADGE_KEY = "badge"; | ||||
private static final String BADGE_ONLY_KEY = "badgeOnly"; | private static final String BADGE_ONLY_KEY = "badgeOnly"; | ||||
private static final String BACKGROUND_NOTIF_TYPE_KEY = "backgroundNotifType"; | |||||
private static final String SET_UNREAD_STATUS_KEY = "setUnreadStatus"; | private static final String SET_UNREAD_STATUS_KEY = "setUnreadStatus"; | ||||
private static final String NOTIF_ID_KEY = "id"; | private static final String NOTIF_ID_KEY = "id"; | ||||
private static final String ENCRYPTED_PAYLOAD_KEY = "encryptedPayload"; | private static final String ENCRYPTED_PAYLOAD_KEY = "encryptedPayload"; | ||||
private static final String ENCRYPTION_FAILED_KEY = "encryptionFailed"; | private static final String ENCRYPTION_FAILED_KEY = "encryptionFailed"; | ||||
private static final String BLOB_HASH_KEY = "blobHash"; | |||||
private static final String AES_ENCRYPTION_KEY_LABEL = "encryptionKey"; | |||||
private static final String GROUP_NOTIF_IDS_KEY = "groupNotifIDs"; | private static final String GROUP_NOTIF_IDS_KEY = "groupNotifIDs"; | ||||
private static final String COLLAPSE_ID_KEY = "collapseKey"; | private static final String COLLAPSE_ID_KEY = "collapseKey"; | ||||
private static final String KEYSERVER_ID_KEY = "keyserverID"; | private static final String KEYSERVER_ID_KEY = "keyserverID"; | ||||
private static final String CHANNEL_ID = "default"; | private static final String CHANNEL_ID = "default"; | ||||
private static final long[] VIBRATION_SPEC = {500, 500}; | private static final long[] VIBRATION_SPEC = {500, 500}; | ||||
// Those and future MMKV-related constants should match | // Those and future MMKV-related constants should match | ||||
// similar constants in NotificationService.mm | // similar constants in NotificationService.mm | ||||
private static final String MMKV_KEY_SEPARATOR = "."; | private static final String MMKV_KEY_SEPARATOR = "."; | ||||
private static final String MMKV_KEYSERVER_PREFIX = "KEYSERVER"; | private static final String MMKV_KEYSERVER_PREFIX = "KEYSERVER"; | ||||
private static final String MMKV_UNREAD_COUNT_SUFFIX = "UNREAD_COUNT"; | private static final String MMKV_UNREAD_COUNT_SUFFIX = "UNREAD_COUNT"; | ||||
private Bitmap displayableNotificationLargeIcon; | private Bitmap displayableNotificationLargeIcon; | ||||
private NotificationManager notificationManager; | private NotificationManager notificationManager; | ||||
private LocalBroadcastManager localBroadcastManager; | private LocalBroadcastManager localBroadcastManager; | ||||
private CommAndroidBlobClient blobClient; | |||||
private AESCryptoModuleCompat aesCryptoModule; | |||||
public static final String RESCIND_KEY = "rescind"; | public static final String RESCIND_KEY = "rescind"; | ||||
public static final String RESCIND_ID_KEY = "rescindID"; | public static final String RESCIND_ID_KEY = "rescindID"; | ||||
public static final String TITLE_KEY = "title"; | public static final String TITLE_KEY = "title"; | ||||
public static final String PREFIX_KEY = "prefix"; | public static final String PREFIX_KEY = "prefix"; | ||||
public static final String BODY_KEY = "body"; | public static final String BODY_KEY = "body"; | ||||
public static final String MESSAGE_INFOS_KEY = "messageInfos"; | public static final String MESSAGE_INFOS_KEY = "messageInfos"; | ||||
public static final String THREAD_ID_KEY = "threadID"; | public static final String THREAD_ID_KEY = "threadID"; | ||||
public static final String TOKEN_EVENT = "TOKEN_EVENT"; | public static final String TOKEN_EVENT = "TOKEN_EVENT"; | ||||
public static final String MESSAGE_EVENT = "MESSAGE_EVENT"; | public static final String MESSAGE_EVENT = "MESSAGE_EVENT"; | ||||
@Override | @Override | ||||
public void onCreate() { | public void onCreate() { | ||||
super.onCreate(); | super.onCreate(); | ||||
CommSecureStore.getInstance().initialize( | CommSecureStore.getInstance().initialize( | ||||
ExpoUtils.createExpoSecureStoreSupplier(this.getApplicationContext())); | ExpoUtils.createExpoSecureStoreSupplier(this.getApplicationContext())); | ||||
notificationManager = (NotificationManager)this.getSystemService( | notificationManager = (NotificationManager)this.getSystemService( | ||||
Context.NOTIFICATION_SERVICE); | Context.NOTIFICATION_SERVICE); | ||||
localBroadcastManager = LocalBroadcastManager.getInstance(this); | localBroadcastManager = LocalBroadcastManager.getInstance(this); | ||||
displayableNotificationLargeIcon = BitmapFactory.decodeResource( | displayableNotificationLargeIcon = BitmapFactory.decodeResource( | ||||
this.getApplicationContext().getResources(), R.mipmap.ic_launcher); | this.getApplicationContext().getResources(), R.mipmap.ic_launcher); | ||||
blobClient = new CommAndroidBlobClient(); | |||||
aesCryptoModule = new AESCryptoModuleCompat(); | |||||
} | } | ||||
@Override | @Override | ||||
public void onNewToken(String token) { | public void onNewToken(String token) { | ||||
Intent intent = new Intent(TOKEN_EVENT); | Intent intent = new Intent(TOKEN_EVENT); | ||||
intent.putExtra("token", token); | intent.putExtra("token", token); | ||||
localBroadcastManager.sendBroadcast(intent); | localBroadcastManager.sendBroadcast(intent); | ||||
} | } | ||||
@Override | @Override | ||||
public void onMessageReceived(RemoteMessage message) { | public void onMessageReceived(RemoteMessage message) { | ||||
if (message.getData().get(KEYSERVER_ID_KEY) == null) { | if (message.getData().get(KEYSERVER_ID_KEY) == null) { | ||||
displayErrorMessageNotification( | displayErrorMessageNotification( | ||||
"Received notification without keyserver ID.", | "Received notification without keyserver ID.", | ||||
"Missing keyserver ID.", | "Missing keyserver ID.", | ||||
null); | null); | ||||
return; | return; | ||||
} | } | ||||
String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY); | String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY); | ||||
if (message.getData().get(ENCRYPTED_PAYLOAD_KEY) != null) { | if (message.getData().get(ENCRYPTED_PAYLOAD_KEY) != null) { | ||||
try { | try { | ||||
message = this.decryptRemoteMessage(message, senderKeyserverID); | message = this.olmDecryptRemoteMessage(message, senderKeyserverID); | ||||
} catch (JSONException e) { | } catch (JSONException e) { | ||||
Log.w("COMM", "Malformed notification JSON payload.", e); | Log.w("COMM", "Malformed notification JSON payload.", e); | ||||
return; | return; | ||||
} catch (IllegalStateException e) { | } catch (IllegalStateException e) { | ||||
Log.w("COMM", "Android notification type violation.", e); | Log.w("COMM", "Android notification type violation.", e); | ||||
return; | return; | ||||
} catch (Exception e) { | } catch (Exception e) { | ||||
Log.w("COMM", "Notification decryption failure.", e); | Log.w("COMM", "Notification decryption failure.", e); | ||||
Show All 17 Lines | try { | ||||
Log.w("COMM", "Unread count update failure.", e); | Log.w("COMM", "Unread count update failure.", e); | ||||
} | } | ||||
String badgeOnly = message.getData().get(BADGE_ONLY_KEY); | String badgeOnly = message.getData().get(BADGE_ONLY_KEY); | ||||
if ("1".equals(badgeOnly)) { | if ("1".equals(badgeOnly)) { | ||||
return; | return; | ||||
} | } | ||||
String backgroundNotifType = | if (message.getData().get(MESSAGE_INFOS_KEY) != null) { | ||||
message.getData().get(BACKGROUND_NOTIF_TYPE_KEY); | handleMessageInfosPersistence(message); | ||||
} | |||||
String rawMessageInfosString = message.getData().get(MESSAGE_INFOS_KEY); | if (message.getData().get(BLOB_HASH_KEY) != null && | ||||
File sqliteFile = | message.getData().get(AES_ENCRYPTION_KEY_LABEL) != null) { | ||||
this.getApplicationContext().getDatabasePath("comm.sqlite"); | handleLargeNotification(message); | ||||
if (rawMessageInfosString != null && sqliteFile.exists()) { | |||||
GlobalDBSingleton.scheduleOrRun(() -> { | |||||
MessageOperationsUtilities.storeMessageInfos( | |||||
sqliteFile.getPath(), rawMessageInfosString); | |||||
}); | |||||
} else if (rawMessageInfosString != null) { | |||||
Log.w("COMM", "Database not existing yet. Skipping notification"); | |||||
} | } | ||||
Intent intent = new Intent(MESSAGE_EVENT); | Intent intent = new Intent(MESSAGE_EVENT); | ||||
intent.putExtra( | intent.putExtra( | ||||
"message", serializeMessageDataForIntentAttachment(message)); | "message", serializeMessageDataForIntentAttachment(message)); | ||||
localBroadcastManager.sendBroadcast(intent); | localBroadcastManager.sendBroadcast(intent); | ||||
if (this.isAppInForeground()) { | if (this.isAppInForeground()) { | ||||
▲ Show 20 Lines • Show All 107 Lines • ▼ Show 20 Lines | private void handleUnreadCountUpdate(RemoteMessage message) { | ||||
if (totalUnreadCount > 0) { | if (totalUnreadCount > 0) { | ||||
ShortcutBadger.applyCount(this, totalUnreadCount); | ShortcutBadger.applyCount(this, totalUnreadCount); | ||||
} else { | } else { | ||||
ShortcutBadger.removeCount(this); | ShortcutBadger.removeCount(this); | ||||
} | } | ||||
} | } | ||||
private void handleMessageInfosPersistence(RemoteMessage message) { | |||||
String rawMessageInfosString = message.getData().get(MESSAGE_INFOS_KEY); | |||||
File sqliteFile = | |||||
this.getApplicationContext().getDatabasePath("comm.sqlite"); | |||||
if (rawMessageInfosString != null && sqliteFile.exists()) { | |||||
GlobalDBSingleton.scheduleOrRun(() -> { | |||||
MessageOperationsUtilities.storeMessageInfos( | |||||
sqliteFile.getPath(), rawMessageInfosString); | |||||
}); | |||||
} else if (rawMessageInfosString != null) { | |||||
Log.w("COMM", "Database not existing yet. Skipping notification"); | |||||
} | |||||
} | |||||
private void handleLargeNotification(RemoteMessage message) { | |||||
String blobHash = message.getData().get(BLOB_HASH_KEY); | |||||
try { | |||||
byte[] largePayload = blobClient.getBlobSync(blobHash); | |||||
message = aesDecryptRemoteMessage(message, largePayload); | |||||
handleMessageInfosPersistence(message); | |||||
} catch (Exception e) { | |||||
Log.w("COMM", "Failure when handling large notification.", e); | |||||
} | |||||
} | |||||
private void addToThreadGroupAndDisplay( | private void addToThreadGroupAndDisplay( | ||||
String notificationID, | String notificationID, | ||||
NotificationCompat.Builder notificationBuilder, | NotificationCompat.Builder notificationBuilder, | ||||
String threadID) { | String threadID) { | ||||
notificationBuilder.setGroup(threadID).setGroupAlertBehavior( | notificationBuilder.setGroup(threadID).setGroupAlertBehavior( | ||||
NotificationCompat.GROUP_ALERT_CHILDREN); | NotificationCompat.GROUP_ALERT_CHILDREN); | ||||
▲ Show 20 Lines • Show All 98 Lines • ▼ Show 20 Lines | createStartMainActivityAction(String notificationID, String threadID) { | ||||
return PendingIntent.getActivity( | return PendingIntent.getActivity( | ||||
this.getApplicationContext(), | this.getApplicationContext(), | ||||
notificationID.hashCode(), | notificationID.hashCode(), | ||||
intent, | intent, | ||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); | PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); | ||||
} | } | ||||
private RemoteMessage | private RemoteMessage updateRemoteMessageWithDecryptedPayload( | ||||
decryptRemoteMessage(RemoteMessage message, String senderKeyserverID) | RemoteMessage message, | ||||
String decryptedSerializedPayload) | |||||
throws JSONException, IllegalStateException { | throws JSONException, IllegalStateException { | ||||
String encryptedSerializedPayload = | |||||
message.getData().get(ENCRYPTED_PAYLOAD_KEY); | |||||
String decryptedSerializedPayload = NotificationsCryptoModule.decrypt( | |||||
senderKeyserverID, | |||||
encryptedSerializedPayload, | |||||
NotificationsCryptoModule.olmEncryptedTypeMessage()); | |||||
JSONObject decryptedPayload = new JSONObject(decryptedSerializedPayload); | JSONObject decryptedPayload = new JSONObject(decryptedSerializedPayload); | ||||
((Iterable<String>)() -> decryptedPayload.keys()) | ((Iterable<String>)() -> decryptedPayload.keys()) | ||||
.forEach(payloadFieldName -> { | .forEach(payloadFieldName -> { | ||||
if (decryptedPayload.optJSONArray(payloadFieldName) != null || | if (decryptedPayload.optJSONArray(payloadFieldName) != null || | ||||
decryptedPayload.optJSONObject(payloadFieldName) != null) { | decryptedPayload.optJSONObject(payloadFieldName) != null) { | ||||
throw new IllegalStateException( | throw new IllegalStateException( | ||||
"Notification payload JSON is not {[string]: string} type."); | "Notification payload JSON is not {[string]: string} type."); | ||||
} | } | ||||
String payloadFieldValue = | String payloadFieldValue = | ||||
decryptedPayload.optString(payloadFieldName); | decryptedPayload.optString(payloadFieldName); | ||||
message.getData().put(payloadFieldName, payloadFieldValue); | message.getData().put(payloadFieldName, payloadFieldValue); | ||||
}); | }); | ||||
return message; | return message; | ||||
} | } | ||||
private RemoteMessage | |||||
olmDecryptRemoteMessage(RemoteMessage message, String senderKeyserverID) | |||||
throws JSONException, IllegalStateException { | |||||
String encryptedSerializedPayload = | |||||
message.getData().get(ENCRYPTED_PAYLOAD_KEY); | |||||
String decryptedSerializedPayload = NotificationsCryptoModule.decrypt( | |||||
senderKeyserverID, | |||||
encryptedSerializedPayload, | |||||
NotificationsCryptoModule.olmEncryptedTypeMessage()); | |||||
return updateRemoteMessageWithDecryptedPayload( | |||||
message, decryptedSerializedPayload); | |||||
} | |||||
private RemoteMessage | |||||
aesDecryptRemoteMessage(RemoteMessage message, byte[] blob) | |||||
throws JSONException, IllegalStateException { | |||||
String aesEncryptionKey = message.getData().get(AES_ENCRYPTION_KEY_LABEL); | |||||
// On the keyserver AES key is generated as raw bytes | |||||
// so to send it in JSON it is encoded to Base64 string. | |||||
byte[] aesEncryptionKeyBytes = Base64.getDecoder().decode(aesEncryptionKey); | |||||
// On the keyserver notification is a string so it is | |||||
// first encoded into UTF8 bytes. Therefore bytes | |||||
// obtained from blob decryption are correct UTF8 bytes. | |||||
String decryptedSerializedPayload = new String( | |||||
aesCryptoModule.decrypt(aesEncryptionKeyBytes, blob), | |||||
StandardCharsets.UTF_8); | |||||
return updateRemoteMessageWithDecryptedPayload( | |||||
message, decryptedSerializedPayload); | |||||
} | |||||
private Bundle | private Bundle | ||||
serializeMessageDataForIntentAttachment(RemoteMessage message) { | serializeMessageDataForIntentAttachment(RemoteMessage message) { | ||||
Bundle bundle = new Bundle(); | Bundle bundle = new Bundle(); | ||||
message.getData().forEach(bundle::putString); | message.getData().forEach(bundle::putString); | ||||
return bundle; | return bundle; | ||||
} | } | ||||
private void displayErrorMessageNotification( | private void displayErrorMessageNotification( | ||||
▲ Show 20 Lines • Show All 135 Lines • Show Last 20 Lines |