commit 0f711b698ee14668bd29d337fe3e07962091a60c parent 88bcfe96516d513e4ef4725ba36b0ed6f4dc9be0 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 28 May 2023 17:27:49 +0200 refactor: optimize code & improve ui Diffstat:
21 files changed, 224 insertions(+), 219 deletions(-)
diff --git a/README.md b/README.md @@ -31,7 +31,7 @@ If the software is redistributed, it must remain under the same GPLv3 license an </details> <details open> - <summary>Spy</summary> + <summary>Spying</summary> - Anonymous story viewing (other users won't know you viewed their story) - Message logger (log all messages sent and received, you will be able to see deleted messages) @@ -59,3 +59,8 @@ You can also download the latest build from the [actions](https://github.com/rhu - BCH: qpu57a05kqljjadvpgjc6t894apprvth9slvlj4vpj - Bitcoin: bc1qaqnfn6mauzhmx0e6kkenh2wh4r6js0vh5vel92 - ETH: 0x0760987491e9de53A73fd87F092Bd432a227Ee92 + +## Contributors +- [rathmerdominik](https://github.com/rathmerdominik) +- [Flole998](https://github.com/Flole998) +- [authorisation](https://github.com/authorisation/)+ \ No newline at end of file diff --git a/app/src/main/assets/lang/en_US.json b/app/src/main/assets/lang/en_US.json @@ -1,13 +1,14 @@ { "category": { "general": "General", - "spy": "Spy", + "spying": "Spying", "media_download": "Media Downloader", "privacy": "Privacy", "ui": "UI", "extras": "Extras", "tweaks": "Tweaks", - "experimental": "Experimental" + "experimental": "Experimental", + "debugging": "Debugging" }, "action": { @@ -21,7 +22,7 @@ "prevent_read_receipts": "Prevent Read Receipts", "hide_bitmoji_presence": "Hide Bitmoji Presence", "show_message_content_in_notifications": "Show Message Content In Notifications", - "notification_filter": "Notification Filter", + "notification_blacklist": "Notification Blacklist", "message_logger": "Message Logger", "unlimited_snap_view_time": "Unlimited Snap View Time", "auto_download_snaps": "Auto Download Snaps", @@ -61,7 +62,7 @@ "option": { "property": { - "notification_filter": { + "notification_blacklist": { "chat": "Chat", "snap": "Snap", "typing": "Typing" diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt @@ -4,7 +4,7 @@ enum class ConfigCategory( val key: String ) { GENERAL("category.general"), - SPY("category.spy"), + SPYING("category.spying"), MEDIA_DOWNLOADER("category.media_download"), PRIVACY("category.privacy"), UI("category.ui"), diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt @@ -25,25 +25,25 @@ enum class ConfigProperty( PREVENT_READ_RECEIPTS( "property.prevent_read_receipts", "description.prevent_read_receipts", - ConfigCategory.SPY, + ConfigCategory.SPYING, ConfigStateValue(false) ), HIDE_BITMOJI_PRESENCE( "property.hide_bitmoji_presence", "description.hide_bitmoji_presence", - ConfigCategory.SPY, + ConfigCategory.SPYING, ConfigStateValue(false) ), SHOW_MESSAGE_CONTENT_IN_NOTIFICATIONS( "property.show_message_content_in_notifications", "description.show_message_content_in_notifications", - ConfigCategory.SPY, + ConfigCategory.SPYING, ConfigStateValue(false) ), - NOTIFICATION_FILTER( - "property.notification_filter", - "description.notification_filter", - ConfigCategory.SPY, + NOTIFICATION_BLACKLIST( + "property.notification_blacklist", + "description.notification_blacklist", + ConfigCategory.SPYING, ConfigStateListValue( listOf("snap", "chat", "typing"), mutableMapOf( @@ -54,8 +54,8 @@ enum class ConfigProperty( ) ), - MESSAGE_LOGGER("property.message_logger", "description.message_logger", ConfigCategory.SPY, ConfigStateValue(false)), - UNLIMITED_SNAP_VIEW_TIME("property.unlimited_snap_view_time", "description.unlimited_snap_view_time", ConfigCategory.SPY, ConfigStateValue(false)), + MESSAGE_LOGGER("property.message_logger", "description.message_logger", ConfigCategory.SPYING, ConfigStateValue(false)), + UNLIMITED_SNAP_VIEW_TIME("property.unlimited_snap_view_time", "description.unlimited_snap_view_time", ConfigCategory.SPYING, ConfigStateValue(false)), AUTO_DOWNLOAD_SNAPS( "property.auto_download_snaps", @@ -134,46 +134,46 @@ enum class ConfigProperty( ConfigIntegerValue(20) ), - GALLERY_MEDIA_SEND_OVERRIDE( - "property.gallery_media_send_override", - "description.gallery_media_send_override", - ConfigCategory.EXTRAS, - ConfigStateSelection( - listOf("OFF", "NOTE", "SNAP"), - "OFF" - ) - ), AUTO_SAVE("property.auto_save", "description.auto_save", ConfigCategory.EXTRAS, ConfigStateValue(false)), ANTI_AUTO_SAVE("property.anti_auto_save", "description.anti_auto_save", ConfigCategory.EXTRAS, ConfigStateValue(false)), SNAPCHAT_PLUS("property.snapchat_plus", "description.snapchat_plus", ConfigCategory.EXTRAS, ConfigStateValue(false)), - DISABLE_SNAP_SPLITTING( "property.disable_snap_splitting", "description.disable_snap_splitting", - ConfigCategory.TWEAKS, + ConfigCategory.EXTRAS, ConfigStateValue(false) ), DISABLE_VIDEO_LENGTH_RESTRICTION( "property.disable_video_length_restriction", "description.disable_video_length_restriction", - ConfigCategory.TWEAKS, + ConfigCategory.EXTRAS, ConfigStateValue(false) ), OVERRIDE_MEDIA_QUALITY( "property.override_media_quality", "description.override_media_quality", - ConfigCategory.TWEAKS, + ConfigCategory.EXTRAS, ConfigStateValue(false) ), MEDIA_QUALITY_LEVEL( "property.media_quality_level", "description.media_quality_level", - ConfigCategory.TWEAKS, + ConfigCategory.EXTRAS, ConfigStateSelection( listOf("LEVEL_NONE", "LEVEL_1", "LEVEL_2", "LEVEL_3", "LEVEL_4", "LEVEL_5", "LEVEL_6", "LEVEL_7", "LEVEL_MAX"), "LEVEL_NONE" ) ), + GALLERY_MEDIA_SEND_OVERRIDE( + "property.gallery_media_send_override", + "description.gallery_media_send_override", + ConfigCategory.EXTRAS, + ConfigStateSelection( + listOf("OFF", "NOTE", "SNAP"), + "OFF" + ) + ), + REMOVE_VOICE_RECORD_BUTTON( "property.remove_voice_record_button", "description.remove_voice_record_button", @@ -233,10 +233,6 @@ enum class ConfigProperty( ); companion object { - fun fromNameKey(nameKey: String): ConfigProperty? { - return values().find { it.nameKey == nameKey } - } - fun sortedByCategory(): List<ConfigProperty> { return values().sortedBy { it.category.ordinal } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -21,7 +21,7 @@ import me.rhunk.snapenhance.data.wrapper.impl.media.opera.ParamMap import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.Messaging -import me.rhunk.snapenhance.features.impl.spy.MessageLogger +import me.rhunk.snapenhance.features.impl.spying.MessageLogger import me.rhunk.snapenhance.hook.HookAdapter import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker @@ -62,7 +62,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam return isFFmpegPresent } - private fun createNewFilePath(hash: Int, author: String, fileType: FileType): String? { + private fun createNewFilePath(hash: Int, author: String, fileType: FileType): String { val hexHash = Integer.toHexString(hash) return author + "/" + hexHash + "." + fileType.fileExtension } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/AutoSave.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/AutoSave.kt @@ -8,8 +8,8 @@ import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.Messaging -import me.rhunk.snapenhance.features.impl.spy.MessageLogger -import me.rhunk.snapenhance.features.impl.spy.StealthMode +import me.rhunk.snapenhance.features.impl.spying.MessageLogger +import me.rhunk.snapenhance.features.impl.spying.StealthMode import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.util.CallbackBuilder diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt @@ -155,8 +155,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } private fun shouldIgnoreNotification(type: String): Boolean { - Logger.debug("notification type: $type") - val states = context.config.options(ConfigProperty.NOTIFICATION_FILTER) + val states = context.config.options(ConfigProperty.NOTIFICATION_BLACKLIST) states["snap"]?.let { if (type.endsWith("SNAP") && it) return true } states["chat"]?.let { if (type.endsWith("CHAT") && it) return true } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt @@ -29,14 +29,14 @@ class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams. Hooker.hook(context.classCache.networkApi, "submit", HookStage.BEFORE, disableMetricsFilter) { param -> val httpRequest: Any = param.arg(0) val url = XposedHelpers.getObjectField(httpRequest, "mUrl").toString() - if (url.contains("resolve?co=")) { + /*if (url.contains("resolve?co=")) { val index = url.indexOf("co=") val end = url.lastIndexOf("&") val co = url.substring(index + 3, end) val decoded = Base64.getDecoder().decode(co.toByteArray(StandardCharsets.UTF_8)) debug("decoded : " + decoded.toString(Charsets.UTF_8)) debug("content: $co") - } + }*/ if (url.contains("app-analytics") || url.endsWith("v1/metrics")) { param.setResult(null) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/AnonymousStoryViewing.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/AnonymousStoryViewing.kt @@ -1,20 +0,0 @@ -package me.rhunk.snapenhance.features.impl.spy - -import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.features.Feature -import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker -import me.rhunk.snapenhance.util.getObjectField - -class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { - Hooker.hook(context.classCache.networkApi,"submit", HookStage.BEFORE, { context.config.bool(ConfigProperty.ANONYMOUS_STORY_VIEW) }) { - val httpRequest: Any = it.arg(0) - val url = httpRequest.getObjectField("mUrl") as String - if (url.endsWith("readreceipt-indexer/batchuploadreadreceipts") || url.endsWith("v2/batch_cta")) { - it.setResult(null) - } - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/MessageLogger.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/MessageLogger.kt @@ -1,86 +0,0 @@ -package me.rhunk.snapenhance.features.impl.spy - -import com.google.gson.JsonParser -import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.data.MessageState -import me.rhunk.snapenhance.data.wrapper.impl.Message -import me.rhunk.snapenhance.features.Feature -import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker - -class MessageLogger : Feature("MessageLogger", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - private val messageCache = mutableMapOf<Long, String>() - private val removedMessages = linkedSetOf<Long>() - private val myUserId by lazy { context.database.getMyUserId() } - - fun isMessageRemoved(messageId: Long) = removedMessages.contains(messageId) - - override fun onActivityCreate() { - if (!context.database.hasArroyo()) { - context.bridgeClient.clearMessageLogger() - } - } - - //FIXME: message disappears when the conversation is set to delete on view - override fun init() { - Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { - context.config.bool(ConfigProperty.MESSAGE_LOGGER) - }) { - val message = Message(it.thisObject()) - val messageId = message.messageDescriptor.messageId - val contentType = message.messageContent.contentType - val messageState = message.messageState - - if (messageState != MessageState.COMMITTED) return@hookConstructor - - - if (contentType == ContentType.STATUS) { - //query the deleted message - val deletedMessage: String = if (messageCache.containsKey(messageId)) messageCache[messageId] else { - context.bridgeClient.getMessageLoggerMessage(messageId)?.toString(Charsets.UTF_8) - } ?: return@hookConstructor - - val messageJsonObject = JsonParser.parseString(deletedMessage).asJsonObject - - //if the message is a snap make it playable - if (messageJsonObject["mMessageContent"]?.asJsonObject?.get("mContentType")?.asString == "SNAP") { - messageJsonObject["mMetadata"].asJsonObject.addProperty("mPlayableSnapState", "PLAYABLE") - } - - //serialize all properties of messageJsonObject and put in the message object - message.instanceNonNull().javaClass.declaredFields.forEach { field -> - field.isAccessible = true - val fieldName = field.name - val fieldValue = messageJsonObject[fieldName] - if (fieldValue != null) { - field.set(message.instanceNonNull(), context.gson.fromJson(fieldValue, field.type)) - } - } - - //set the message state to PREPARING for visibility - if (message.messageContent.contentType != ContentType.SNAP && message.messageContent.contentType != ContentType.EXTERNAL_MEDIA) { - message.messageState = MessageState.PREPARING - } - removedMessages.add(messageId) - return@hookConstructor - } - - //exclude messages sent by me - if (message.senderId.toString() == myUserId) return@hookConstructor - - if (!messageCache.containsKey(messageId)) { - context.executeAsync { - val storedMessage = context.bridgeClient.getMessageLoggerMessage(messageId)?.toString(Charsets.UTF_8) - if (storedMessage == null) { - messageCache[messageId] = context.gson.toJson(message.instanceNonNull()) - context.bridgeClient.addMessageLoggerMessage(messageId, messageCache[messageId]!!.toByteArray(Charsets.UTF_8)) - return@executeAsync - } - messageCache[messageId] = storedMessage - } - } - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventReadReceipts.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventReadReceipts.kt @@ -1,28 +0,0 @@ -package me.rhunk.snapenhance.features.impl.spy - -import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID -import me.rhunk.snapenhance.features.Feature -import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker - -class PreventReadReceipts : Feature("PreventReadReceipts", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { - val isConversationInStealthMode: (SnapUUID) -> Boolean = hook@{ - if (context.config.bool(ConfigProperty.PREVENT_READ_RECEIPTS)) return@hook true - context.feature(StealthMode::class).isStealth(it.toString()) - } - - arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String -> - Hooker.hook(context.classCache.conversationManager, methodName, HookStage.BEFORE, { isConversationInStealthMode(SnapUUID(it.arg(0))) }) { - it.setResult(null) - } - } - Hooker.hook(context.classCache.snapManager, "onSnapInteraction", HookStage.BEFORE) { - if (isConversationInStealthMode(SnapUUID(it.arg(1) as Any))) { - it.setResult(null) - } - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/StealthMode.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/StealthMode.kt @@ -1,20 +0,0 @@ -package me.rhunk.snapenhance.features.impl.spy - -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType -import me.rhunk.snapenhance.features.BridgeFileFeature -import me.rhunk.snapenhance.features.FeatureLoadParams - - -class StealthMode : BridgeFileFeature("StealthMode", BridgeFileType.STEALTH, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { - readFile() - } - - fun setStealth(conversationId: String, stealth: Boolean) { - setState(conversationId.hashCode().toLong().toString(16), stealth) - } - - fun isStealth(conversationId: String): Boolean { - return exists(conversationId.hashCode().toLong().toString(16)) - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.features.impl.spying + +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.util.getObjectField + +class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { + Hooker.hook(context.classCache.networkApi,"submit", HookStage.BEFORE, { context.config.bool(ConfigProperty.ANONYMOUS_STORY_VIEW) }) { + val httpRequest: Any = it.arg(0) + val url = httpRequest.getObjectField("mUrl") as String + if (url.endsWith("readreceipt-indexer/batchuploadreadreceipts") || url.endsWith("v2/batch_cta")) { + it.setResult(null) + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt @@ -0,0 +1,86 @@ +package me.rhunk.snapenhance.features.impl.spying + +import com.google.gson.JsonParser +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.MessageState +import me.rhunk.snapenhance.data.wrapper.impl.Message +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker + +class MessageLogger : Feature("MessageLogger", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + private val messageCache = mutableMapOf<Long, String>() + private val removedMessages = linkedSetOf<Long>() + private val myUserId by lazy { context.database.getMyUserId() } + + fun isMessageRemoved(messageId: Long) = removedMessages.contains(messageId) + + override fun onActivityCreate() { + if (!context.database.hasArroyo()) { + context.bridgeClient.clearMessageLogger() + } + } + + //FIXME: message disappears when the conversation is set to delete on view + override fun init() { + Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { + context.config.bool(ConfigProperty.MESSAGE_LOGGER) + }) { + val message = Message(it.thisObject()) + val messageId = message.messageDescriptor.messageId + val contentType = message.messageContent.contentType + val messageState = message.messageState + + if (messageState != MessageState.COMMITTED) return@hookConstructor + + + if (contentType == ContentType.STATUS) { + //query the deleted message + val deletedMessage: String = if (messageCache.containsKey(messageId)) messageCache[messageId] else { + context.bridgeClient.getMessageLoggerMessage(messageId)?.toString(Charsets.UTF_8) + } ?: return@hookConstructor + + val messageJsonObject = JsonParser.parseString(deletedMessage).asJsonObject + + //if the message is a snap make it playable + if (messageJsonObject["mMessageContent"]?.asJsonObject?.get("mContentType")?.asString == "SNAP") { + messageJsonObject["mMetadata"].asJsonObject.addProperty("mPlayableSnapState", "PLAYABLE") + } + + //serialize all properties of messageJsonObject and put in the message object + message.instanceNonNull().javaClass.declaredFields.forEach { field -> + field.isAccessible = true + val fieldName = field.name + val fieldValue = messageJsonObject[fieldName] + if (fieldValue != null) { + field.set(message.instanceNonNull(), context.gson.fromJson(fieldValue, field.type)) + } + } + + //set the message state to PREPARING for visibility + if (message.messageContent.contentType != ContentType.SNAP && message.messageContent.contentType != ContentType.EXTERNAL_MEDIA) { + message.messageState = MessageState.PREPARING + } + removedMessages.add(messageId) + return@hookConstructor + } + + //exclude messages sent by me + if (message.senderId.toString() == myUserId) return@hookConstructor + + if (!messageCache.containsKey(messageId)) { + context.executeAsync { + val storedMessage = context.bridgeClient.getMessageLoggerMessage(messageId)?.toString(Charsets.UTF_8) + if (storedMessage == null) { + messageCache[messageId] = context.gson.toJson(message.instanceNonNull()) + context.bridgeClient.addMessageLoggerMessage(messageId, messageCache[messageId]!!.toByteArray(Charsets.UTF_8)) + return@executeAsync + } + messageCache[messageId] = storedMessage + } + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt @@ -0,0 +1,28 @@ +package me.rhunk.snapenhance.features.impl.spying + +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker + +class PreventReadReceipts : Feature("PreventReadReceipts", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + override fun onActivityCreate() { + val isConversationInStealthMode: (SnapUUID) -> Boolean = hook@{ + if (context.config.bool(ConfigProperty.PREVENT_READ_RECEIPTS)) return@hook true + context.feature(StealthMode::class).isStealth(it.toString()) + } + + arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String -> + Hooker.hook(context.classCache.conversationManager, methodName, HookStage.BEFORE, { isConversationInStealthMode(SnapUUID(it.arg(0))) }) { + it.setResult(null) + } + } + Hooker.hook(context.classCache.snapManager, "onSnapInteraction", HookStage.BEFORE) { + if (isConversationInStealthMode(SnapUUID(it.arg(1) as Any))) { + it.setResult(null) + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.features.impl.spying + +import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.features.BridgeFileFeature +import me.rhunk.snapenhance.features.FeatureLoadParams + + +class StealthMode : BridgeFileFeature("StealthMode", BridgeFileType.STEALTH, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + override fun onActivityCreate() { + readFile() + } + + fun setStealth(conversationId: String, stealth: Boolean) { + setState(conversationId.hashCode().toLong().toString(16), stealth) + } + + fun isStealth(conversationId: String): Boolean { + return exists(conversationId.hashCode().toLong().toString(16)) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/ViewAppearanceHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/ViewAppearanceHelper.kt @@ -11,25 +11,24 @@ import android.widget.TextView import me.rhunk.snapenhance.Constants object ViewAppearanceHelper { - fun applyIndentation(view: TextView) { - view.setPadding(70, 0, 55, 0) - } - @SuppressLint("UseSwitchCompatOrMaterialCode", "RtlHardcoded", "DiscouragedApi") fun applyTheme(viewModel: View, view: TextView) { + val snapchatFontResId = view.context.resources.getIdentifier("avenir_next_medium", "font", "com.snapchat.android") //remove the shadow view.setBackgroundColor(0x00000000) view.setTextColor(viewModel.resources.getColor(viewModel.resources.getIdentifier("sig_color_text_primary_light", "color", Constants.SNAPCHAT_PACKAGE_NAME), null)) - view.setShadowLayer(0f, 0f, 0f, 0) + view.setShadowLayer(0F, 0F, 0F, 0) view.outlineProvider = null view.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL view.width = viewModel.width - //FIXME: hardcoded dimensions - view.height = 160 - view.setPadding(35, 0, 55, 0) + + //DPI Calculator + val scalingFactor = view.context.resources.displayMetrics.densityDpi.toDouble() / 400 + view.height = (150 * scalingFactor).toInt() + view.setPadding((40 * scalingFactor).toInt(), 0, (25 * scalingFactor).toInt(), 0) view.isAllCaps = false - view.textSize = 15f - view.typeface = Typeface.DEFAULT + view.textSize = 16f + view.typeface = view.context.resources.getFont(snapchatFontResId) //remove click effect if (view.javaClass == TextView::class.java) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/ChatActionMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/ChatActionMenu.kt @@ -2,9 +2,8 @@ package me.rhunk.snapenhance.features.impl.ui.menus.impl import android.annotation.SuppressLint import android.content.res.Resources -import android.graphics.BlendMode -import android.graphics.BlendModeColorFilter import android.graphics.Color +import android.graphics.drawable.ColorDrawable import android.os.SystemClock import android.util.TypedValue import android.view.MotionEvent @@ -27,7 +26,7 @@ class ChatActionMenu : AbstractMenu() { } private fun applyButtonTheme(parent: View, button: Button) { - button.background.colorFilter = BlendModeColorFilter(Color.WHITE, BlendMode.SRC_ATOP) + button.background = ColorDrawable(Color.WHITE) button.setTextColor(Color.BLACK) button.transformationMethod = null val margin = TypedValue.applyDimension( diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/FriendFeedInfoMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/FriendFeedInfoMenu.kt @@ -22,7 +22,7 @@ import me.rhunk.snapenhance.database.objects.UserConversationLink import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload import me.rhunk.snapenhance.features.impl.extras.AntiAutoSave -import me.rhunk.snapenhance.features.impl.spy.StealthMode +import me.rhunk.snapenhance.features.impl.spying.StealthMode import me.rhunk.snapenhance.features.impl.ui.menus.AbstractMenu import me.rhunk.snapenhance.features.impl.ui.menus.ViewAppearanceHelper.applyTheme import java.net.HttpURLConnection diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/SettingsMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/SettingsMenu.kt @@ -2,6 +2,7 @@ package me.rhunk.snapenhance.features.impl.ui.menus.impl import android.annotation.SuppressLint import android.app.AlertDialog +import android.graphics.Typeface import android.text.InputType import android.view.View import android.widget.Button @@ -24,7 +25,8 @@ class SettingsMenu : AbstractMenu() { val categoryText = TextView(viewModel.context) categoryText.text = context.translation.get(key) ViewAppearanceHelper.applyTheme(viewModel, categoryText) - categoryText.textSize = 18f + categoryText.textSize = 20f + categoryText.typeface = categoryText.typeface?.let { Typeface.create(it, Typeface.BOLD) } return categoryText } @@ -33,6 +35,9 @@ class SettingsMenu : AbstractMenu() { val updateButtonText: (TextView, String) -> Unit = { textView, text -> textView.text = "${context.translation.get(property.nameKey)} $text" } + val updateStateSelectionText: (TextView, String) -> Unit = { textView, text -> + updateButtonText(textView, text.let { if (it.isEmpty()) "(empty)" else ": $it" }) + } val textEditor: ((String) -> Unit) -> Unit = { updateValue -> val builder = AlertDialog.Builder(viewModel.context) @@ -116,7 +121,7 @@ class SettingsMenu : AbstractMenu() { } is ConfigStateListValue -> { val button = Button(viewModel.context) - updateButtonText(button, property.valueContainer.toString()) + updateStateSelectionText(button, property.valueContainer.toString()) button.setOnClickListener {_ -> val builder = AlertDialog.Builder(viewModel.context) @@ -134,7 +139,7 @@ class SettingsMenu : AbstractMenu() { } builder.setPositiveButton("OK") { _, _ -> - updateButtonText(button, property.valueContainer.toString()) + updateStateSelectionText(button, property.valueContainer.toString()) } builder.show() @@ -177,7 +182,8 @@ class SettingsMenu : AbstractMenu() { } } - //actions + addView(createCategoryTitle(viewModel, "category.debugging")) + context.actionManager.getActions().forEach { val button = Button(viewModel.context) button.text = context.translation.get(it.nameKey) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -20,10 +20,10 @@ import me.rhunk.snapenhance.features.impl.extras.SnapchatPlus import me.rhunk.snapenhance.features.impl.extras.UnlimitedSnapViewTime import me.rhunk.snapenhance.features.impl.privacy.DisableMetrics import me.rhunk.snapenhance.features.impl.privacy.PreventMessageSending -import me.rhunk.snapenhance.features.impl.spy.AnonymousStoryViewing -import me.rhunk.snapenhance.features.impl.spy.MessageLogger -import me.rhunk.snapenhance.features.impl.spy.PreventReadReceipts -import me.rhunk.snapenhance.features.impl.spy.StealthMode +import me.rhunk.snapenhance.features.impl.spying.AnonymousStoryViewing +import me.rhunk.snapenhance.features.impl.spying.MessageLogger +import me.rhunk.snapenhance.features.impl.spying.PreventReadReceipts +import me.rhunk.snapenhance.features.impl.spying.StealthMode import me.rhunk.snapenhance.features.impl.ui.UITweaks import me.rhunk.snapenhance.features.impl.ui.menus.MenuViewInjector import me.rhunk.snapenhance.manager.Manager @@ -37,7 +37,6 @@ class FeatureManager(private val context: ModContext) : Manager { private fun register(featureClass: KClass<out Feature>) { runCatching { with(featureClass.java.newInstance()) { - if (loadParams and FeatureLoadParams.NO_INIT != 0) return@with context = this@FeatureManager.context features.add(this) }