commit 517aa82157a868b44c19257a68f737bbbcb7ad44
parent f1a368d44e9cd1c9714d0e0ccaf9d38ac2768ae1
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun,  5 Nov 2023 02:32:56 +0100

fix(core): notifications

Diffstat:
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt | 350+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/util/CallbackBuilder.kt | 19++++++++++---------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/util/hook/HookAdapter.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt | 12+++++++-----
4 files changed, 208 insertions(+), 175 deletions(-)

diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt @@ -12,6 +12,10 @@ import android.os.Bundle import android.os.UserHandle import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.data.MediaReferenceType import me.rhunk.snapenhance.common.data.MessageUpdate @@ -33,8 +37,25 @@ import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.media.PreviewUtils import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID +import kotlin.coroutines.suspendCoroutine class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) { + inner class NotificationData( + val tag: String?, + val id: Int, + var notification: Notification, + val userHandle: UserHandle + ) { + fun send() { + XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( + tag, id, notification, userHandle + )) + } + + fun copy(tag: String? = this.tag, id: Int = this.id, notification: Notification = this.notification, userHandle: UserHandle = this.userHandle) = + NotificationData(tag, id, notification, userHandle) + } + companion object{ const val ACTION_REPLY = "me.rhunk.snapenhance.action.notification.REPLY" const val ACTION_DOWNLOAD = "me.rhunk.snapenhance.action.notification.DOWNLOAD" @@ -42,9 +63,10 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN const val SNAPCHAT_NOTIFICATION_GROUP = "snapchat_notification_group" } - private val notificationDataQueue = mutableMapOf<Long, NotificationData>() // messageId => notification - private val cachedMessages = mutableMapOf<String, MutableList<String>>() // conversationId => cached messages - private val notificationIdMap = mutableMapOf<Int, String>() // notificationId => conversationId + @OptIn(ExperimentalCoroutinesApi::class) + private val coroutineDispatcher = Dispatchers.IO.limitedParallelism(1) + private val cachedMessages = mutableMapOf<String, MutableMap<Long, String>>() // conversationId => orderKey, message + private val sentNotifications = mutableMapOf<Int, String>() // notificationId => conversationId private val notifyAsUserMethod by lazy { XposedHelpers.findMethodExact( @@ -56,10 +78,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN ) } - private val fetchConversationWithMessagesMethod by lazy { - context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessages"} - } - private val notificationManager by lazy { context.androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } @@ -70,11 +88,17 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN context.config.messaging.betterNotifications.get() } + private fun newNotificationBuilder(notification: Notification) = XposedHelpers.newInstance( + Notification.Builder::class.java, + context.androidContext, + notification + ) as Notification.Builder + private fun setNotificationText(notification: Notification, conversationId: String) { val messageText = StringBuilder().apply { - cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.forEach { + cachedMessages.computeIfAbsent(conversationId) { sortedMapOf() }.forEach { if (isNotEmpty()) append("\n") - append(it) + append(it.value) } }.toString() @@ -91,14 +115,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } } - private fun setupNotificationActionButtons(contentType: ContentType, conversationId: String, messageId: Long, notificationData: NotificationData) { - - val notificationBuilder = XposedHelpers.newInstance( - Notification.Builder::class.java, - context.androidContext, - notificationData.notification - ) as Notification.Builder - + private fun setupNotificationActionButtons(contentType: ContentType, conversationId: String, message: Message, notificationData: NotificationData) { val actions = mutableListOf<Notification.Action>() actions.addAll(notificationData.notification.actions) @@ -108,7 +125,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN val intent = SnapWidgetBroadcastReceiverHelper.create(remoteAction) { putExtra("conversation_id", conversationId) putExtra("notification_id", notificationData.id) - putExtra("message_id", messageId) + putExtra("client_message_id", message.messageDescriptor!!.messageId!!) } val action = Notification.Action.Builder(null, title, PendingIntent.getBroadcast( @@ -137,7 +154,9 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN betterNotificationFilter.contains("mark_as_read_button") }) {} - notificationBuilder.setActions(*actions.toTypedArray()) + val notificationBuilder = newNotificationBuilder(notificationData.notification).apply { + setActions(*actions.toTypedArray()) + } notificationData.notification = notificationBuilder.build() } @@ -145,15 +164,26 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN context.event.subscribe(SnapWidgetBroadcastReceiveEvent::class) { event -> val intent = event.intent ?: return@subscribe val conversationId = intent.getStringExtra("conversation_id") ?: return@subscribe - val messageId = intent.getLongExtra("message_id", -1) + val clientMessageId = intent.getLongExtra("client_message_id", -1) val notificationId = intent.getIntExtra("notification_id", -1) val updateNotification: (Int, (Notification) -> Unit) -> Unit = { id, notificationBuilder -> notificationManager.activeNotifications.firstOrNull { it.id == id }?.let { notificationBuilder(it.notification) - XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( - it.tag, it.id, it.notification, it.user - )) + NotificationData(it.tag, it.id, it.notification, it.user).send() + } + } + + suspend fun appendNotificationText(input: String) { + cachedMessages.computeIfAbsent(conversationId) { sortedMapOf() }.let { + it[(it.keys.lastOrNull() ?: 0) + 1L] = input + } + + withContext(Dispatchers.Main) { + updateNotification(notificationId) { notification -> + notification.flags = notification.flags or Notification.FLAG_ONLY_ALERT_ONCE + setNotificationText(notification, conversationId) + } } } @@ -161,23 +191,22 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN ACTION_REPLY -> { val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input") .toString() + val myUser = context.database.myUserId.let { context.database.getFriendInfo(it) } ?: return@subscribe - context.database.myUserId.let { context.database.getFriendInfo(it) }?.let { myUser -> - cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input") - - updateNotification(notificationId) { notification -> - notification.flags = notification.flags or Notification.FLAG_ONLY_ALERT_ONCE - setNotificationText(notification, conversationId) + context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = { + context.longToast("Failed to send message: $it") + context.coroutineScope.launch(coroutineDispatcher) { + appendNotificationText("Failed to send message: $it") } - - context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = { - context.longToast("Failed to send message: $it") - }) - } + }, onSuccess = { + context.coroutineScope.launch(coroutineDispatcher) { + appendNotificationText("${myUser.displayName ?: myUser.mutableUsername}: $input") + } + }) } ACTION_DOWNLOAD -> { runCatching { - context.feature(MediaDownloader::class).downloadMessageId(messageId, isPreview = false) + context.feature(MediaDownloader::class).downloadMessageId(clientMessageId, isPreview = false) }.onFailure { context.longToast(it) } @@ -193,7 +222,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN conversationManager.displayedMessages( conversationId, - messageId, + clientMessageId, onResult = { if (it != null) { context.log.error("Failed to mark conversation as read: $it") @@ -202,10 +231,10 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } ) - val conversationMessage = context.database.getConversationMessageFromId(messageId) ?: return@subscribe + val conversationMessage = context.database.getConversationMessageFromId(clientMessageId) ?: return@subscribe if (conversationMessage.contentType == ContentType.SNAP.id) { - conversationManager.updateMessage(conversationId, messageId, MessageUpdate.READ) { + conversationManager.updateMessage(conversationId, clientMessageId, MessageUpdate.READ) { if (it != null) { context.log.error("Failed to open snap: $it") context.shortToast("Failed to open snap") @@ -225,117 +254,111 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } } - private fun fetchMessagesResult(conversationId: String, messages: List<Message>) { - val sendNotificationData = { notificationData: NotificationData, forceCreate: Boolean -> - val notificationId = if (forceCreate) System.nanoTime().toInt() else notificationData.id - notificationIdMap.computeIfAbsent(notificationId) { conversationId } - if (betterNotificationFilter.contains("group")) { - runCatching { - notificationData.notification.setObjectField("mGroupKey", SNAPCHAT_NOTIFICATION_GROUP) - - val summaryNotification = Notification.Builder(context.androidContext, notificationData.notification.channelId) - .setSmallIcon(notificationData.notification.smallIcon) - .setGroup(SNAPCHAT_NOTIFICATION_GROUP) - .setGroupSummary(true) - .setAutoCancel(true) - .setOnlyAlertOnce(true) - .build() - - if (notificationManager.activeNotifications.firstOrNull { it.notification.flags and Notification.FLAG_GROUP_SUMMARY != 0 } == null) { - notificationManager.notify(notificationData.tag, notificationData.id, summaryNotification) - } - }.onFailure { - context.log.warn("Failed to set notification group key: ${it.stackTraceToString()}", featureKey) + private fun sendNotification(message: Message, notificationData: NotificationData, forceCreate: Boolean) { + val conversationId = message.messageDescriptor?.conversationId.toString() + val notificationId = if (forceCreate) System.nanoTime().toInt() else notificationData.id + sentNotifications.computeIfAbsent(notificationId) { conversationId } + + if (betterNotificationFilter.contains("group")) { + runCatching { + if (notificationManager.activeNotifications.firstOrNull { + it.notification.flags and Notification.FLAG_GROUP_SUMMARY != 0 + } == null) { + notificationManager.notify( + notificationData.tag, + System.nanoTime().toInt(), + Notification.Builder(context.androidContext, notificationData.notification.channelId) + .setSmallIcon(notificationData.notification.smallIcon) + .setGroup(SNAPCHAT_NOTIFICATION_GROUP) + .setGroupSummary(true) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .build() + ) } + }.onFailure { + context.log.warn("Failed to set notification group key: ${it.stackTraceToString()}", featureKey) } - - XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( - notificationData.tag, if (forceCreate) System.nanoTime().toInt() else notificationData.id, notificationData.notification, notificationData.userHandle - )) } - synchronized(notificationDataQueue) { - notificationDataQueue.entries.onEach { (messageId, notificationData) -> - val snapMessage = messages.firstOrNull { message -> message.orderKey == messageId } ?: return - val senderUsername by lazy { - context.database.getFriendInfo(snapMessage.senderId.toString())?.let { - it.displayName ?: it.mutableUsername - } - } + notificationData.copy(id = notificationId).also { + setupNotificationActionButtons(message.messageContent!!.contentType!!, conversationId, message, it) + }.send() + } - val contentType = snapMessage.messageContent!!.contentType ?: return@onEach - val contentData = snapMessage.messageContent!!.content!! + private fun onMessageReceived(data: NotificationData, message: Message) { + val conversationId = message.messageDescriptor?.conversationId.toString() + val orderKey = message.orderKey ?: return + val senderUsername by lazy { + context.database.getFriendInfo(message.senderId.toString())?.let { + it.displayName ?: it.mutableUsername + } ?: "Unknown" + } - val formatUsername: (String) -> String = { "$senderUsername: $it" } - val notificationCache = cachedMessages.let { it.computeIfAbsent(conversationId) { mutableListOf() } } - val appendNotifications: () -> Unit = { setNotificationText(notificationData.notification, conversationId)} + val formatUsername: (String) -> String = { "$senderUsername: $it" } + val notificationCache = cachedMessages.let { it.computeIfAbsent(conversationId) { sortedMapOf() } } + val appendNotifications: () -> Unit = { setNotificationText(data.notification, conversationId)} - setupNotificationActionButtons(contentType, conversationId, snapMessage.messageDescriptor!!.messageId!!, notificationData) - when (contentType) { - ContentType.NOTE -> { - notificationCache.add(formatUsername("sent audio note")) - appendNotifications() - } - ContentType.CHAT -> { - ProtoReader(contentData).getString(2, 1)?.trim()?.let { - notificationCache.add(formatUsername(it)) - } - appendNotifications() - } - ContentType.SNAP, ContentType.EXTERNAL_MEDIA -> { - val mediaReferences = MessageDecoder.getMediaReferences( - messageContent = context.gson.toJsonTree(snapMessage.messageContent!!.instanceNonNull()) - ) + when (val contentType = message.messageContent!!.contentType) { + ContentType.NOTE -> { + notificationCache[orderKey] = formatUsername("sent audio note") + appendNotifications() + } + ContentType.CHAT -> { + ProtoReader(message.messageContent!!.content!!).getString(2, 1)?.trim()?.let { + notificationCache[orderKey] = formatUsername(it) + } + appendNotifications() + } + ContentType.SNAP, ContentType.EXTERNAL_MEDIA -> { + val mediaReferences = MessageDecoder.getMediaReferences( + messageContent = context.gson.toJsonTree(message.messageContent!!.instanceNonNull()) + ) - val mediaReferenceKeys = mediaReferences.map { reference -> - reference.asJsonObject.getAsJsonArray("mContentObject").map { it.asByte }.toByteArray() - } + val mediaReferenceKeys = mediaReferences.map { reference -> + reference.asJsonObject.getAsJsonArray("mContentObject").map { it.asByte }.toByteArray() + } - MessageDecoder.decode(snapMessage.messageContent!!).firstOrNull()?.also { media -> - val mediaType = MediaReferenceType.valueOf(mediaReferences.first().asJsonObject["mMediaType"].asString) + MessageDecoder.decode(message.messageContent!!).firstOrNull()?.also { media -> + val mediaType = MediaReferenceType.valueOf(mediaReferences.first().asJsonObject["mMediaType"].asString) - runCatching { - val downloadedMedia = RemoteMediaResolver.downloadBoltMedia(mediaReferenceKeys.first(), decryptionCallback = { - media.attachmentInfo?.encryption?.decryptInputStream(it) ?: it - }) ?: throw Throwable("Unable to download media") + runCatching { + val downloadedMedia = RemoteMediaResolver.downloadBoltMedia(mediaReferenceKeys.first(), decryptionCallback = { + media.attachmentInfo?.encryption?.decryptInputStream(it) ?: it + }) ?: throw Throwable("Unable to download media") - val downloadedMedias = mutableMapOf<SplitMediaAssetType, ByteArray>() + val downloadedMedias = mutableMapOf<SplitMediaAssetType, ByteArray>() - MediaDownloaderHelper.getSplitElements(downloadedMedia.inputStream()) { type, inputStream -> - downloadedMedias[type] = inputStream.readBytes() - } + MediaDownloaderHelper.getSplitElements(downloadedMedia.inputStream()) { type, inputStream -> + downloadedMedias[type] = inputStream.readBytes() + } - var bitmapPreview = PreviewUtils.createPreview(downloadedMedias[SplitMediaAssetType.ORIGINAL]!!, mediaType.name.contains("VIDEO"))!! + var bitmapPreview = PreviewUtils.createPreview(downloadedMedias[SplitMediaAssetType.ORIGINAL]!!, mediaType.name.contains("VIDEO"))!! - downloadedMedias[SplitMediaAssetType.OVERLAY]?.let { - bitmapPreview = PreviewUtils.mergeBitmapOverlay(bitmapPreview, BitmapFactory.decodeByteArray(it, 0, it.size)) - } + downloadedMedias[SplitMediaAssetType.OVERLAY]?.let { + bitmapPreview = PreviewUtils.mergeBitmapOverlay(bitmapPreview, BitmapFactory.decodeByteArray(it, 0, it.size)) + } - val notificationBuilder = XposedHelpers.newInstance( - Notification.Builder::class.java, - context.androidContext, - notificationData.notification - ) as Notification.Builder - notificationBuilder.setLargeIcon(bitmapPreview) - notificationBuilder.style = Notification.BigPictureStyle().bigPicture(bitmapPreview).bigLargeIcon(null as Bitmap?) - - sendNotificationData(notificationData.copy(notification = notificationBuilder.build()), true) - return@onEach - }.onFailure { - context.log.error("Failed to send preview notification", it) - } + val notificationBuilder = newNotificationBuilder(data.notification).apply { + setLargeIcon(bitmapPreview) + style = Notification.BigPictureStyle().bigPicture(bitmapPreview).bigLargeIcon(null as Bitmap?) } - } - else -> { - notificationCache.add(formatUsername("sent ${contentType.name.lowercase()}")) - appendNotifications() + + sendNotification(message, data.copy(notification = notificationBuilder.build()), true) + return + }.onFailure { + context.log.error("Failed to send preview notification", it) } } - - sendNotificationData(notificationData, false) - }.clear() + } + else -> { + notificationCache[orderKey] = formatUsername("sent ${contentType?.name?.lowercase()}") + appendNotifications() + } } + + sendNotification(message, data, false) } override fun init() { @@ -343,39 +366,53 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN notifyAsUserMethod.hook(HookStage.BEFORE) { param -> val notificationData = NotificationData(param.argNullable(0), param.arg(1), param.arg(2), param.arg(3)) + val extras = notificationData.notification.extras.getBundle("system_notification_extras")?: return@hook - val extras: Bundle = notificationData.notification.extras.getBundle("system_notification_extras")?: return@hook - - val messageId = extras.getString("message_id") ?: return@hook - val notificationType = extras.getString("notification_type") ?: return@hook - val conversationId = extras.getString("conversation_id") ?: return@hook + if (betterNotificationFilter.contains("group")) { + notificationData.notification.setObjectField("mGroupKey", SNAPCHAT_NOTIFICATION_GROUP) + } - if (betterNotificationFilter.map { it.uppercase() }.none { - notificationType.contains(it) - }) return@hook + val conversationId = extras.getString("conversation_id").also { id -> + sentNotifications.computeIfAbsent(notificationData.id) { id ?: "" } + } ?: return@hook - synchronized(notificationDataQueue) { - notificationDataQueue[messageId.toLong()] = notificationData - } + val serverMessageId = extras.getString("message_id") ?: return@hook + val notificationType = extras.getString("notification_type") ?: return@hook - context.feature(Messaging::class).conversationManager?.fetchConversationWithMessages(conversationId, onSuccess = { messages -> - fetchMessagesResult(conversationId, messages) - }, onError = { - context.log.error("Failed to fetch conversation with messages: $it") - }) + if (betterNotificationFilter.none { notificationType.contains(it, ignoreCase = true) }) return@hook param.setResult(null) + val conversationManager = context.feature(Messaging::class).conversationManager ?: return@hook + + context.coroutineScope.launch(coroutineDispatcher) { + suspendCoroutine { continuation -> + conversationManager.fetchMessageByServerId(conversationId, serverMessageId, onSuccess = { + onMessageReceived(notificationData, it) + continuation.resumeWith(Result.success(Unit)) + }, onError = { + context.log.error("Failed to fetch message id ${serverMessageId}: $it") + continuation.resumeWith(Result.success(Unit)) + }) + } + } } - XposedHelpers.findMethodExact( - NotificationManager::class.java, - "cancelAsUser", String::class.java, - Int::class.javaPrimitiveType, - UserHandle::class.java - ).hook(HookStage.BEFORE) { param -> + NotificationManager::class.java.declaredMethods.find { + it.name == "cancelAsUser" + }?.hook(HookStage.AFTER) { param -> val notificationId = param.arg<Int>(1) - notificationIdMap[notificationId]?.let { - cachedMessages[it]?.clear() + + context.coroutineScope.launch(coroutineDispatcher) { + sentNotifications[notificationId]?.let { + cachedMessages[it]?.clear() + } + sentNotifications.remove(notificationId) + } + + notificationManager.activeNotifications.let { notifications -> + if (notifications.all { it.notification.flags and Notification.FLAG_GROUP_SUMMARY != 0 }) { + notifications.forEach { param.invokeOriginal(arrayOf(it.tag, it.id, it.user)) } + } } } @@ -398,11 +435,4 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } } } - - data class NotificationData( - val tag: String?, - val id: Int, - var notification: Notification, - val userHandle: UserHandle - ) } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/CallbackBuilder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/CallbackBuilder.kt @@ -73,15 +73,16 @@ class CallbackBuilder( //compute the args for the constructor with null or default primitive values val args = constructor.parameterTypes.map { type: Class<*> -> if (type.isPrimitive) { - when (type.name) { - "boolean" -> return@map false - "byte" -> return@map 0.toByte() - "char" -> return@map 0.toChar() - "short" -> return@map 0.toShort() - "int" -> return@map 0 - "long" -> return@map 0L - "float" -> return@map 0f - "double" -> return@map 0.0 + return@map when (type.name) { + "boolean" -> false + "byte" -> 0.toByte() + "char" -> 0.toChar() + "short" -> 0.toShort() + "int" -> 0 + "long" -> 0L + "float" -> 0f + "double" -> 0.0 + else -> null } } null diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/hook/HookAdapter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/hook/HookAdapter.kt @@ -58,7 +58,7 @@ class HookAdapter( return XposedBridge.invokeOriginalMethod(method(), thisObject(), args()) } - fun invokeOriginal(args: Array<Any>): Any? { + fun invokeOriginal(args: Array<Any?>): Any? { return XposedBridge.invokeOriginalMethod(method(), thisObject(), args) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt @@ -3,6 +3,7 @@ package me.rhunk.snapenhance.core.wrapper.impl import me.rhunk.snapenhance.common.data.MessageUpdate import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.util.CallbackBuilder +import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.wrapper.AbstractWrapper typealias CallbackResult = (error: String?) -> Unit @@ -63,7 +64,7 @@ class ConversationManager( fun displayedMessages(conversationId: String, messageId: Long, onResult: CallbackResult = {}) { displayedMessagesMethod.invoke( instanceNonNull(), - conversationId.toSnapUUID(), + conversationId.toSnapUUID().instanceNonNull(), messageId, CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) .override("onSuccess") { onResult(null) } @@ -87,16 +88,17 @@ class ConversationManager( } fun fetchMessageByServerId(conversationId: String, serverMessageId: String, onSuccess: (Message) -> Unit, onError: (error: String) -> Unit) { - val serverMessageIdentifier = context.classCache.serverMessageIdentifier - .getConstructor(context.classCache.snapUUID, Long::class.javaPrimitiveType) - .newInstance(conversationId.toSnapUUID().instanceNonNull(), serverMessageId.toLong()) + val serverMessageIdentifier = CallbackBuilder.createEmptyObject(context.classCache.serverMessageIdentifier.constructors.first())?.apply { + setObjectField("mServerConversationId", conversationId.toSnapUUID().instanceNonNull()) + setObjectField("mServerMessageId", serverMessageId.toLong()) + } fetchMessageByServerId.invoke( instanceNonNull(), serverMessageIdentifier, CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessageCallback")) .override("onFetchMessageComplete") { param -> - onSuccess(Message(param.arg(1))) + onSuccess(Message(param.arg(0))) } .override("onError") { onError(it.arg<Any>(0).toString())