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:
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)