commit 645b7befa91c0af4f69e7e69067942db21b55ec9
parent 97aed78894b02f8c19354243d4cfe81d9e384126
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 23 May 2024 00:27:51 +0200

refactor: better notifications
- caption in notifications
- media preview
- stack media messages
- fix typing notification

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 54+++++++++++++++++++++++++++++++++++++++++++-----------
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt | 26++++++++++++++++----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt | 19+++++++++++--------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt | 107++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ConversationExporter.kt | 2+-
6 files changed, 126 insertions(+), 84 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -600,7 +600,49 @@ }, "better_notifications": { "name": "Better Notifications", - "description": "Adds more information in received notifications" + "description": "Adds more information in received notifications", + "properties": { + "group_notifications": { + "name": "Group Notifications", + "description": "Group notifications into a single one" + }, + "chat_preview": { + "name": "Chat Preview", + "description": "Shows a preview of received messages in the notification" + }, + "media_preview": { + "name": "Media Preview", + "description": "Shows a preview of the selected media types in the notification" + }, + "media_caption": { + "name": "Media Caption", + "description": "Shows the attached caption of media in the notification" + }, + "stacked_media_messages": { + "name": "Stacked Media Messages", + "description": "Combines multiple media messages into one text notification when they cannot be previewed. Use in combination with Chat Preview" + }, + "friend_add_source": { + "name": "Friend Add Source", + "description": "Shows the source of a friend request in the notification" + }, + "reply_button": { + "name": "Reply Button", + "description": "Adds a reply button to the notification" + }, + "download_button": { + "name": "Download Button", + "description": "Allows you to download media from the notification" + }, + "mark_as_read_button": { + "name": "Mark as Read Button", + "description": "Allows you to mark a message as read from the notification" + }, + "mark_as_read_and_save_in_chat": { + "name": "Mark as Read and Save in Chat", + "description": "Adds a mark as read and save in chat button to the notification" + } + } }, "notification_blacklist": { "name": "Notification Blacklist", @@ -1048,16 +1090,6 @@ "always_light": "Always Light", "always_dark": "Always Dark" }, - "better_notifications": { - "chat_preview": "Show a preview of chat", - "media_preview": "Show a preview of media", - "reply_button": "Add reply button", - "download_button": "Add download button", - "mark_as_read_button": "Mark as Read button", - "mark_as_read_and_save_in_chat": "Save in Chat when marking as read (depends on Auto Save)", - "friend_add_source": "Show friend add source", - "group": "Group notifications" - }, "theme_picker": { "amoled_dark_mode": "AMOLED Dark Mode", "custom": "Custom Colors", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt @@ -48,6 +48,21 @@ class MessagingTweaks : ConfigContainer() { } } + class BetterNotifications: ConfigContainer() { + val groupNotifications = boolean("group_notifications") + val chatPreview = boolean("chat_preview") + val mediaPreview = multiple("media_preview", "SNAP", "NOTE", "EXTERNAL_MEDIA", "STICKER") { + customOptionTranslationPath = "content_type" + } + val mediaCaption = boolean("media_caption") + val stackedMediaMessages = boolean("stacked_media_messages") + val friendAddSource = boolean("friend_add_source") + val replyButton = boolean("reply_button") { addNotices(FeatureNotice.UNSTABLE) } + val downloadButton = boolean("download_button") + val markAsReadButton = boolean("mark_as_read_button") { addNotices(FeatureNotice.UNSTABLE) } + val markAsReadAndSaveInChat = boolean("mark_as_read_and_save_in_chat") { addNotices(FeatureNotice.UNSTABLE) } + } + val bypassScreenshotDetection = boolean("bypass_screenshot_detection") { requireRestart() } val anonymousStoryViewing = boolean("anonymous_story_viewing") val preventStoryRewatchIndicator = boolean("prevent_story_rewatch_indicator") { requireRestart() } @@ -81,16 +96,7 @@ class MessagingTweaks : ConfigContainer() { "bitmoji_background_changes", "bitmoji_scene_changes", ) { requireRestart() } - val betterNotifications = multiple("better_notifications", - "chat_preview", - "media_preview", - "reply_button", - "download_button", - "mark_as_read_button", - "mark_as_read_and_save_in_chat", - "friend_add_source", - "group" - ) { requireRestart() } + val betterNotifications = container("better_notifications", BetterNotifications()) { requireRestart() } val notificationBlacklist = multiple("notification_blacklist", *NotificationType.getIncomingValues().map { it.key }.toTypedArray()) { customOptionTranslationPath = "features.options.notifications" } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt @@ -574,7 +574,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp var previewBitmap: Bitmap? = null val previewCoroutine = context.coroutineScope.launch { runCatching { - attachment.openStream { attachmentStream -> + attachment.openStream { attachmentStream, _ -> val downloadedMediaList = mutableMapOf<SplitMediaAssetType, ByteArray>() MediaDownloaderHelper.getSplitElements(attachmentStream!!) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt @@ -31,18 +31,21 @@ data class DecodedAttachment( } @OptIn(ExperimentalEncodingApi::class) - inline fun openStream(crossinline callback: (InputStream?) -> Unit) { + inline fun openStream(crossinline callback: (mediaStream: InputStream?, length: Long) -> Unit) { boltKey?.let { mediaUrlKey -> - RemoteMediaResolver.downloadBoltMedia(Base64.decode(mediaUrlKey), decryptionCallback = { + RemoteMediaResolver.downloadBoltMedia(Base64.UrlSafe.decode(mediaUrlKey), decryptionCallback = { attachmentInfo?.encryption?.decryptInputStream(it) ?: it - }, resultCallback = { inputStream, _ -> - callback(inputStream) + }, resultCallback = { inputStream, length -> + callback(inputStream, length) }) } ?: directUrl?.let { rawMediaUrl -> - URL(rawMediaUrl).openStream().let { inputStream -> - attachmentInfo?.encryption?.decryptInputStream(inputStream) ?: inputStream - }.use(callback) - } ?: callback(null) + val connection = URL(rawMediaUrl).openConnection() + connection.getInputStream().let { + attachmentInfo?.encryption?.decryptInputStream(it) ?: it + }.use { + callback(it, connection.contentLengthLong) + } + } ?: callback(null, 0) } fun createInputMedia( 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 @@ -14,13 +14,11 @@ import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers import kotlinx.coroutines.* import me.rhunk.snapenhance.common.data.ContentType -import me.rhunk.snapenhance.common.data.MediaReferenceType +import me.rhunk.snapenhance.common.data.FileType import me.rhunk.snapenhance.common.data.MessageUpdate import me.rhunk.snapenhance.common.data.NotificationType import me.rhunk.snapenhance.common.data.download.SplitMediaAssetType -import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper -import me.rhunk.snapenhance.common.util.snap.RemoteMediaResolver import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent import me.rhunk.snapenhance.core.features.Feature @@ -81,10 +79,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } private val translations by lazy { context.translation.getCategory("better_notifications") } - - private val betterNotificationFilter by lazy { - context.config.messaging.betterNotifications.get() - } + private val config by lazy { context.config.messaging.betterNotifications } private fun newNotificationBuilder(notification: Notification) = XposedHelpers.newInstance( Notification.Builder::class.java, @@ -138,7 +133,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } newAction(translations["button.reply"], ACTION_REPLY, { - betterNotificationFilter.contains("reply_button") && contentType == ContentType.CHAT + config.replyButton.get() && contentType == ContentType.CHAT }) { val chatReplyInput = RemoteInput.Builder("chat_reply_input") .setLabel(translations["button.reply"]) @@ -147,11 +142,11 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } newAction(translations["button.download"], ACTION_DOWNLOAD, { - betterNotificationFilter.contains("download_button") && betterNotificationFilter.contains("media_preview") && (contentType == ContentType.EXTERNAL_MEDIA || contentType == ContentType.SNAP) + config.downloadButton.get() && config.mediaPreview.get().contains(contentType.name) }) {} newAction(translations["button.mark_as_read"], ACTION_MARK_AS_READ, { - betterNotificationFilter.contains("mark_as_read_button") + config.markAsReadButton.get() }) {} val notificationBuilder = newNotificationBuilder(notificationData.notification).apply { @@ -232,7 +227,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } ) - if (betterNotificationFilter.contains("mark_as_read_and_save_in_chat")) { + if (config.markAsReadAndSaveInChat.get()) { val messaging = context.feature(Messaging::class) val autoSave = context.feature(AutoSave::class) @@ -285,7 +280,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN val notificationId = if (forceCreate) System.nanoTime().toInt() else message.messageDescriptor?.conversationId?.toBytes().contentHashCode() sentNotifications.computeIfAbsent(notificationId) { conversationId } - if (betterNotificationFilter.contains("group")) { + if (config.groupNotifications.get()) { runCatching { if (notificationManager.activeNotifications.firstOrNull { it.notification.flags and Notification.FLAG_GROUP_SUMMARY != 0 @@ -336,41 +331,23 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN }[orderKey] = if (includeUsername) "$senderUsername: $text" else text } - when ( - contentType.takeIf { - (it != ContentType.SNAP && it != ContentType.EXTERNAL_MEDIA) || betterNotificationFilter.contains("media_preview") - } ?: ContentType.UNKNOWN - ) { - ContentType.CHAT -> { - ProtoReader(message.messageContent!!.content!!).getString(2, 1)?.trim()?.let { - setNotificationText(it) - } - computeMessages() - } - 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() - } - - 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") - + if (config.mediaPreview.get().contains(contentType.name)) { + MessageDecoder.decode(message.messageContent!!).firstOrNull()?.also { media -> + runCatching { + media.openStream { mediaStream, length -> + if (mediaStream == null || length > 25 * 1024 * 1024) { + context.log.error("Failed to open media stream or media is too large") + sendNotification(message, data, true) + return@openStream + } val downloadedMedias = mutableMapOf<SplitMediaAssetType, ByteArray>() - MediaDownloaderHelper.getSplitElements(downloadedMedia.inputStream()) { type, inputStream -> + MediaDownloaderHelper.getSplitElements(mediaStream) { type, inputStream -> downloadedMedias[type] = inputStream.readBytes() } - var bitmapPreview = PreviewUtils.createPreview(downloadedMedias[SplitMediaAssetType.ORIGINAL]!!, mediaType.name.contains("VIDEO"))!! + val originalMedia = downloadedMedias[SplitMediaAssetType.ORIGINAL]!! + var bitmapPreview = PreviewUtils.createPreview(originalMedia, FileType.fromByteArray(originalMedia).isVideo)!! downloadedMedias[SplitMediaAssetType.OVERLAY]?.let { bitmapPreview = PreviewUtils.mergeBitmapOverlay(bitmapPreview, BitmapFactory.decodeByteArray(it, 0, it.size)) @@ -381,19 +358,43 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN style = Notification.BigPictureStyle().bigPicture(bitmapPreview).bigLargeIcon(null as Bitmap?) } + if (config.mediaCaption.get()) { + message.serialize()?.let { + notificationBuilder.setContentText(it) + } + } + sendNotification(message, data.copy(notification = notificationBuilder.build()), true) - return - }.onFailure { - context.log.error("Failed to send preview notification", it) } + return + }.onFailure { + context.log.error("Failed to send preview notification", it) + sendNotification(message, data, true) + return } } - else -> { - setNotificationText("[" + context.translation.getCategory("content_type")[contentType.name] + "]") - computeMessages() + } + + if (config.chatPreview.get()) { + if (contentType == ContentType.CHAT) { + setNotificationText(message.serialize() ?: "[Failed to parse message]") + } else { + if (config.stackedMediaMessages.get()) { + setNotificationText(buildString { + append("[") + append(context.translation.getCategory("content_type")[contentType.name]) + append("]") + if (config.mediaCaption.get()) { + message.serialize()?.let { append(" $it") } + } + }) + } else { + sendNotification(message, data, true) + return + } } + computeMessages() } - if (!betterNotificationFilter.contains("chat_preview")) return sendNotification(message, data, false) } @@ -419,7 +420,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN 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 - if (betterNotificationFilter.contains("group")) { + if (config.groupNotifications.get()) { notificationData.notification.setObjectField("mGroupKey", SNAPCHAT_NOTIFICATION_GROUP) } @@ -430,7 +431,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN return@hook } - if (notificationType == "addfriend" && betterNotificationFilter.contains("friend_add_source")) { + if (notificationType == "addfriend" && config.friendAddSource.get()) { val userId = notificationData.notification.shortcutId?.split("|")?.lastOrNull() ?: return@hook runBlocking { var addSource: String? = null @@ -446,8 +447,8 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN return@hook } - if (!betterNotificationFilter.contains("chat_preview") && !betterNotificationFilter.contains("media_preview")) return@hook - if (notificationType == "typing") return@hook + if (!config.chatPreview.get() && config.mediaPreview.isEmpty()) return@hook + if (notificationType.endsWith("typing")) return@hook val serverMessageId = extras.getString("message_id") ?: return@hook val conversationId = extras.getString("conversation_id").also { id -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ConversationExporter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/ConversationExporter.kt @@ -114,7 +114,7 @@ class ConversationExporter( for (i in 0..5) { printLog("downloading ${attachment.boltKey ?: attachment.directUrl}... (attempt ${i + 1}/5)") runCatching { - attachment.openStream { downloadedInputStream -> + attachment.openStream { downloadedInputStream, _ -> MediaDownloaderHelper.getSplitElements(downloadedInputStream!!) { type, splitInputStream -> val mediaKey = "${type}_${attachment.mediaUniqueId}" val bufferedInputStream = BufferedInputStream(splitInputStream)