diff --git a/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt b/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt index dc0e00de320..08a3a3e1c98 100644 --- a/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt +++ b/android/app/src/main/java/com/mattermost/helpers/PushNotificationDataHelper.kt @@ -4,29 +4,27 @@ import android.content.Context import android.os.Bundle import android.util.Log import com.facebook.react.bridge.Arguments - import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap - -import com.mattermost.helpers.database_extension.* -import com.mattermost.helpers.push_notification.* - -import kotlinx.coroutines.* +import com.mattermost.helpers.database_extension.getDatabaseForServer +import com.mattermost.helpers.database_extension.saveToDatabase +import com.mattermost.helpers.push_notification.addToDefaultCategoryIfNeeded +import com.mattermost.helpers.push_notification.fetchMyChannel +import com.mattermost.helpers.push_notification.fetchMyTeamCategories +import com.mattermost.helpers.push_notification.fetchNeededUsers +import com.mattermost.helpers.push_notification.fetchPosts +import com.mattermost.helpers.push_notification.fetchTeamIfNeeded +import com.mattermost.helpers.push_notification.fetchThread +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext class PushNotificationDataHelper(private val context: Context) { - private var coroutineScope = CoroutineScope(Dispatchers.Default) - fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? { - var result: Bundle? = null - val job = coroutineScope.launch(Dispatchers.Default) { - result = PushNotificationDataRunnable.start(context, initialData, isReactInit) + suspend fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? { + return withContext(Dispatchers.Default) { + PushNotificationDataRunnable.start(context, initialData, isReactInit) } - runBlocking { - job.join() - } - - return result } } @@ -37,8 +35,8 @@ class PushNotificationDataRunnable { private val mutex = Mutex() suspend fun start(context: Context, initialData: Bundle, isReactInit: Boolean): Bundle? { - // for more info see: https://blog.danlew.net/2020/01/28/coroutines-and-java-synchronization-dont-mix/ mutex.withLock { + // for more info see: https://blog.danlew.net/2020/01/28/coroutines-and-java-synchronization-dont-mix/ val serverUrl: String = initialData.getString("server_url") ?: return null val db = dbHelper.getDatabaseForServer(context, serverUrl) var result: Bundle? = null @@ -50,8 +48,9 @@ class PushNotificationDataRunnable { val postId = initialData.getString("post_id") val rootId = initialData.getString("root_id") val isCRTEnabled = initialData.getString("is_crt_enabled") == "true" + val ackId = initialData.getString("ack_id") - Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId") + Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId and ack=$ackId") val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty() val notificationData = Arguments.createMap() @@ -89,7 +88,7 @@ class PushNotificationDataRunnable { getThreadList(notificationThread, postData?.getArray("threads"))?.let { val threadsArray = Arguments.createArray() - for(item in it) { + for (item in it) { threadsArray.pushMap(item) } notificationData.putArray("threads", threadsArray) @@ -105,7 +104,7 @@ class PushNotificationDataRunnable { dbHelper.saveToDatabase(db, notificationData, teamId, channelId, receivingThreads) } - Log.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId") + Log.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId and ack=$ackId") } } catch (e: Exception) { e.printStackTrace() diff --git a/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt b/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt index a3789cbabdf..985758b1c36 100644 --- a/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt +++ b/android/app/src/main/java/com/mattermost/helpers/database_extension/General.kt @@ -1,6 +1,7 @@ package com.mattermost.helpers.database_extension import android.content.Context +import android.database.sqlite.SQLiteDatabase import android.text.TextUtils import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableMap @@ -8,8 +9,7 @@ import com.mattermost.helpers.DatabaseHelper import com.mattermost.helpers.QueryArgs import com.mattermost.helpers.mapCursor import com.nozbe.watermelondb.WMDatabase -import java.util.* -import kotlin.Exception +import java.util.Arrays internal fun DatabaseHelper.saveToDatabase(db: WMDatabase, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) { db.transaction { @@ -57,7 +57,7 @@ fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): W if (cursor.count == 1) { cursor.moveToFirst() val databasePath = String.format("file://%s", cursor.getString(0)) - return WMDatabase.getInstance(databasePath, context!!) + return WMDatabase.buildDatabase(databasePath, context!!, SQLiteDatabase.CREATE_IF_NECESSARY) } } } catch (e: Exception) { @@ -73,7 +73,7 @@ fun DatabaseHelper.getDeviceToken(): String? { defaultDatabase!!.rawQuery(query, arrayOf("deviceToken")).use { cursor -> if (cursor.count == 1) { cursor.moveToFirst() - return cursor.getString(0); + return cursor.getString(0) } } } catch (e: Exception) { diff --git a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java deleted file mode 100644 index b0054eb23bb..00000000000 --- a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.mattermost.rnbeta; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; - -import java.util.Objects; - -import com.facebook.react.bridge.ReadableMap; -import com.mattermost.helpers.CustomPushNotificationHelper; -import com.mattermost.helpers.DatabaseHelper; -import com.mattermost.helpers.Network; -import com.mattermost.helpers.NotificationHelper; -import com.mattermost.helpers.PushNotificationDataHelper; -import com.mattermost.helpers.ReadableMapUtils; -import com.mattermost.share.ShareModule; -import com.wix.reactnativenotifications.core.NotificationIntentAdapter; -import com.wix.reactnativenotifications.core.notification.PushNotification; -import com.wix.reactnativenotifications.core.AppLaunchHelper; -import com.wix.reactnativenotifications.core.AppLifecycleFacade; -import com.wix.reactnativenotifications.core.JsIOHelper; - -import static com.mattermost.helpers.database_extension.GeneralKt.*; -import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME; - - -public class CustomPushNotification extends PushNotification { - private final PushNotificationDataHelper dataHelper; - - public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) { - super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper); - dataHelper = new PushNotificationDataHelper(context); - - try { - Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).init(context); - Network.init(context); - NotificationHelper.cleanNotificationPreferencesIfNeeded(context); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Override - public void onReceived() { - final Bundle initialData = mNotificationProps.asBundle(); - final String type = initialData.getString("type"); - final String ackId = initialData.getString("ack_id"); - final String postId = initialData.getString("post_id"); - final String channelId = initialData.getString("channel_id"); - final String signature = initialData.getString("signature"); - final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true"); - int notificationId = NotificationHelper.getNotificationId(initialData); - - String serverUrl = addServerUrlToBundle(initialData); - - if (ackId != null && serverUrl != null) { - Bundle response = ReceiptDelivery.send(ackId, serverUrl, postId, type, isIdLoaded); - if (isIdLoaded && response != null) { - Bundle current = mNotificationProps.asBundle(); - if (!current.containsKey("server_url")) { - response.putString("server_url", serverUrl); - } - current.putAll(response); - mNotificationProps = createProps(current); - } - } - - if (!CustomPushNotificationHelper.verifySignature(mContext, signature, serverUrl, ackId)) { - Log.i("Mattermost Notifications Signature verification", "Notification skipped because we could not verify it."); - return; - } - - finishProcessingNotification(serverUrl, type, channelId, notificationId); - } - - @Override - public void onOpened() { - if (mNotificationProps != null) { - digestNotification(); - - Bundle data = mNotificationProps.asBundle(); - NotificationHelper.clearChannelOrThreadNotifications(mContext, data); - } - } - - private void finishProcessingNotification(final String serverUrl, @NonNull final String type, final String channelId, final int notificationId) { - final boolean isReactInit = mAppLifecycleFacade.isReactInitialized(); - - switch (type) { - case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE: - case CustomPushNotificationHelper.PUSH_TYPE_SESSION: - ShareModule shareModule = ShareModule.getInstance(); - String currentActivityName = shareModule != null ? shareModule.getCurrentActivityName() : ""; - Log.i("ReactNative", currentActivityName); - if (!mAppLifecycleFacade.isAppVisible() || !currentActivityName.equals("MainActivity")) { - boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE); - if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) { - if (channelId != null) { - Bundle notificationBundle = mNotificationProps.asBundle(); - if (serverUrl != null) { - // We will only fetch the data related to the notification on the native side - // as updating the data directly to the db removes the wal & shm files needed - // by watermelonDB, if the DB is updated while WDB is running it causes WDB to - // detect the database as malformed, thus the app stop working and a restart is required. - // Data will be fetch from within the JS context instead. - Bundle notificationResult = dataHelper.fetchAndStoreDataForPushNotification(notificationBundle, isReactInit); - if (notificationResult != null) { - notificationBundle.putBundle("data", notificationResult); - mNotificationProps = createProps(notificationBundle); - } - } - createSummary = NotificationHelper.addNotificationToPreferences( - mContext, - notificationId, - notificationBundle - ); - } - } - - buildNotification(notificationId, createSummary); - } - break; - case CustomPushNotificationHelper.PUSH_TYPE_CLEAR: - NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle()); - break; - } - - if (isReactInit) { - notifyReceivedToJS(); - } - } - - private void buildNotification(Integer notificationId, boolean createSummary) { - final PendingIntent pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps); - final Notification notification = buildNotification(pendingIntent); - if (createSummary) { - final Notification summary = getNotificationSummaryBuilder(pendingIntent).build(); - super.postNotification(summary, notificationId + 1); - } - super.postNotification(notification, notificationId); - } - - @Override - protected NotificationCompat.Builder getNotificationBuilder(PendingIntent intent) { - Bundle bundle = mNotificationProps.asBundle(); - return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, false); - } - - protected NotificationCompat.Builder getNotificationSummaryBuilder(PendingIntent intent) { - Bundle bundle = mNotificationProps.asBundle(); - return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true); - } - - private void notifyReceivedToJS() { - mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext()); - } - - private String addServerUrlToBundle(Bundle bundle) { - DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance(); - String serverId = bundle.getString("server_id"); - String serverUrl = null; - if (dbHelper != null) { - if (serverId == null) { - serverUrl = dbHelper.getOnlyServerUrl(); - } else { - serverUrl = getServerUrlForIdentifier(dbHelper, serverId); - } - - if (!TextUtils.isEmpty(serverUrl)) { - bundle.putString("server_url", serverUrl); - mNotificationProps = createProps(bundle); - } - } - - return serverUrl; - } -} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.kt b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.kt new file mode 100644 index 00000000000..192355c6cc8 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.kt @@ -0,0 +1,179 @@ +package com.mattermost.rnbeta + +import android.app.PendingIntent +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.core.app.NotificationCompat +import com.mattermost.helpers.CustomPushNotificationHelper +import com.mattermost.helpers.DatabaseHelper +import com.mattermost.helpers.Network +import com.mattermost.helpers.NotificationHelper +import com.mattermost.helpers.PushNotificationDataHelper +import com.mattermost.helpers.database_extension.getServerUrlForIdentifier +import com.mattermost.share.ShareModule +import com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME +import com.wix.reactnativenotifications.core.AppLaunchHelper +import com.wix.reactnativenotifications.core.AppLifecycleFacade +import com.wix.reactnativenotifications.core.JsIOHelper +import com.wix.reactnativenotifications.core.NotificationIntentAdapter +import com.wix.reactnativenotifications.core.notification.PushNotification +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class CustomPushNotification( + context: Context, + bundle: Bundle, + appLifecycleFacade: AppLifecycleFacade, + appLaunchHelper: AppLaunchHelper, + jsIoHelper: JsIOHelper +) : PushNotification(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper) { + private val dataHelper = PushNotificationDataHelper(context) + + init { + try { + DatabaseHelper.instance?.init(context) + Network.init(context) + NotificationHelper.cleanNotificationPreferencesIfNeeded(context) + } catch (e: Exception) { + e.printStackTrace() + } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onReceived() { + val initialData = mNotificationProps.asBundle() + val type = initialData.getString("type") + val ackId = initialData.getString("ack_id") + val postId = initialData.getString("post_id") + val channelId = initialData.getString("channel_id") + val signature = initialData.getString("signature") + val isIdLoaded = initialData.getString("id_loaded") == "true" + val notificationId = NotificationHelper.getNotificationId(initialData) + val serverUrl = addServerUrlToBundle(initialData) + + GlobalScope.launch { + try { + handlePushNotificationInCoroutine(serverUrl, type, channelId, ackId, isIdLoaded, notificationId, postId, signature) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private suspend fun handlePushNotificationInCoroutine( + serverUrl: String?, + type: String?, + channelId: String?, + ackId: String?, + isIdLoaded: Boolean, + notificationId: Int, + postId: String?, + signature: String? + ) { + if (ackId != null && serverUrl != null) { + val response = ReceiptDelivery.send(ackId, serverUrl, postId, type, isIdLoaded) + if (isIdLoaded && response != null) { + val current = mNotificationProps.asBundle() + if (!current.containsKey("server_url")) { + response.putString("server_url", serverUrl) + } + current.putAll(response) + mNotificationProps = createProps(current) + } + } + + if (!CustomPushNotificationHelper.verifySignature(mContext, signature, serverUrl, ackId)) { + Log.i("Mattermost Notifications Signature verification", "Notification skipped because we could not verify it.") + return + } + + finishProcessingNotification(serverUrl, type, channelId, notificationId) + } + + override fun onOpened() { + mNotificationProps?.let { + digestNotification() + NotificationHelper.clearChannelOrThreadNotifications(mContext, it.asBundle()) + } + } + + private suspend fun finishProcessingNotification(serverUrl: String?, type: String?, channelId: String?, notificationId: Int) { + val isReactInit = mAppLifecycleFacade.isReactInitialized() + + when (type) { + CustomPushNotificationHelper.PUSH_TYPE_MESSAGE, CustomPushNotificationHelper.PUSH_TYPE_SESSION -> { + val shareModule = ShareModule.getInstance() + val currentActivityName = shareModule?.currentActivityName ?: "" + Log.i("ReactNative", currentActivityName) + if (!mAppLifecycleFacade.isAppVisible() || currentActivityName != "MainActivity") { + var createSummary = type == CustomPushNotificationHelper.PUSH_TYPE_MESSAGE + if (type == CustomPushNotificationHelper.PUSH_TYPE_MESSAGE) { + channelId?.let { + val notificationBundle = mNotificationProps.asBundle() + serverUrl?.let { + val notificationResult = dataHelper.fetchAndStoreDataForPushNotification(notificationBundle, isReactInit) + notificationResult?.let { result -> + notificationBundle.putBundle("data", result) + mNotificationProps = createProps(notificationBundle) + } + } + createSummary = NotificationHelper.addNotificationToPreferences(mContext, notificationId, notificationBundle) + } + } + buildNotification(notificationId, createSummary) + } + } + CustomPushNotificationHelper.PUSH_TYPE_CLEAR -> NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle()) + } + + if (isReactInit) { + notifyReceivedToJS() + } + } + + private fun buildNotification(notificationId: Int, createSummary: Boolean) { + val pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps) + val notification = buildNotification(pendingIntent) + if (createSummary) { + val summary = getNotificationSummaryBuilder(pendingIntent).build() + super.postNotification(summary, notificationId + 1) + } + super.postNotification(notification, notificationId) + } + + override fun getNotificationBuilder(intent: PendingIntent): NotificationCompat.Builder { + val bundle = mNotificationProps.asBundle() + return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, false) + } + + private fun getNotificationSummaryBuilder(intent: PendingIntent): NotificationCompat.Builder { + val bundle = mNotificationProps.asBundle() + return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true) + } + + private fun notifyReceivedToJS() { + mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.runningReactContext) + } + + private fun addServerUrlToBundle(bundle: Bundle): String? { + val dbHelper = DatabaseHelper.instance + val serverId = bundle.getString("server_id") + var serverUrl: String? = null + + dbHelper?.let { + serverUrl = if (serverId == null) { + it.onlyServerUrl + } else { + it.getServerUrlForIdentifier(serverId) + } + + if (!serverUrl.isNullOrEmpty()) { + bundle.putString("server_url", serverUrl) + mNotificationProps = createProps(bundle) + } + } + return serverUrl + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.kt b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.kt index b8767cba4f2..6d7693f55bc 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.kt +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.kt @@ -173,10 +173,10 @@ class MainApplication : NavigationApplication(), INotificationsApplication { defaultAppLaunchHelper: AppLaunchHelper? ): IPushNotification { return CustomPushNotification( - context, - bundle, - defaultFacade, - defaultAppLaunchHelper, + context!!, + bundle!!, + defaultFacade!!, + defaultAppLaunchHelper!!, JsIOHelper() ) } diff --git a/patches/@nozbe+watermelondb+0.27.1.patch b/patches/@nozbe+watermelondb+0.27.1.patch index c5d98d1fc7f..fe90ee08685 100644 --- a/patches/@nozbe+watermelondb+0.27.1.patch +++ b/patches/@nozbe+watermelondb+0.27.1.patch @@ -370,7 +370,7 @@ index 027c366..5807e79 100644 unsafeExecute(work: UnsafeExecuteOperations, callback: ResultCallback): void diff --git a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java -index 2f170e0..bac0352 100644 +index 2f170e0..bd87f92 100644 --- a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java +++ b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java @@ -11,6 +11,8 @@ import java.util.Arrays; @@ -414,17 +414,19 @@ index 2f170e0..bac0352 100644 } else { // On some systems there is some kind of lock on `/databases` folder ¯\_(ツ)_/¯ path = context.getDatabasePath("" + name + ".db").getPath().replace("/databases", ""); -@@ -172,6 +190,10 @@ public class WMDatabase { +@@ -172,7 +190,11 @@ public class WMDatabase { }); } +- interface TransactionFunction { + public void unsafeVacuum() { + execute("vacuum"); + } + - interface TransactionFunction { ++ public interface TransactionFunction { void applyTransactionFunction(); } + diff --git a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java index 117b2bc..57e4abb 100644 --- a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java