commit 85335dafa81e59af9a35c65a8cdcdf9463350984
parent cfb0ba5d65e435f622e7408d21c32d9f5855ded0
Author: auth <64337177+authorisation@users.noreply.github.com>
Date:   Sun,  4 Jun 2023 01:53:05 +0200

refactor: reorganize (#15)

* refactor: organization (1/?)

open for ideas and suggestions
TODO:
    • Merge UI Hide Elements
    • Remove Longitude and Latitude from the menu and integrate into the MapActivity
    • Move New Map UI to Experimental
    • Shorten „Show Message Content in Notification“

And possibly even more

* Merge hide UI Elements

bugfix(?): merge overlay

* add: download options

* add: config listeners

* fix(ci): debug

remove prod as it's not getting built anymore

* fix(messagelogger): clear database bug

* update locale, reorganize config

* add: set precise location dialog

* config: better notifications

---------

Co-authored-by: rhunk <101876869+rhunk@users.noreply.github.com>
Diffstat:
M.github/workflows/android.yml | 5-----
M.gitignore | 6+++---
Mapp/src/main/assets/lang/en_US.json | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerRequest.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt | 13++++---------
Mapp/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt | 291+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/config/ConfigValue.kt | 19+++++++++++++++++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigIntegerValue.kt | 6+++---
Mapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateListValue.kt | 13+++++++++----
Mapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateSelection.kt | 14++++----------
Mapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateValue.kt | 6+++---
Mapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt | 9++++++---
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 36+++++++++++++++++-------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/AntiAutoSave.kt | 20--------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/AutoSave.kt | 139-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/DisableVideoLengthRestriction.kt | 26--------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/GalleryMediaSendOverride.kt | 38--------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/LocationSpoofer.kt | 65-----------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/MediaQualityLevelOverride.kt | 27---------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt | 302------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/SnapchatPlus.kt | 30------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/UnlimitedSnapViewTime.kt | 36------------------------------------
Aapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt | 20++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt | 26++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt | 38++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/LocationSpoofer.kt | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt | 27+++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt | 301+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt | 30++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt | 36++++++++++++++++++++++++++++++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt | 17+++++++++--------
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/MapActivity.kt | 32+++++++++++++++++++++++++++++---
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/ChatActionMenu.kt | 43++++++++++++++++++++++---------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/FriendFeedInfoMenu.kt | 4++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/SettingsMenu.kt | 51+++++++++++++++++++++++++++++----------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt | 4++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt | 18+++++++++---------
Mapp/src/main/res/layout/map.xml | 41++++++++++++++++++++++++++++++-----------
Aapp/src/main/res/layout/precise_location_dialog.xml | 29+++++++++++++++++++++++++++++
40 files changed, 1124 insertions(+), 974 deletions(-)

diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml @@ -34,8 +34,3 @@ jobs: with: name: app-armv7-release path: app/build/outputs/apk/armv7/debug/*.apk - - name: Upload prod - uses: actions/upload-artifact@v3.1.2 - with: - name: app-prod-release - path: app/build/outputs/apk/prod/debug/*.apk diff --git a/.gitignore b/.gitignore @@ -1,10 +1,9 @@ *.iml .gradle -/local.properties +local.properties /.idea/ .DS_Store /build /captures .externalNativeBuild -.cxx -local.properties +.cxx+ \ 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,22 +1,16 @@ { "category": { - "general": "General", - "spying": "Spying", - "media_download": "Media Downloader", - "privacy": "Privacy", - "ui": "UI", - "extras": "Extras", - "tweaks": "Tweaks", - "location_spoof": "Location Spoof", - "experimental": "Experimental", - "debugging": "Debugging" + "spying_privacy": "Spying & Privacy", + "media_manager": "Media Manager", + "ui_tweaks": "UI & Tweaks", + "experimental_debugging": "Experimental" }, "action": { "clean_cache": "Clean Cache", "clear_message_logger": "Clear Message Logger", "refresh_mappings": "Refresh Mappings", - "open_map": "Pick a location on the map" + "open_map": "Choose location on map" }, "property": { @@ -24,16 +18,14 @@ "prevent_read_receipts": "Prevent Read Receipts", "hide_bitmoji_presence": "Hide Bitmoji Presence", "show_message_content_in_notifications": "Show Message Content In Notifications", + "better_notifications": "Better Notifications", "notification_blacklist": "Notification Blacklist", "message_logger": "Message Logger", "unlimited_snap_view_time": "Unlimited Snap View Time", - "auto_download_snaps": "Auto Download Snaps", - "auto_download_stories": "Auto Download Stories", - "auto_download_public_stories": "Auto Download Public Stories", - "auto_download_spotlight": "Auto Download Spotlight", + "auto_download_options": "Auto Download Options", "download_options": "Download Options", - "download_inchat_snaps": "Download Inchat Snaps", - "anti_auto_download_button": "Anti Auto Download Button", + "chat_download_context_menu": "Enable Chat Download Context Menu", + "auto_download_blacklist": "Auto Download Blacklist", "disable_metrics": "Disable Metrics", "prevent_screenshot_notifications": "Prevent Screenshot Notifications", "prevent_status_notifications": "Prevent Status Notifications (Save to camera roll, missed calls)", @@ -42,7 +34,7 @@ "menu_slot_id": "Friend Menu Slot ID", "message_preview_length": "Message Preview Length", "gallery_media_send_override": "Gallery Media Send Override", - "auto_save": "Auto Save", + "auto_save_messages": "Auto Save Messages", "anti_auto_save": "Anti Auto Save Button", "snapchat_plus": "Snapchat Plus", "disable_snap_splitting": "Disable Snap Splitting", @@ -62,11 +54,17 @@ "meo_passcode_bypass": "My Eyes Only Passcode Bypass", "location_spoof": "Snapmap Location Spoofer", "latitude_value": "Latitude", - "longitude_value": "Longitude" + "longitude_value": "Longitude", + "hide_ui_elements": "Hide UI Elements" }, "option": { "property": { + "better_notifications": { + "chat": "Show chat messages", + "snap": "Show medias", + "reply_button": "Add reply button" + }, "download_options": { "format_user_folder": "Create folder for each user", "format_hash": "Add a unique hash to the file path", @@ -74,10 +72,46 @@ "format_date_time": "Add the date and time to the file path", "merge_overlay": "Merge Snap Image Overlays" }, + "auto_download_options": { + "friend_snaps": "Friend Snaps", + "friend_stories": "Friend Stories", + "public_stories": "Public Stories", + "spotlight": "Spotlight" + }, + "auto_save_messages": { + "NOTE": "Audio Note", + "CHAT": "Chat", + "EXTERNAL_MEDIA": "External Media", + "SNAP": "Snap", + "STICKER": "Sticker" + }, "notification_blacklist": { "chat": "Chat", "snap": "Snap", "typing": "Typing" + }, + "gallery_media_send_override": { + "OFF": "Off", + "NOTE": "Audio Note", + "SNAP": "Snap", + "LIVE_SNAP": "Snap with audio" + }, + "media_quality_level": { + "LEVEL_NONE": "Level None", + "LEVEL_1": "Level 1", + "LEVEL_2": "Level 2", + "LEVEL_3": "Level 3", + "LEVEL_4": "Level 4", + "LEVEL_5": "Level 5", + "LEVEL_6": "Level 6", + "LEVEL_7": "Level 7", + "LEVEL_MAX": "Level Max" + }, + "hide_ui_elements": { + "remove_call_buttons": "Remove Call Buttons", + "remove_cognac_button": "Remove Cognac Button", + "remove_stickers_button": "Remove Stickers Button", + "remove_voice_record_button": "Remove Voice Record Button" } } }, @@ -94,6 +128,12 @@ "preview": "Preview" }, + "chat_action_menu": { + "preview_button": "Preview", + "download_button": "Download", + "delete_logged_message_button": "Delete Logged Message" + }, + "opera_context_menu": { "download": "Download Media" }, diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerRequest.kt @@ -13,7 +13,7 @@ class MessageLoggerRequest( override fun write(bundle: Bundle) { bundle.putString("action", action!!.name) bundle.putString("conversationId", conversationId) - bundle.putLong("messageId", messageId!!) + bundle.putLong("messageId", messageId ?: 0) bundle.putByteArray("message", message) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt @@ -3,13 +3,8 @@ package me.rhunk.snapenhance.config enum class ConfigCategory( val key: String ) { - GENERAL("category.general"), - SPYING("category.spying"), - MEDIA_DOWNLOADER("category.media_download"), - PRIVACY("category.privacy"), - UI("category.ui"), - EXTRAS("category.extras"), - TWEAKS("category.tweaks"), - LOCATION_SPOOF("category.location_spoof"), - EXPERIMENTAL("category.experimental"); + SPYING_PRIVACY("category.spying_privacy"), + MEDIA_MANAGEMENT("category.media_manager"), + UI_TWEAKS("category.ui_tweaks"), + EXPERIMENTAL_DEBUGGING("category.experimental_debugging"); } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt @@ -12,30 +12,45 @@ enum class ConfigProperty( val nameKey: String, val descriptionKey: String, val category: ConfigCategory, - val valueContainer: ConfigValue<*> + val valueContainer: ConfigValue<*>, + val shouldAppearInSettings: Boolean = true ) { + + //SPYING AND PRIVACY + MESSAGE_LOGGER("property.message_logger", + "description.message_logger", + ConfigCategory.SPYING_PRIVACY, + ConfigStateValue(false) + ), PREVENT_READ_RECEIPTS( "property.prevent_read_receipts", "description.prevent_read_receipts", - ConfigCategory.SPYING, + ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), HIDE_BITMOJI_PRESENCE( "property.hide_bitmoji_presence", "description.hide_bitmoji_presence", - ConfigCategory.SPYING, + ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), - SHOW_MESSAGE_CONTENT_IN_NOTIFICATIONS( - "property.show_message_content_in_notifications", - "description.show_message_content_in_notifications", - ConfigCategory.SPYING, - ConfigStateValue(false) + BETTER_NOTIFICATIONS( + "property.better_notifications", + "description.better_notifications", + ConfigCategory.SPYING_PRIVACY, + ConfigStateListValue( + listOf("snap", "chat", "reply_button"), + mutableMapOf( + "snap" to false, + "chat" to false, + "reply_button" to false + ) + ) ), NOTIFICATION_BLACKLIST( "property.notification_blacklist", "description.notification_blacklist", - ConfigCategory.SPYING, + ConfigCategory.SPYING_PRIVACY, ConfigStateListValue( listOf("snap", "chat", "typing"), mutableMapOf( @@ -45,215 +60,227 @@ enum class ConfigProperty( ) ) ), - - 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)), - - SAVE_FOLDER( - "property.save_folder", "description.save_folder", ConfigCategory.MEDIA_DOWNLOADER, - ConfigStringValue(File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).absolutePath + "/Snapchat", - "SnapEnhance" - ).absolutePath) - ), - AUTO_DOWNLOAD_SNAPS( - "property.auto_download_snaps", - "description.auto_download_snaps", - ConfigCategory.MEDIA_DOWNLOADER, - ConfigStateValue(false) - ), - AUTO_DOWNLOAD_STORIES( - "property.auto_download_stories", - "description.auto_download_stories", - ConfigCategory.MEDIA_DOWNLOADER, + DISABLE_METRICS("property.disable_metrics", + "description.disable_metrics", + ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), - AUTO_DOWNLOAD_PUBLIC_STORIES( - "property.auto_download_public_stories", - "description.auto_download_public_stories", - ConfigCategory.MEDIA_DOWNLOADER, + BLOCK_ADS("property.block_ads", + "description.block_ads", + ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), - AUTO_DOWNLOAD_SPOTLIGHT( - "property.auto_download_spotlight", - "description.auto_download_spotlight", - ConfigCategory.MEDIA_DOWNLOADER, + UNLIMITED_SNAP_VIEW_TIME("property.unlimited_snap_view_time", + "description.unlimited_snap_view_time", + ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), - DOWNLOAD_OPTIONS( - "property.download_options", "description.download_options", ConfigCategory.MEDIA_DOWNLOADER, - ConfigStateListValue( - listOf("format_user_folder", "format_hash", "format_date_time", "format_username", "merge_overlay"), - mutableMapOf( - "format_user_folder" to true, - "format_hash" to true, - "format_date_time" to true, - "format_username" to false, - "merge_overlay" to false, - ) - ) - ), - DOWNLOAD_INCHAT_SNAPS( - "property.download_inchat_snaps", - "description.download_inchat_snaps", - ConfigCategory.MEDIA_DOWNLOADER, - ConfigStateValue(false) - ), - ANTI_DOWNLOAD_BUTTON( - "property.anti_auto_download_button", - "description.anti_auto_download_button", - ConfigCategory.MEDIA_DOWNLOADER, - ConfigStateValue(false) - ), - - DISABLE_METRICS("property.disable_metrics", "description.disable_metrics", ConfigCategory.PRIVACY, ConfigStateValue(false)), PREVENT_SCREENSHOT_NOTIFICATIONS( "property.prevent_screenshot_notifications", "description.prevent_screenshot_notifications", - ConfigCategory.PRIVACY, + ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), PREVENT_STATUS_NOTIFICATIONS( "property.prevent_status_notifications", "description.prevent_status_notifications", - ConfigCategory.PRIVACY, + ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), ANONYMOUS_STORY_VIEW( "property.anonymous_story_view", "description.anonymous_story_view", - ConfigCategory.PRIVACY, + ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), HIDE_TYPING_NOTIFICATION( "property.hide_typing_notification", "description.hide_typing_notification", - ConfigCategory.PRIVACY, + ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), - - MENU_SLOT_ID("property.menu_slot_id", "description.menu_slot_id", ConfigCategory.UI, ConfigIntegerValue(1)), - MESSAGE_PREVIEW_LENGTH( - "property.message_preview_length", - "description.message_preview_length", - ConfigCategory.UI, - ConfigIntegerValue(20) + + //MEDIA MANAGEMENT + SAVE_FOLDER( + "property.save_folder", "description.save_folder", ConfigCategory.MEDIA_MANAGEMENT, + ConfigStringValue(File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).absolutePath + "/Snapchat", + "SnapEnhance" + ).absolutePath) ), - - 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.EXTRAS, + AUTO_DOWNLOAD_OPTIONS( + "property.auto_download_options", "description.auto_download_options", ConfigCategory.MEDIA_MANAGEMENT, + ConfigStateListValue( + listOf("friend_snaps", "friend_stories", "public_stories", "spotlight"), + mutableMapOf( + "friend_snaps" to false, + "friend_stories" to false, + "public_stories" to false, + "spotlight" to false + ) + ) + ), + DOWNLOAD_OPTIONS( + "property.download_options", "description.download_options", ConfigCategory.MEDIA_MANAGEMENT, + ConfigStateListValue( + listOf("format_user_folder", "format_hash", "format_date_time", "format_username", "merge_overlay"), + mutableMapOf( + "format_user_folder" to true, + "format_hash" to true, + "format_date_time" to true, + "format_username" to false, + "merge_overlay" to false, + ) + ) + ), + CHAT_DOWNLOAD_CONTEXT_MENU( + "property.chat_download_context_menu", + "description.chat_download_context_menu", + ConfigCategory.MEDIA_MANAGEMENT, ConfigStateValue(false) ), - DISABLE_VIDEO_LENGTH_RESTRICTION( - "property.disable_video_length_restriction", - "description.disable_video_length_restriction", - ConfigCategory.EXTRAS, + DOWNLOAD_BLACKLIST( + "property.auto_download_blacklist", + "description.auto_download_blacklist", + ConfigCategory.MEDIA_MANAGEMENT, + ConfigStateValue(false) + ), + GALLERY_MEDIA_SEND_OVERRIDE( + "property.gallery_media_send_override", + "description.gallery_media_send_override", + ConfigCategory.MEDIA_MANAGEMENT, + ConfigStateSelection( + listOf("OFF", "NOTE", "SNAP", "LIVE_SNAP"), + "OFF" + ) + ), + AUTO_SAVE_MESSAGES("property.auto_save_messages", + "description.auto_save_messages", + ConfigCategory.MEDIA_MANAGEMENT, + ConfigStateListValue( + listOf("CHAT", "SNAP", "NOTE", "EXTERNAL_MEDIA", "STICKER") + ) + ), + ANTI_AUTO_SAVE("property.anti_auto_save", + "description.anti_auto_save", + ConfigCategory.MEDIA_MANAGEMENT, ConfigStateValue(false) ), + OVERRIDE_MEDIA_QUALITY( "property.override_media_quality", "description.override_media_quality", - ConfigCategory.EXTRAS, + ConfigCategory.MEDIA_MANAGEMENT, ConfigStateValue(false) ), MEDIA_QUALITY_LEVEL( "property.media_quality_level", "description.media_quality_level", - ConfigCategory.EXTRAS, + ConfigCategory.MEDIA_MANAGEMENT, 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", "LIVE_SNAP"), - "OFF" + + //UI AND TWEAKS + HIDE_UI_ELEMENTS( + "property.hide_ui_elements", + "description.hide_ui_elements", + ConfigCategory.UI_TWEAKS, + ConfigStateListValue( + listOf("remove_voice_record_button", "remove_stickers_button", "remove_cognac_button", "remove_call_buttons"), + mutableMapOf( + "remove_voice_record_button" to false, + "remove_stickers_button" to false, + "remove_cognac_button" to false, + "remove_call_buttons" to false, + ) ) ), - - REMOVE_VOICE_RECORD_BUTTON( - "property.remove_voice_record_button", - "description.remove_voice_record_button", - ConfigCategory.TWEAKS, + STREAK_EXPIRATION_INFO( + "property.streak_expiration_info", + "description.streakexpirationinfo", + ConfigCategory.UI_TWEAKS, ConfigStateValue(false) ), - REMOVE_STICKERS_BUTTON( - "property.remove_stickers_button", - "description.remove_stickers_button", - ConfigCategory.TWEAKS, + DISABLE_SNAP_SPLITTING( + "property.disable_snap_splitting", + "description.disable_snap_splitting", + ConfigCategory.UI_TWEAKS, ConfigStateValue(false) ), - REMOVE_COGNAC_BUTTON( - "property.remove_cognac_button", - "description.remove_cognac_button", - ConfigCategory.TWEAKS, + DISABLE_VIDEO_LENGTH_RESTRICTION( + "property.disable_video_length_restriction", + "description.disable_video_length_restriction", + ConfigCategory.UI_TWEAKS, ConfigStateValue(false) ), - REMOVE_CALL_BUTTONS( - "property.remove_call_buttons", - "description.remove_call_buttons", - ConfigCategory.TWEAKS, + SNAPCHAT_PLUS("property.snapchat_plus", + "description.snapchat_plus", + ConfigCategory.UI_TWEAKS, ConfigStateValue(false) ), - BLOCK_ADS("property.block_ads", "description.block_ads", ConfigCategory.TWEAKS, ConfigStateValue(false)), - STREAK_EXPIRATION_INFO( - "property.streak_expiration_info", - "description.streakexpirationinfo", - ConfigCategory.TWEAKS, + NEW_MAP_UI("property.new_map_ui", + "description.new_map_ui", + ConfigCategory.UI_TWEAKS, ConfigStateValue(false) ), - NEW_MAP_UI("property.new_map_ui", "description.new_map_ui", ConfigCategory.TWEAKS, ConfigStateValue(false)), - LOCATION_SPOOF( "property.location_spoof", "description.location_spoof", - ConfigCategory.LOCATION_SPOOF, + ConfigCategory.UI_TWEAKS, ConfigStateValue(false) ), LATITUDE( "property.latitude_value", "description.latitude_value", - ConfigCategory.LOCATION_SPOOF, - ConfigStringValue("0.0000") + ConfigCategory.UI_TWEAKS, + ConfigStringValue("0.0000"), + shouldAppearInSettings = false ), LONGITUDE( "property.longitude_value", "description.longitude_value", - ConfigCategory.LOCATION_SPOOF, - ConfigStringValue("0.0000") + ConfigCategory.UI_TWEAKS, + ConfigStringValue("0.0000"), + shouldAppearInSettings = false ), - + MENU_SLOT_ID("property.menu_slot_id", + "description.menu_slot_id", + ConfigCategory.UI_TWEAKS, + ConfigIntegerValue(1) + ), + MESSAGE_PREVIEW_LENGTH( + "property.message_preview_length", + "description.message_preview_length", + ConfigCategory.UI_TWEAKS, + ConfigIntegerValue(20) + ), + + // EXPERIMENTAL DEBUGGING USE_DOWNLOAD_MANAGER( "property.use_download_manager", "description.use_download_manager", - ConfigCategory.EXPERIMENTAL, + ConfigCategory.EXPERIMENTAL_DEBUGGING, ConfigStateValue(false) ), APP_PASSCODE( "property.app_passcode", "description.app_passcode", - ConfigCategory.EXPERIMENTAL, - ConfigStringValue("") + ConfigCategory.EXPERIMENTAL_DEBUGGING, + ConfigStringValue("", isHidden = true) ), APP_LOCK_ON_RESUME( "property.app_lock_on_resume", "description.app_lock_on_resume", - ConfigCategory.EXPERIMENTAL, + ConfigCategory.EXPERIMENTAL_DEBUGGING, ConfigStateValue(false) ), MEO_PASSCODE_BYPASS( "property.meo_passcode_bypass", "description.meo_passcode_bypass", - ConfigCategory.EXPERIMENTAL, + ConfigCategory.EXPERIMENTAL_DEBUGGING, ConfigStateValue(false) ); diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigValue.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigValue.kt @@ -1,7 +1,22 @@ package me.rhunk.snapenhance.config abstract class ConfigValue<T> { + private val propertyChangeListeners = mutableListOf<(T) -> Unit>() + + fun addPropertyChangeListener(listener: (T) -> Unit) = propertyChangeListeners.add(listener) + fun removePropertyChangeListener(listener: (T) -> Unit) = propertyChangeListeners.remove(listener) + abstract fun value(): T - abstract fun write(): String - abstract fun read(value: String) + abstract fun read(): String + protected abstract fun write(value: String) + + protected fun onValueChanged() { + propertyChangeListeners.forEach { it(value()) } + } + + fun writeFrom(value: String) { + val oldValue = read() + write(value) + if (oldValue != value) onValueChanged() + } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigIntegerValue.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigIntegerValue.kt @@ -3,15 +3,15 @@ package me.rhunk.snapenhance.config.impl import me.rhunk.snapenhance.config.ConfigValue class ConfigIntegerValue( - var value: Int + private var value: Int ) : ConfigValue<Int>() { override fun value() = value - override fun write(): String { + override fun read(): String { return value.toString() } - override fun read(value: String) { + override fun write(value: String) { this.value = value.toInt() } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateListValue.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateListValue.kt @@ -4,17 +4,22 @@ import me.rhunk.snapenhance.config.ConfigValue class ConfigStateListValue( private val keys: List<String>, - var states: MutableMap<String, Boolean> = mutableMapOf() + private var states: MutableMap<String, Boolean> = mutableMapOf() ) : ConfigValue<Map<String, Boolean>>() { override fun value() = states - fun value(key: String) = states[key] ?: false + fun setKey(key: String, state: Boolean) { + states[key] = state + onValueChanged() + } + + operator fun get(key: String) = states[key] ?: false - override fun write(): String { + override fun read(): String { return keys.joinToString("|") { "$it:${states[it]}" } } - override fun read(value: String) { + override fun write(value: String) { value.split("|").forEach { val (key, state) = it.split(":") states[key] = state.toBoolean() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateSelection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateSelection.kt @@ -4,25 +4,19 @@ import me.rhunk.snapenhance.config.ConfigValue class ConfigStateSelection( private val keys: List<String>, - var state: String = "" + private var state: String = "" ) : ConfigValue<String>() { - fun keys(): List<String> { return keys } - override fun value(): String { - return state - } - fun value(key: String) { - state = key - } + override fun value() = state - override fun write(): String { + override fun read(): String { return state } - override fun read(value: String) { + override fun write(value: String) { state = value } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateValue.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateValue.kt @@ -3,15 +3,15 @@ package me.rhunk.snapenhance.config.impl import me.rhunk.snapenhance.config.ConfigValue class ConfigStateValue( - var value: Boolean + private var value: Boolean ) : ConfigValue<Boolean>() { override fun value() = value - override fun write(): String { + override fun read(): String { return value.toString() } - override fun read(value: String) { + override fun write(value: String) { this.value = value.toBoolean() } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt @@ -3,15 +3,18 @@ package me.rhunk.snapenhance.config.impl import me.rhunk.snapenhance.config.ConfigValue class ConfigStringValue( - var value: String = "" + private var value: String = "", + val isHidden: Boolean = false ) : ConfigValue<String>() { override fun value() = value - override fun write(): String { + fun hiddenValue() = if (isHidden) value.map { '*' }.joinToString("") else value + + override fun read(): String { return value } - override fun read(value: String) { + override fun write(value: String) { this.value = value } } \ No newline at end of file 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 @@ -60,7 +60,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } private fun canMergeOverlay(): Boolean { - if (context.config.options(ConfigProperty.DOWNLOAD_OPTIONS)["overlay_merge"] == false) return false + if (context.config.options(ConfigProperty.DOWNLOAD_OPTIONS)["merge_overlay"] == false) return false return isFFmpegPresent } @@ -228,9 +228,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam forceDownload: Boolean ) { //messages - if (paramMap.containsKey("MESSAGE_ID") && - (forceDownload || context.config.bool(ConfigProperty.AUTO_DOWNLOAD_SNAPS))) { - val id = paramMap["MESSAGE_ID"].toString() + paramMap["MESSAGE_ID"]?.toString()?.takeIf { forceDownload || canAutoDownload("friend_snaps") }?.let { id -> val messageId = id.substring(id.lastIndexOf(":") + 1).toLong() val senderId: String = context.database.getConversationMessageFromId(messageId)!!.sender_id!! @@ -244,23 +242,25 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } //private stories - val playlistV2Group = - if (paramMap.containsKey("PLAYLIST_V2_GROUP")) paramMap["PLAYLIST_V2_GROUP"].toString() else null - if (playlistV2Group != null && - playlistV2Group.contains("storyUserId=") && - (forceDownload || context.config.bool(ConfigProperty.AUTO_DOWNLOAD_STORIES)) - ) { - val storyIdStartIndex = playlistV2Group.indexOf("storyUserId=") + 12 - val storyUserId = playlistV2Group.substring(storyIdStartIndex, playlistV2Group.indexOf(",", storyIdStartIndex)) + paramMap["PLAYLIST_V2_GROUP"]?.toString()?.takeIf { + it.contains("storyUserId=") && (forceDownload || canAutoDownload("friend_stories")) + }?.let { playlistGroup -> + val storyIdStartIndex = playlistGroup.indexOf("storyUserId=") + 12 + val storyUserId = playlistGroup.substring( + storyIdStartIndex, + playlistGroup.indexOf(",", storyIdStartIndex) + ) val author = context.database.getFriendInfo(if (storyUserId == "null") context.database.getMyUserId()!! else storyUserId) + downloadOperaMedia(mediaInfoMap, author!!.usernameForSorting!!) return } + val snapSource = paramMap["SNAP_SOURCE"].toString() //public stories if ((snapSource == "PUBLIC_USER" || snapSource == "SAVED_STORY") && - (forceDownload || context.config.bool(ConfigProperty.AUTO_DOWNLOAD_PUBLIC_STORIES))) { + (forceDownload || canAutoDownload("public_stories"))) { val userDisplayName = (if (paramMap.containsKey("USER_DISPLAY_NAME")) paramMap["USER_DISPLAY_NAME"].toString() else "").replace( "[^\\x00-\\x7F]".toRegex(), "") @@ -269,7 +269,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } //spotlight - if (snapSource == "SINGLE_SNAP_STORY" && (forceDownload || context.config.bool(ConfigProperty.AUTO_DOWNLOAD_SPOTLIGHT))) { + if (snapSource == "SINGLE_SNAP_STORY" && (forceDownload || canAutoDownload("spotlight"))) { downloadOperaMedia(mediaInfoMap, "Spotlight") return } @@ -323,11 +323,9 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } } - private fun canAutoDownload(): Boolean { - return context.config.bool(ConfigProperty.AUTO_DOWNLOAD_SNAPS) || - context.config.bool(ConfigProperty.AUTO_DOWNLOAD_STORIES) || - context.config.bool(ConfigProperty.AUTO_DOWNLOAD_PUBLIC_STORIES) || - context.config.bool(ConfigProperty.AUTO_DOWNLOAD_SPOTLIGHT) + private fun canAutoDownload(keyFilter: String? = null): Boolean { + val options = context.config.options(ConfigProperty.AUTO_DOWNLOAD_OPTIONS) + return options.filter { it.value }.any { keyFilter == null || it.key.contains(keyFilter, true) } } override fun asyncOnActivityCreate() { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/AntiAutoSave.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/AntiAutoSave.kt @@ -1,19 +0,0 @@ -package me.rhunk.snapenhance.features.impl.extras - -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType -import me.rhunk.snapenhance.features.BridgeFileFeature -import me.rhunk.snapenhance.features.FeatureLoadParams - -class AntiAutoSave : BridgeFileFeature("AntiAutoSave", BridgeFileType.ANTI_AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { - readFile() - } - - fun setConversationIgnored(userId: String, state: Boolean) { - setState(userId.hashCode().toLong().toString(16), state) - } - - fun isConversationIgnored(userId: String): Boolean { - return exists(userId.hashCode().toLong().toString(16)) - } -}- \ No newline at end of file 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 @@ -1,138 +0,0 @@ -package me.rhunk.snapenhance.features.impl.extras - -import me.rhunk.snapenhance.Logger -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.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.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 -import me.rhunk.snapenhance.util.getObjectField -import java.util.concurrent.Executors - -class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - private val asyncSaveExecutorService = Executors.newSingleThreadExecutor() - - private val messageLogger by lazy { context.feature(MessageLogger::class) } - private val messaging by lazy { context.feature(Messaging::class) } - - private val myUserId by lazy { context.database.getMyUserId() } - - private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") } - private val callbackClass by lazy { context.mappings.getMappedClass("callbacks", "Callback") } - - private val updateMessageMethod by lazy { context.classCache.conversationManager.methods.first { it.name == "updateMessage" } } - private val fetchConversationWithMessagesPaginatedMethod by lazy { - context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" } - } - - private fun saveMessage(conversationId: SnapUUID, message: Message) { - val messageId = message.messageDescriptor.messageId - if (messageLogger.isMessageRemoved(messageId)) return - if (message.messageState != MessageState.COMMITTED) return - - val callback = CallbackBuilder(callbackClass) - .override("onError") { - Logger.xposedLog("Error saving message $messageId") - }.build() - - runCatching { - updateMessageMethod.invoke( - context.feature(Messaging::class).conversationManager, - conversationId.instanceNonNull(), - messageId, - context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == "SAVE" }, - callback - ) - }.onFailure { - Logger.xposedLog("Error saving message $messageId", it) - } - - //delay between saves - Thread.sleep(100L) - } - - private fun canSaveMessage(message: Message): Boolean { - if (message.messageMetadata.savedBy.any { uuid -> uuid.toString() == myUserId }) return false - //only save chats - with(message.messageContent.contentType) { - if (this != ContentType.CHAT && - this != ContentType.NOTE && - this != ContentType.STICKER && - this != ContentType.EXTERNAL_MEDIA) return false - } - return true - } - - private fun canSave(): Boolean { - with(context.feature(Messaging::class)) { - if (lastOpenedConversationUUID == null) return@canSave false - val conversation = lastOpenedConversationUUID.toString() - if (context.feature(StealthMode::class).isStealth(conversation)) return@canSave false - if (context.feature(AntiAutoSave::class).isConversationIgnored(conversation)) return@canSave false - } - return true - } - - override fun asyncOnActivityCreate() { - //called when enter in a conversation (or when a message is sent) - Hooker.hook( - context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback"), - "onFetchConversationWithMessagesComplete", - HookStage.BEFORE, - { context.config.bool(ConfigProperty.AUTO_SAVE) && canSave()} - ) { param -> - val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!) - val messages = param.arg<List<Any>>(1).map { Message(it) } - messages.forEach { - if (!canSaveMessage(it)) return@forEach - asyncSaveExecutorService.submit { - saveMessage(conversationId, it) - } - } - } - - //called when a message is received - Hooker.hook( - context.mappings.getMappedClass("callbacks", "FetchMessageCallback"), - "onFetchMessageComplete", - HookStage.BEFORE, - { context.config.bool(ConfigProperty.AUTO_SAVE) && canSave()} - ) { param -> - val message = Message(param.arg(0)) - if (!canSaveMessage(message)) return@hook - val conversationId = message.messageDescriptor.conversationId - - asyncSaveExecutorService.submit { - saveMessage(conversationId, message) - } - } - - Hooker.hook( - context.mappings.getMappedClass("callbacks", "SendMessageCallback"), - "onSuccess", - HookStage.BEFORE, - { context.config.bool(ConfigProperty.AUTO_SAVE) && canSave()} - ) { - val callback = CallbackBuilder(fetchConversationWithMessagesCallbackClass).build() - runCatching { - fetchConversationWithMessagesPaginatedMethod.invoke( - messaging.conversationManager, messaging.lastOpenedConversationUUID!!.instanceNonNull(), - Long.MAX_VALUE, - 3, - callback - ) - }.onFailure { - Logger.xposedLog("failed to save message", it) - } - } - - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/DisableVideoLengthRestriction.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/DisableVideoLengthRestriction.kt @@ -1,25 +0,0 @@ -package me.rhunk.snapenhance.features.impl.extras - -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 -import me.rhunk.snapenhance.util.setObjectField - -class DisableVideoLengthRestriction : Feature("DisableVideoLengthRestriction", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { - val gridMediaItem = context.mappings.getMappedClass("GridMediaItem") - val gridMediaItemDurationFieldName = context.mappings.getMappedValue("GridMediaItemDurationField") - - Hooker.hookConstructor(gridMediaItem, HookStage.AFTER, { - context.config.bool(ConfigProperty.DISABLE_VIDEO_LENGTH_RESTRICTION) - }) {param -> - val durationMs = param.thisObject<Any>().getObjectField(gridMediaItemDurationFieldName) as Double - if (durationMs > 60000) { - param.thisObject<Any>().setObjectField(gridMediaItemDurationFieldName, 60000) - } - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/GalleryMediaSendOverride.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/GalleryMediaSendOverride.kt @@ -1,37 +0,0 @@ -package me.rhunk.snapenhance.features.impl.extras - -import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.data.MessageSender -import me.rhunk.snapenhance.data.wrapper.impl.MessageContent -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.protobuf.ProtoReader - -class GalleryMediaSendOverride : Feature("Gallery Media Send Override", loadParams = FeatureLoadParams.INIT_SYNC) { - - override fun init() { - Hooker.hook(context.classCache.conversationManager, "sendMessageWithContent", HookStage.BEFORE) { param -> - val localMessageContent = MessageContent(param.arg(1)) - - if (localMessageContent.contentType != ContentType.EXTERNAL_MEDIA) return@hook - //story replies - val messageProtoReader = ProtoReader(localMessageContent.content) - if (messageProtoReader.exists(7)) return@hook - - when (val overrideType = context.config.state(ConfigProperty.GALLERY_MEDIA_SEND_OVERRIDE)) { - "SNAP", "LIVE_SNAP" -> { - localMessageContent.contentType = ContentType.SNAP - localMessageContent.content = MessageSender.redSnapProto(overrideType == "LIVE_SNAP") - } - "NOTE" -> { - localMessageContent.contentType = ContentType.NOTE - val mediaDuration = messageProtoReader.getInt(3, 3, 5, 1, 1, 15) ?: 0 - localMessageContent.content = MessageSender.audioNoteProto(mediaDuration) - } - } - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/LocationSpoofer.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/LocationSpoofer.kt @@ -1,64 +0,0 @@ -package me.rhunk.snapenhance.features.impl.extras - -import android.content.Intent -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 - -class LocationSpoofer: Feature("LocationSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { - Hooker.hook(context.mainActivity!!.javaClass, "onActivityResult", HookStage.BEFORE) { param -> - val intent = param.argNullable<Intent>(2) ?: return@hook - val bundle = intent.getBundleExtra("location") ?: return@hook - param.setResult(null) - val latitude = bundle.getFloat("latitude") - val longitude = bundle.getFloat("longitude") - - with(context.config) { - get(ConfigProperty.LATITUDE).read(latitude.toString()) - get(ConfigProperty.LONGITUDE).read(longitude.toString()) - writeConfig() - } - context.longToast("Location set to $latitude, $longitude") - } - - if (!context.config.bool(ConfigProperty.LOCATION_SPOOF)) return - val locationClass = android.location.Location::class.java - val locationManagerClass = android.location.LocationManager::class.java - - Hooker.hook(locationClass, "getLatitude", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(getLatitude()) - } - - Hooker.hook(locationClass, "getLongitude", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(getLongitude()) - } - - Hooker.hook(locationClass, "getAccuracy", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(getAccuracy()) - } - - //Might be redundant because it calls isProviderEnabledForUser which we also hook, meaning if isProviderEnabledForUser returns true this will also return true - Hooker.hook(locationManagerClass, "isProviderEnabled", HookStage.BEFORE) { hookAdapter -> - hookAdapter.setResult(true) - } - - Hooker.hook(locationManagerClass, "isProviderEnabledForUser", HookStage.BEFORE) {hookAdapter -> - hookAdapter.setResult(true) - } - } - - private fun getLatitude():Double { - return context.config.string(ConfigProperty.LATITUDE).toDouble() - } - - private fun getLongitude():Double { - return context.config.string(ConfigProperty.LONGITUDE).toDouble() - } - - private fun getAccuracy():Float { - return 0.0f - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/MediaQualityLevelOverride.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/MediaQualityLevelOverride.kt @@ -1,26 +0,0 @@ -package me.rhunk.snapenhance.features.impl.extras - -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 - -class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) { - override fun init() { - val enumQualityLevel = context.mappings.getMappedClass("enums", "QualityLevel") - - Hooker.hook(context.mappings.getMappedClass("MediaQualityLevelProvider"), - context.mappings.getMappedValue("MediaQualityLevelProviderMethod"), - HookStage.BEFORE, - {context.config.bool(ConfigProperty.OVERRIDE_MEDIA_QUALITY)} - ) { param -> - val currentCompressionLevel = enumQualityLevel.enumConstants - .firstOrNull { it.toString() == context.config.state(ConfigProperty.MEDIA_QUALITY_LEVEL)} ?: run { - context.longToast("Invalid media quality level: ${context.config.state(ConfigProperty.MEDIA_QUALITY_LEVEL)}") - return@hook - } - param.setResult(currentCompressionLevel) - } - } -}- \ No newline at end of file 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 @@ -1,301 +0,0 @@ -package me.rhunk.snapenhance.features.impl.extras - -import android.app.Notification -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.RemoteInput -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.os.Bundle -import android.os.UserHandle -import de.robv.android.xposed.XposedBridge -import de.robv.android.xposed.XposedHelpers -import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.data.MediaReferenceType -import me.rhunk.snapenhance.data.wrapper.impl.Message -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.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker -import me.rhunk.snapenhance.util.CallbackBuilder -import me.rhunk.snapenhance.util.EncryptionUtils -import me.rhunk.snapenhance.util.MediaDownloaderHelper -import me.rhunk.snapenhance.util.MediaType -import me.rhunk.snapenhance.util.PreviewUtils -import me.rhunk.snapenhance.util.protobuf.ProtoReader - -class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) { - companion object{ - const val ACTION_REPLY = "me.rhunk.snapenhance.action.REPLY" - } - - 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 - - private val broadcastReceiverClass by lazy { - context.androidContext.classLoader.loadClass("com.snap.widgets.core.BestFriendsWidgetProvider") - } - - private val notifyAsUserMethod by lazy { - XposedHelpers.findMethodExact( - NotificationManager::class.java, "notifyAsUser", - String::class.java, - Int::class.javaPrimitiveType, - Notification::class.java, - UserHandle::class.java - ) - } - - private val cancelAsUserMethod by lazy { - XposedHelpers.findMethodExact(NotificationManager::class.java, "cancelAsUser", String::class.java, Int::class.javaPrimitiveType, UserHandle::class.java) - } - - 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 - } - - private fun setNotificationText(notification: Notification, text: String) { - with(notification.extras) { - putString("android.text", text) - putString("android.bigText", text) - } - } - - private fun computeNotificationText(conversationId: String): String { - val messageBuilder = StringBuilder() - cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.forEach { - if (messageBuilder.isNotEmpty()) messageBuilder.append("\n") - messageBuilder.append(it) - } - return messageBuilder.toString() - } - - private fun setupNotificationActionButtons(conversationId: String, notificationData: NotificationData) { - val notificationBuilder = XposedHelpers.newInstance( - Notification.Builder::class.java, - context.androidContext, - notificationData.notification - ) as Notification.Builder - - val chatReplyInput = RemoteInput.Builder("chat_reply_input") - .setLabel("Reply") - .build() - - val replyIntent = Intent() - .setClassName(Constants.SNAPCHAT_PACKAGE_NAME, broadcastReceiverClass.name) - .putExtra("conversation_id", conversationId) - .putExtra("notification_id", notificationData.id) - .setAction(ACTION_REPLY) - - val action = Notification.Action.Builder( - null, - "Reply", - PendingIntent.getBroadcast( - context.androidContext, - System.nanoTime().toInt(), - replyIntent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - ) - ).addRemoteInput(chatReplyInput).build() - - notificationBuilder.setActions(action) - notificationData.notification = notificationBuilder.build() - } - - private fun setupBroadcastReceiverHook() { - Hooker.hook(broadcastReceiverClass, "onReceive", HookStage.BEFORE) { param -> - val androidContext = param.arg<Context>(0) - val intent = param.arg<Intent>(1) - if (intent.action != ACTION_REPLY) return@hook - param.setResult(null) - - val notificationManager = androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val updateNotification: (Int, (Notification) -> Unit) -> Unit = { notificationId, notificationBuilder -> - notificationManager.activeNotifications.firstOrNull { it.id == notificationId }?.let { - notificationBuilder(it.notification) - XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( - it.tag, it.id, it.notification, it.user - )) - } - } - - val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input") - .toString() - val conversationId = intent.getStringExtra("conversation_id")!! - val notificationId = intent.getIntExtra("notification_id", -1) - - context.database.getMyUserId()?.let { context.database.getFriendInfo(it) }?.let { myUser -> - cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input") - - updateNotification(notificationId) { notification -> - setNotificationText(notification, computeNotificationText(conversationId)) - } - - context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = { - context.longToast("Failed to send message: $it") - }) - } - } - } - - 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 } - - XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( - notificationData.tag, if (forceCreate) System.nanoTime().toInt() else notificationData.id, notificationData.notification, notificationData.userHandle - )) - } - - notificationDataQueue.entries.onEach { (messageId, notificationData) -> - val snapMessage = messages.firstOrNull { message -> message.orderKey == messageId } ?: return - val senderUsername = context.database.getFriendInfo(snapMessage.senderId.toString())?.displayName ?: throw Throwable("Cant find senderId of message $snapMessage") - - val contentType = snapMessage.messageContent.contentType - val contentData = snapMessage.messageContent.content - - val formatUsername: (String) -> String = { "$senderUsername: $it" } - val notificationCache = cachedMessages.let { it.computeIfAbsent(conversationId) { mutableListOf() } } - val appendNotifications: () -> Unit = { setNotificationText(notificationData.notification, computeNotificationText(conversationId))} - - 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-> { - //serialize the message content into a json object - val serializedMessageContent = context.gson.toJsonTree(snapMessage.messageContent.instanceNonNull()).asJsonObject - val mediaReferences = serializedMessageContent["mRemoteMediaReferences"] - .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } - .flatten() - - mediaReferences.forEach { media -> - val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() - val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) - runCatching { - //download the media - val mediaInfo = ProtoReader(contentData).let { - if (contentType == ContentType.EXTERNAL_MEDIA) - return@let it.readPath(*Constants.MESSAGE_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH) - else - return@let it.readPath(*Constants.MESSAGE_SNAP_ENCRYPTION_PROTO_PATH) - }?: return@runCatching - - val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference, mergeOverlay = false, isPreviewMode = false) { - if (mediaInfo.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX)) - EncryptionUtils.decryptInputStream(it, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX) - else it - }[MediaType.ORIGINAL] ?: throw Throwable("Failed to download media") - - val bitmapPreview = PreviewUtils.createPreview(downloadedMedia, mediaType == MediaReferenceType.VIDEO)!! - 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 { - Logger.xposedLog("Failed to send preview notification", it) - } - } - } - else -> { - notificationCache.add(formatUsername("sent $contentType")) - } - } - - if (contentType == ContentType.CHAT) { - setupNotificationActionButtons(conversationId, notificationData) - } - - sendNotificationData(notificationData, false) - }.clear() - } - - private fun shouldIgnoreNotification(type: String): Boolean { - 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 } - states["typing"]?.let { if (type.endsWith("TYPING") && it) return true } - - return false - } - - override fun init() { - setupBroadcastReceiverHook() - - val fetchConversationWithMessagesCallback = context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") - - Hooker.hook(notifyAsUserMethod, HookStage.BEFORE) { param -> - val notificationData = NotificationData(param.argNullable(0), param.arg(1), param.arg(2), param.arg(3)) - - 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 (shouldIgnoreNotification(notificationType)) { - param.setResult(null) - return@hook - } - - if (!context.config.bool(ConfigProperty.SHOW_MESSAGE_CONTENT_IN_NOTIFICATIONS)) return@hook - - if (!notificationType.endsWith("CHAT") && !notificationType.endsWith("SNAP")) return@hook - - val conversationManager: Any = context.feature(Messaging::class).conversationManager - notificationDataQueue[messageId.toLong()] = notificationData - - val callback = CallbackBuilder(fetchConversationWithMessagesCallback) - .override("onFetchConversationWithMessagesComplete") { callbackParam -> - val messageList = (callbackParam.arg(1) as List<Any>).map { msg -> Message(msg) } - fetchMessagesResult(conversationId, messageList) - } - .override("onError") { - Logger.xposedLog("Failed to fetch message ${it.arg(0) as Any}") - }.build() - - fetchConversationWithMessagesMethod.invoke(conversationManager, SnapUUID.fromString(conversationId).instanceNonNull(), callback) - param.setResult(null) - } - - Hooker.hook(cancelAsUserMethod, HookStage.BEFORE) { param -> - val notificationId = param.arg<Int>(1) - notificationIdMap[notificationId]?.let { - cachedMessages[it]?.clear() - } - } - } - - data class NotificationData( - val tag: String?, - val id: Int, - var notification: Notification, - val userHandle: UserHandle - ) -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/SnapchatPlus.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/SnapchatPlus.kt @@ -1,29 +0,0 @@ -package me.rhunk.snapenhance.features.impl.extras - -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 - -class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - private val originalSubscriptionTime = (System.currentTimeMillis() - 7776000000L) - private val expirationTimeMillis = (System.currentTimeMillis() + 15552000000L) - - override fun asyncOnActivityCreate() { - if (!context.config.bool(ConfigProperty.SNAPCHAT_PLUS)) return - - val subscriptionInfoClass = context.mappings.getMappedClass("SubscriptionInfoClass") - - Hooker.hookConstructor(subscriptionInfoClass, HookStage.BEFORE) { param -> - if (param.arg<Int>(0) == 2) return@hookConstructor - //subscription tier - param.setArg(0, 2) - //subscription status - param.setArg(1, 2) - - param.setArg(2, originalSubscriptionTime) - param.setArg(3, expirationTimeMillis) - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/UnlimitedSnapViewTime.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/UnlimitedSnapViewTime.kt @@ -1,36 +0,0 @@ -package me.rhunk.snapenhance.features.impl.extras - -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 -import me.rhunk.snapenhance.util.protobuf.ProtoEditor -import me.rhunk.snapenhance.util.protobuf.ProtoReader - -class UnlimitedSnapViewTime : - Feature("UnlimitedSnapViewTime", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { - Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { - context.config.bool(ConfigProperty.UNLIMITED_SNAP_VIEW_TIME) - }) { param -> - val message = Message(param.thisObject()) - if (message.messageState != MessageState.COMMITTED) return@hookConstructor - if (message.messageContent.contentType != ContentType.SNAP) return@hookConstructor - - with(message.messageContent) { - val mediaAttributes = ProtoReader(this.content).readPath(11, 5, 2) ?: return@hookConstructor - if (mediaAttributes.exists(6)) return@hookConstructor - this.content = ProtoEditor(this.content).apply { - edit(11, 5, 2) { - mediaAttributes.getInt(5)?.let { writeConstant(5, it) } - writeBuffer(6, byteArrayOf()) - } - }.toByteArray() - } - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt @@ -0,0 +1,19 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.features.BridgeFileFeature +import me.rhunk.snapenhance.features.FeatureLoadParams + +class AntiAutoSave : BridgeFileFeature("AntiAutoSave", BridgeFileType.ANTI_AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + override fun onActivityCreate() { + readFile() + } + + fun setConversationIgnored(userId: String, state: Boolean) { + setState(userId.hashCode().toLong().toString(16), state) + } + + fun isConversationIgnored(userId: String): Boolean { + return exists(userId.hashCode().toLong().toString(16)) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt @@ -0,0 +1,134 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.MessageState +import me.rhunk.snapenhance.data.wrapper.impl.Message +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.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 +import me.rhunk.snapenhance.util.getObjectField +import java.util.concurrent.Executors + +class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + private val asyncSaveExecutorService = Executors.newSingleThreadExecutor() + + private val messageLogger by lazy { context.feature(MessageLogger::class) } + private val messaging by lazy { context.feature(Messaging::class) } + + private val myUserId by lazy { context.database.getMyUserId() } + + private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") } + private val callbackClass by lazy { context.mappings.getMappedClass("callbacks", "Callback") } + + private val updateMessageMethod by lazy { context.classCache.conversationManager.methods.first { it.name == "updateMessage" } } + private val fetchConversationWithMessagesPaginatedMethod by lazy { + context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" } + } + + private fun saveMessage(conversationId: SnapUUID, message: Message) { + val messageId = message.messageDescriptor.messageId + if (messageLogger.isMessageRemoved(messageId)) return + if (message.messageState != MessageState.COMMITTED) return + + val callback = CallbackBuilder(callbackClass) + .override("onError") { + Logger.xposedLog("Error saving message $messageId") + }.build() + + runCatching { + updateMessageMethod.invoke( + context.feature(Messaging::class).conversationManager, + conversationId.instanceNonNull(), + messageId, + context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == "SAVE" }, + callback + ) + }.onFailure { + Logger.xposedLog("Error saving message $messageId", it) + } + + //delay between saves + Thread.sleep(100L) + } + + private fun canSaveMessage(message: Message): Boolean { + if (message.messageMetadata.savedBy.any { uuid -> uuid.toString() == myUserId }) return false + val contentType = message.messageContent.contentType.toString() + + return context.config.options(ConfigProperty.AUTO_SAVE_MESSAGES).filter { it.value }.any { it.key == contentType } + } + + private fun canSave(): Boolean { + if (context.config.options(ConfigProperty.AUTO_SAVE_MESSAGES).none { it.value }) return false + + with(context.feature(Messaging::class)) { + if (lastOpenedConversationUUID == null) return@canSave false + val conversation = lastOpenedConversationUUID.toString() + if (context.feature(StealthMode::class).isStealth(conversation)) return@canSave false + if (context.feature(AntiAutoSave::class).isConversationIgnored(conversation)) return@canSave false + } + return true + } + + override fun asyncOnActivityCreate() { + //called when enter in a conversation (or when a message is sent) + Hooker.hook( + context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback"), + "onFetchConversationWithMessagesComplete", + HookStage.BEFORE, + { canSave() } + ) { param -> + val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!) + val messages = param.arg<List<Any>>(1).map { Message(it) } + messages.forEach { + if (!canSaveMessage(it)) return@forEach + asyncSaveExecutorService.submit { + saveMessage(conversationId, it) + } + } + } + + //called when a message is received + Hooker.hook( + context.mappings.getMappedClass("callbacks", "FetchMessageCallback"), + "onFetchMessageComplete", + HookStage.BEFORE, + { canSave() } + ) { param -> + val message = Message(param.arg(0)) + if (!canSaveMessage(message)) return@hook + val conversationId = message.messageDescriptor.conversationId + + asyncSaveExecutorService.submit { + saveMessage(conversationId, message) + } + } + + Hooker.hook( + context.mappings.getMappedClass("callbacks", "SendMessageCallback"), + "onSuccess", + HookStage.BEFORE, + { canSave() } + ) { + val callback = CallbackBuilder(fetchConversationWithMessagesCallbackClass).build() + runCatching { + fetchConversationWithMessagesPaginatedMethod.invoke( + messaging.conversationManager, messaging.lastOpenedConversationUUID!!.instanceNonNull(), + Long.MAX_VALUE, + 3, + callback + ) + }.onFailure { + Logger.xposedLog("failed to save message", it) + } + } + + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt @@ -0,0 +1,25 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +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 +import me.rhunk.snapenhance.util.setObjectField + +class DisableVideoLengthRestriction : Feature("DisableVideoLengthRestriction", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { + val gridMediaItem = context.mappings.getMappedClass("GridMediaItem") + val gridMediaItemDurationFieldName = context.mappings.getMappedValue("GridMediaItemDurationField") + + Hooker.hookConstructor(gridMediaItem, HookStage.AFTER, { + context.config.bool(ConfigProperty.DISABLE_VIDEO_LENGTH_RESTRICTION) + }) {param -> + val durationMs = param.thisObject<Any>().getObjectField(gridMediaItemDurationFieldName) as Double + if (durationMs > 60000) { + param.thisObject<Any>().setObjectField(gridMediaItemDurationFieldName, 60000) + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt @@ -0,0 +1,37 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.MessageSender +import me.rhunk.snapenhance.data.wrapper.impl.MessageContent +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.protobuf.ProtoReader + +class GalleryMediaSendOverride : Feature("Gallery Media Send Override", loadParams = FeatureLoadParams.INIT_SYNC) { + + override fun init() { + Hooker.hook(context.classCache.conversationManager, "sendMessageWithContent", HookStage.BEFORE) { param -> + val localMessageContent = MessageContent(param.arg(1)) + + if (localMessageContent.contentType != ContentType.EXTERNAL_MEDIA) return@hook + //story replies + val messageProtoReader = ProtoReader(localMessageContent.content) + if (messageProtoReader.exists(7)) return@hook + + when (val overrideType = context.config.state(ConfigProperty.GALLERY_MEDIA_SEND_OVERRIDE)) { + "SNAP", "LIVE_SNAP" -> { + localMessageContent.contentType = ContentType.SNAP + localMessageContent.content = MessageSender.redSnapProto(overrideType == "LIVE_SNAP") + } + "NOTE" -> { + localMessageContent.contentType = ContentType.NOTE + val mediaDuration = messageProtoReader.getInt(3, 3, 5, 1, 1, 15) ?: 0 + localMessageContent.content = MessageSender.audioNoteProto(mediaDuration) + } + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/LocationSpoofer.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/LocationSpoofer.kt @@ -0,0 +1,64 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +import android.content.Intent +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 + +class LocationSpoofer: Feature("LocationSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { + Hooker.hook(context.mainActivity!!.javaClass, "onActivityResult", HookStage.BEFORE) { param -> + val intent = param.argNullable<Intent>(2) ?: return@hook + val bundle = intent.getBundleExtra("location") ?: return@hook + param.setResult(null) + val latitude = bundle.getFloat("latitude") + val longitude = bundle.getFloat("longitude") + + with(context.config) { + get(ConfigProperty.LATITUDE).writeFrom(latitude.toString()) + get(ConfigProperty.LONGITUDE).writeFrom(longitude.toString()) + writeConfig() + } + context.longToast("Location set to $latitude, $longitude") + } + + if (!context.config.bool(ConfigProperty.LOCATION_SPOOF)) return + val locationClass = android.location.Location::class.java + val locationManagerClass = android.location.LocationManager::class.java + + Hooker.hook(locationClass, "getLatitude", HookStage.BEFORE) { hookAdapter -> + hookAdapter.setResult(getLatitude()) + } + + Hooker.hook(locationClass, "getLongitude", HookStage.BEFORE) { hookAdapter -> + hookAdapter.setResult(getLongitude()) + } + + Hooker.hook(locationClass, "getAccuracy", HookStage.BEFORE) { hookAdapter -> + hookAdapter.setResult(getAccuracy()) + } + + //Might be redundant because it calls isProviderEnabledForUser which we also hook, meaning if isProviderEnabledForUser returns true this will also return true + Hooker.hook(locationManagerClass, "isProviderEnabled", HookStage.BEFORE) { hookAdapter -> + hookAdapter.setResult(true) + } + + Hooker.hook(locationManagerClass, "isProviderEnabledForUser", HookStage.BEFORE) {hookAdapter -> + hookAdapter.setResult(true) + } + } + + private fun getLatitude():Double { + return context.config.string(ConfigProperty.LATITUDE).toDouble() + } + + private fun getLongitude():Double { + return context.config.string(ConfigProperty.LONGITUDE).toDouble() + } + + private fun getAccuracy():Float { + return 0.0f + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt @@ -0,0 +1,26 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +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 + +class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) { + override fun init() { + val enumQualityLevel = context.mappings.getMappedClass("enums", "QualityLevel") + + Hooker.hook(context.mappings.getMappedClass("MediaQualityLevelProvider"), + context.mappings.getMappedValue("MediaQualityLevelProviderMethod"), + HookStage.BEFORE, + {context.config.bool(ConfigProperty.OVERRIDE_MEDIA_QUALITY)} + ) { param -> + val currentCompressionLevel = enumQualityLevel.enumConstants + .firstOrNull { it.toString() == context.config.state(ConfigProperty.MEDIA_QUALITY_LEVEL)} ?: run { + context.longToast("Invalid media quality level: ${context.config.state(ConfigProperty.MEDIA_QUALITY_LEVEL)}") + return@hook + } + param.setResult(currentCompressionLevel) + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt @@ -0,0 +1,300 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.RemoteInput +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.os.Bundle +import android.os.UserHandle +import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.MediaReferenceType +import me.rhunk.snapenhance.data.wrapper.impl.Message +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.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.util.CallbackBuilder +import me.rhunk.snapenhance.util.EncryptionUtils +import me.rhunk.snapenhance.util.MediaDownloaderHelper +import me.rhunk.snapenhance.util.MediaType +import me.rhunk.snapenhance.util.PreviewUtils +import me.rhunk.snapenhance.util.protobuf.ProtoReader + +class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) { + companion object{ + const val ACTION_REPLY = "me.rhunk.snapenhance.action.REPLY" + } + + 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 + + private val broadcastReceiverClass by lazy { + context.androidContext.classLoader.loadClass("com.snap.widgets.core.BestFriendsWidgetProvider") + } + + private val notifyAsUserMethod by lazy { + XposedHelpers.findMethodExact( + NotificationManager::class.java, "notifyAsUser", + String::class.java, + Int::class.javaPrimitiveType, + Notification::class.java, + UserHandle::class.java + ) + } + + private val cancelAsUserMethod by lazy { + XposedHelpers.findMethodExact(NotificationManager::class.java, "cancelAsUser", String::class.java, Int::class.javaPrimitiveType, UserHandle::class.java) + } + + 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 + } + + private fun setNotificationText(notification: Notification, text: String) { + with(notification.extras) { + putString("android.text", text) + putString("android.bigText", text) + } + } + + private fun computeNotificationText(conversationId: String): String { + val messageBuilder = StringBuilder() + cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.forEach { + if (messageBuilder.isNotEmpty()) messageBuilder.append("\n") + messageBuilder.append(it) + } + return messageBuilder.toString() + } + + private fun setupNotificationActionButtons(conversationId: String, notificationData: NotificationData) { + val notificationBuilder = XposedHelpers.newInstance( + Notification.Builder::class.java, + context.androidContext, + notificationData.notification + ) as Notification.Builder + + val chatReplyInput = RemoteInput.Builder("chat_reply_input") + .setLabel("Reply") + .build() + + val replyIntent = Intent() + .setClassName(Constants.SNAPCHAT_PACKAGE_NAME, broadcastReceiverClass.name) + .putExtra("conversation_id", conversationId) + .putExtra("notification_id", notificationData.id) + .setAction(ACTION_REPLY) + + val action = Notification.Action.Builder( + null, + "Reply", + PendingIntent.getBroadcast( + context.androidContext, + System.nanoTime().toInt(), + replyIntent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE + ) + ).addRemoteInput(chatReplyInput).build() + + notificationBuilder.setActions(action) + notificationData.notification = notificationBuilder.build() + } + + private fun setupBroadcastReceiverHook() { + Hooker.hook(broadcastReceiverClass, "onReceive", HookStage.BEFORE) { param -> + val androidContext = param.arg<Context>(0) + val intent = param.arg<Intent>(1) + if (intent.action != ACTION_REPLY) return@hook + param.setResult(null) + + val notificationManager = androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val updateNotification: (Int, (Notification) -> Unit) -> Unit = { notificationId, notificationBuilder -> + notificationManager.activeNotifications.firstOrNull { it.id == notificationId }?.let { + notificationBuilder(it.notification) + XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( + it.tag, it.id, it.notification, it.user + )) + } + } + + val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input") + .toString() + val conversationId = intent.getStringExtra("conversation_id")!! + val notificationId = intent.getIntExtra("notification_id", -1) + + context.database.getMyUserId()?.let { context.database.getFriendInfo(it) }?.let { myUser -> + cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input") + + updateNotification(notificationId) { notification -> + setNotificationText(notification, computeNotificationText(conversationId)) + } + + context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = { + context.longToast("Failed to send message: $it") + }) + } + } + } + + 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 } + + XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( + notificationData.tag, if (forceCreate) System.nanoTime().toInt() else notificationData.id, notificationData.notification, notificationData.userHandle + )) + } + + notificationDataQueue.entries.onEach { (messageId, notificationData) -> + val snapMessage = messages.firstOrNull { message -> message.orderKey == messageId } ?: return + val senderUsername = context.database.getFriendInfo(snapMessage.senderId.toString())?.displayName ?: throw Throwable("Cant find senderId of message $snapMessage") + + val contentType = snapMessage.messageContent.contentType + val contentData = snapMessage.messageContent.content + + val formatUsername: (String) -> String = { "$senderUsername: $it" } + val notificationCache = cachedMessages.let { it.computeIfAbsent(conversationId) { mutableListOf() } } + val appendNotifications: () -> Unit = { setNotificationText(notificationData.notification, computeNotificationText(conversationId))} + + 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-> { + //serialize the message content into a json object + val serializedMessageContent = context.gson.toJsonTree(snapMessage.messageContent.instanceNonNull()).asJsonObject + val mediaReferences = serializedMessageContent["mRemoteMediaReferences"] + .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } + .flatten() + + mediaReferences.forEach { media -> + val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() + val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) + runCatching { + //download the media + val mediaInfo = ProtoReader(contentData).let { + if (contentType == ContentType.EXTERNAL_MEDIA) + return@let it.readPath(*Constants.MESSAGE_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH) + else + return@let it.readPath(*Constants.MESSAGE_SNAP_ENCRYPTION_PROTO_PATH) + }?: return@runCatching + + val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference, mergeOverlay = false, isPreviewMode = false) { + if (mediaInfo.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX)) + EncryptionUtils.decryptInputStream(it, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX) + else it + }[MediaType.ORIGINAL] ?: throw Throwable("Failed to download media") + + val bitmapPreview = PreviewUtils.createPreview(downloadedMedia, mediaType.name.contains("VIDEO"))!! + 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 { + Logger.xposedLog("Failed to send preview notification", it) + } + } + } + else -> { + notificationCache.add(formatUsername("sent $contentType")) + } + } + + if (contentType == ContentType.CHAT && context.config.options(ConfigProperty.BETTER_NOTIFICATIONS)["reply_button"] == true) { + setupNotificationActionButtons(conversationId, notificationData) + } + + sendNotificationData(notificationData, false) + }.clear() + } + + private fun shouldIgnoreNotification(type: String): Boolean { + 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 } + states["typing"]?.let { if (type.endsWith("TYPING") && it) return true } + + return false + } + + override fun init() { + setupBroadcastReceiverHook() + + val fetchConversationWithMessagesCallback = context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") + + Hooker.hook(notifyAsUserMethod, HookStage.BEFORE) { param -> + val notificationData = NotificationData(param.argNullable(0), param.arg(1), param.arg(2), param.arg(3)) + + 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 (shouldIgnoreNotification(notificationType)) { + param.setResult(null) + return@hook + } + + if (context.config.options(ConfigProperty.BETTER_NOTIFICATIONS) + .filter { it.value }.none { notificationType.endsWith(it.key.uppercase())}) return@hook + + val conversationManager: Any = context.feature(Messaging::class).conversationManager + notificationDataQueue[messageId.toLong()] = notificationData + + val callback = CallbackBuilder(fetchConversationWithMessagesCallback) + .override("onFetchConversationWithMessagesComplete") { callbackParam -> + val messageList = (callbackParam.arg(1) as List<Any>).map { msg -> Message(msg) } + fetchMessagesResult(conversationId, messageList) + } + .override("onError") { + Logger.xposedLog("Failed to fetch message ${it.arg(0) as Any}") + }.build() + + fetchConversationWithMessagesMethod.invoke(conversationManager, SnapUUID.fromString(conversationId).instanceNonNull(), callback) + param.setResult(null) + } + + Hooker.hook(cancelAsUserMethod, HookStage.BEFORE) { param -> + val notificationId = param.arg<Int>(1) + notificationIdMap[notificationId]?.let { + cachedMessages[it]?.clear() + } + } + } + + data class NotificationData( + val tag: String?, + val id: Int, + var notification: Notification, + val userHandle: UserHandle + ) +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt @@ -0,0 +1,29 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +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 + +class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + private val originalSubscriptionTime = (System.currentTimeMillis() - 7776000000L) + private val expirationTimeMillis = (System.currentTimeMillis() + 15552000000L) + + override fun asyncOnActivityCreate() { + if (!context.config.bool(ConfigProperty.SNAPCHAT_PLUS)) return + + val subscriptionInfoClass = context.mappings.getMappedClass("SubscriptionInfoClass") + + Hooker.hookConstructor(subscriptionInfoClass, HookStage.BEFORE) { param -> + if (param.arg<Int>(0) == 2) return@hookConstructor + //subscription tier + param.setArg(0, 2) + //subscription status + param.setArg(1, 2) + + param.setArg(2, originalSubscriptionTime) + param.setArg(3, expirationTimeMillis) + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt @@ -0,0 +1,36 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +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 +import me.rhunk.snapenhance.util.protobuf.ProtoEditor +import me.rhunk.snapenhance.util.protobuf.ProtoReader + +class UnlimitedSnapViewTime : + Feature("UnlimitedSnapViewTime", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + override fun onActivityCreate() { + Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { + context.config.bool(ConfigProperty.UNLIMITED_SNAP_VIEW_TIME) + }) { param -> + val message = Message(param.thisObject()) + if (message.messageState != MessageState.COMMITTED) return@hookConstructor + if (message.messageContent.contentType != ContentType.SNAP) return@hookConstructor + + with(message.messageContent) { + val mediaAttributes = ProtoReader(this.content).readPath(11, 5, 2) ?: return@hookConstructor + if (mediaAttributes.exists(6)) return@hookConstructor + this.content = ProtoEditor(this.content).apply { + edit(11, 5, 2) { + mediaAttributes.getInt(5)?.let { writeConstant(5, it) } + writeBuffer(6, byteArrayOf()) + } + }.toByteArray() + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt @@ -22,14 +22,15 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE val chatNoteRecordButton = resources.getIdentifier("chat_note_record_button", "id", Constants.SNAPCHAT_PACKAGE_NAME) val chatInputBarSticker = resources.getIdentifier("chat_input_bar_sticker", "id", Constants.SNAPCHAT_PACKAGE_NAME) val chatInputBarCognac = resources.getIdentifier("chat_input_bar_cognac", "id", Constants.SNAPCHAT_PACKAGE_NAME) - + val hiddenElements = context.config.options(ConfigProperty.HIDE_UI_ELEMENTS) + Hooker.hook(View::class.java, "setVisibility", HookStage.BEFORE) { methodParam -> val viewId = (methodParam.thisObject() as View).id - if (viewId == chatNoteRecordButton && context.config.bool(ConfigProperty.REMOVE_VOICE_RECORD_BUTTON)) { + if (viewId == chatNoteRecordButton && hiddenElements["remove_voice_record_button"] == true) { methodParam.setArg(0, View.GONE) } if (viewId == callButton1 || viewId == callButton2) { - if (!context.config.bool(ConfigProperty.REMOVE_CALL_BUTTONS)) return@hook + if (hiddenElements["remove_call_buttons"] == false) return@hook methodParam.setArg(0, View.GONE) } } @@ -45,23 +46,23 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE val view: View = param.arg(0) val viewId = view.id - if (viewId == chatNoteRecordButton && context.config.bool(ConfigProperty.REMOVE_VOICE_RECORD_BUTTON)) { + if (viewId == chatNoteRecordButton && hiddenElements["remove_voice_record_button"] == true) { view.isEnabled = false view.setWillNotDraw(true) } - if (chatInputBarCognac == viewId && context.config.bool(ConfigProperty.REMOVE_COGNAC_BUTTON)) { + if (chatInputBarCognac == viewId && hiddenElements["remove_cognac_button"] == true) { view.visibility = View.GONE } - if (chatInputBarSticker == viewId && context.config.bool(ConfigProperty.REMOVE_STICKERS_BUTTON)) { + if (chatInputBarSticker == viewId && hiddenElements["remove_stickers_button"] == true) { view.visibility = View.GONE } if (viewId == callButton1 || viewId == callButton2) { - if (!context.config.bool(ConfigProperty.REMOVE_CALL_BUTTONS)) return@hook + if (hiddenElements["remove_call_buttons"] == false) return@hook if (view.visibility == View.GONE) return@hook } if (viewId == callButtonsStub) { - if (!context.config.bool(ConfigProperty.REMOVE_CALL_BUTTONS)) return@hook + if (hiddenElements["remove_call_buttons"] == false) return@hook param.setResult(null) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/MapActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/MapActivity.kt @@ -1,10 +1,13 @@ package me.rhunk.snapenhance.features.impl.ui.menus +import android.annotation.SuppressLint import android.app.Activity +import android.app.AlertDialog import android.content.Context import android.os.Bundle import android.view.MotionEvent import android.widget.Button +import android.widget.EditText import me.rhunk.snapenhance.R import org.osmdroid.config.Configuration import org.osmdroid.tileprovider.tilesource.TileSourceFactory @@ -20,12 +23,13 @@ class MapActivity : Activity() { private lateinit var mapView: MapView + @SuppressLint("MissingInflatedId", "ResourceType") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val contextBundle = intent.extras?.getBundle("location") ?: return - val latitude = contextBundle.getDouble("latitude") - val longitude = contextBundle.getDouble("longitude") + val locationLatitude = contextBundle.getDouble("latitude") + val locationLongitude = contextBundle.getDouble("longitude") Configuration.getInstance().load(applicationContext, getSharedPreferences("osmdroid", Context.MODE_PRIVATE)) @@ -35,7 +39,7 @@ class MapActivity : Activity() { mapView.setMultiTouchControls(true); mapView.setTileSource(TileSourceFactory.MAPNIK) - val startPoint = GeoPoint(latitude, longitude) + val startPoint = GeoPoint(locationLatitude, locationLongitude) mapView.controller.setZoom(10.0) mapView.controller.setCenter(startPoint) @@ -64,6 +68,28 @@ class MapActivity : Activity() { setResult(RESULT_OK, intent.putExtra("location", bundle)) finish() } + + val setPreciseLocationButton = findViewById<Button>(R.id.set_precise_location_button) + + setPreciseLocationButton.setOnClickListener { + val locationDialog = layoutInflater.inflate(R.layout.precise_location_dialog, null) + val dialogLatitude = locationDialog.findViewById<EditText>(R.id.dialog_latitude).also { it.setText(marker.position.latitude.toString()) } + val dialogLongitude = locationDialog.findViewById<EditText>(R.id.dialog_longitude).also { it.setText(marker.position.longitude.toString()) } + + AlertDialog.Builder(this) + .setView(locationDialog) + .setTitle("Set a precise location") + .setPositiveButton("Set") { _, _ -> + val latitude = dialogLatitude.text.toString().toDoubleOrNull() + val longitude = dialogLongitude.text.toString().toDoubleOrNull() + if (latitude != null && longitude != null) { + val preciseLocation = GeoPoint(latitude, longitude) + mapView.controller.setCenter(preciseLocation) + marker.position = preciseLocation + mapView.invalidate() + } + }.setNegativeButton("Cancel") { _, _ -> }.show() + } } override fun onDestroy() { 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 @@ -64,34 +64,35 @@ class ChatActionMenu : AbstractMenu() { ) ) } - if (context.config.bool(ConfigProperty.DOWNLOAD_INCHAT_SNAPS)) { - val previewButton = Button(viewGroup.context) - applyButtonTheme(parent, previewButton) - previewButton.text = "Preview" - previewButton.setOnClickListener { - closeActionMenu() - context.executeAsync { context.feature(MediaDownloader::class).onMessageActionMenu(true) } - } - parent.addView(previewButton) - } + if (context.config.bool(ConfigProperty.CHAT_DOWNLOAD_CONTEXT_MENU)) { + parent.addView(Button(viewGroup.context).apply { + applyButtonTheme(parent, this) + text = this@ChatActionMenu.context.translation.get("chat_action_menu.preview_button") + setOnClickListener { + closeActionMenu() + this@ChatActionMenu.context.executeAsync { this@ChatActionMenu.context.feature(MediaDownloader::class).onMessageActionMenu(true) } + } + }) - //download snap in chat - if (context.config.bool(ConfigProperty.DOWNLOAD_INCHAT_SNAPS)) { - val downloadButton = Button(viewGroup.context) - applyButtonTheme(parent, downloadButton) - downloadButton.text = "Download" - downloadButton.setOnClickListener { - closeActionMenu() - context.executeAsync { context.feature(MediaDownloader::class).onMessageActionMenu(false) } - } - parent.addView(downloadButton) + parent.addView(Button(viewGroup.context).apply { + applyButtonTheme(parent, this) + text = this@ChatActionMenu.context.translation.get("chat_action_menu.download_button") + setOnClickListener { + closeActionMenu() + this@ChatActionMenu.context.executeAsync { + this@ChatActionMenu.context.feature( + MediaDownloader::class + ).onMessageActionMenu(false) + } + } + }) } //delete logged message button if (context.config.bool(ConfigProperty.MESSAGE_LOGGER)) { val downloadButton = Button(viewGroup.context) applyButtonTheme(parent, downloadButton) - downloadButton.text = "Deleted logged message" + downloadButton.text = context.translation.get("chat_action_menu.delete_logged_message_button") downloadButton.setOnClickListener { closeActionMenu() context.executeAsync { 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 @@ -21,7 +21,7 @@ import me.rhunk.snapenhance.database.objects.FriendInfo 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.tweaks.AntiAutoSave 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 @@ -215,7 +215,7 @@ class FriendFeedInfoMenu : AbstractMenu() { run { val userId = context.database.getFriendFeedInfoByConversationId(conversationId)?.friendUserId ?: return@run - if (context.config.bool(ConfigProperty.ANTI_DOWNLOAD_BUTTON)) { + if (context.config.bool(ConfigProperty.DOWNLOAD_BLACKLIST)) { createToggleFeature(viewModel, viewConsumer, "friend_menu_option.anti_auto_download", 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 @@ -35,10 +35,11 @@ class SettingsMenu : AbstractMenu() { @SuppressLint("SetTextI18n") private fun createPropertyView(viewModel: View, property: ConfigProperty): View { val updateButtonText: (TextView, String) -> Unit = { textView, text -> - textView.text = "${context.translation.get(property.nameKey)} $text" + textView.text = "${context.translation.get(property.nameKey)}${if (text.isEmpty()) "" else ": $text"}" } - val updateStateSelectionText: (TextView, String) -> Unit = { textView, text -> - updateButtonText(textView, text.let { if (it.isEmpty()) "(empty)" else ": $it" }) + + val updateLocalizedText: (TextView, String) -> Unit = { textView, value -> + updateButtonText(textView, value.let { if (it.isEmpty()) "(empty)" else context.translation.get("option." + property.nameKey + "." + it) }) } val textEditor: ((String) -> Unit) -> Unit = { updateValue -> @@ -61,23 +62,29 @@ class SettingsMenu : AbstractMenu() { val resultView: View = when (property.valueContainer) { is ConfigStringValue -> { val textView = TextView(viewModel.context) - updateButtonText(textView, property.valueContainer.value) + updateButtonText(textView, property.valueContainer.let { + if (it.isHidden) it.hiddenValue() + else it.value() + }) ViewAppearanceHelper.applyTheme(viewModel, textView) textView.setOnClickListener { textEditor { value -> - property.valueContainer.value = value - updateButtonText(textView, value) + property.valueContainer.writeFrom(value) + updateButtonText(textView, property.valueContainer.let { + if (it.isHidden) it.hiddenValue() + else it.value() + }) } } textView } is ConfigIntegerValue -> { val button = Button(viewModel.context) - updateButtonText(button, property.valueContainer.value.toString()) + updateButtonText(button, property.valueContainer.value().toString()) button.setOnClickListener { textEditor { value -> runCatching { - property.valueContainer.value = value.toInt() + property.valueContainer.writeFrom(value) updateButtonText(button, value) }.onFailure { context.shortToast("Invalid value") @@ -90,30 +97,30 @@ class SettingsMenu : AbstractMenu() { is ConfigStateValue -> { val switch = Switch(viewModel.context) switch.text = context.translation.get(property.nameKey) - switch.isChecked = property.valueContainer.value + switch.isChecked = property.valueContainer.value() switch.setOnCheckedChangeListener { _, isChecked -> - property.valueContainer.value = isChecked + property.valueContainer.writeFrom(isChecked.toString()) } ViewAppearanceHelper.applyTheme(viewModel, switch) switch } is ConfigStateSelection -> { val button = Button(viewModel.context) - updateStateSelectionText(button, property.valueContainer.value()) + updateLocalizedText(button, property.valueContainer.value()) button.setOnClickListener {_ -> val builder = AlertDialog.Builder(viewModel.context) builder.setTitle(context.translation.get(property.nameKey)) builder.setSingleChoiceItems( - property.valueContainer.keys().toTypedArray(), + property.valueContainer.keys().toTypedArray().map { context.translation.get("option." + property.nameKey + "." + it) }.toTypedArray(), property.valueContainer.keys().indexOf(property.valueContainer.value()) ) { _, which -> - property.valueContainer.value(property.valueContainer.keys()[which]) + property.valueContainer.writeFrom(property.valueContainer.keys()[which]) } builder.setPositiveButton("OK") { _, _ -> - updateStateSelectionText(button, property.valueContainer.value()) + updateLocalizedText(button, property.valueContainer.value()) } builder.show() @@ -123,25 +130,25 @@ class SettingsMenu : AbstractMenu() { } is ConfigStateListValue -> { val button = Button(viewModel.context) - updateStateSelectionText(button, property.valueContainer.toString()) + updateButtonText(button, "(${property.valueContainer.value().count { it.value }})") button.setOnClickListener {_ -> val builder = AlertDialog.Builder(viewModel.context) builder.setTitle(context.translation.get(property.nameKey)) - val sortedStates = property.valueContainer.states.toSortedMap() + val sortedStates = property.valueContainer.value().toSortedMap() builder.setMultiChoiceItems( sortedStates.toSortedMap().map { context.translation.get("option." + property.nameKey + "." +it.key) }.toTypedArray(), sortedStates.map { it.value }.toBooleanArray() ) { _, which, isChecked -> sortedStates.keys.toList()[which].let { key -> - property.valueContainer.states[key] = isChecked + property.valueContainer.setKey(key, isChecked) } } builder.setPositiveButton("OK") { _, _ -> - updateStateSelectionText(button, property.valueContainer.toString()) + updateButtonText(button, "(${property.valueContainer.value().count { it.value }})") } builder.show() @@ -191,15 +198,15 @@ class SettingsMenu : AbstractMenu() { it.key.category }.forEach { (category, value) -> addView(createCategoryTitle(viewModel, category.key)) - value.forEach { - addView(createPropertyView(viewModel, it.key)) - actions.find { pair -> pair.first.dependsOnProperty == it.key }?.let { pair -> + value.filter { it.key.shouldAppearInSettings }.forEach { (property, _) -> + addView(createPropertyView(viewModel, property)) + actions.find { pair -> pair.first.dependsOnProperty == property}?.let { pair -> addView(pair.second()) } } } - addView(createCategoryTitle(viewModel, "category.debugging")) + //addView(createCategoryTitle(viewModel, "category.debugging")) actions.filter { it.first.dependsOnProperty == null }.forEach { addView(it.second()) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt @@ -41,14 +41,14 @@ class ConfigManager( JsonObject::class.java ) entries().forEach { (key, value) -> - value.read(configObject.get(key.name)?.asString ?: value.write()) + value.writeFrom(configObject.get(key.name)?.asString ?: value.read()) } } fun writeConfig() { val configObject = JsonObject() entries().forEach { (key, value) -> - configObject.addProperty(key.name, value.write()) + configObject.addProperty(key.name, value.read()) } context.bridgeClient.writeFile( BridgeFileType.CONFIG, 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 @@ -10,15 +10,15 @@ import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.features.impl.experiments.AppPasscode import me.rhunk.snapenhance.features.impl.experiments.MeoPasscodeBypass -import me.rhunk.snapenhance.features.impl.extras.AntiAutoSave -import me.rhunk.snapenhance.features.impl.extras.AutoSave -import me.rhunk.snapenhance.features.impl.extras.DisableVideoLengthRestriction -import me.rhunk.snapenhance.features.impl.extras.GalleryMediaSendOverride -import me.rhunk.snapenhance.features.impl.extras.LocationSpoofer -import me.rhunk.snapenhance.features.impl.extras.MediaQualityLevelOverride -import me.rhunk.snapenhance.features.impl.extras.Notifications -import me.rhunk.snapenhance.features.impl.extras.SnapchatPlus -import me.rhunk.snapenhance.features.impl.extras.UnlimitedSnapViewTime +import me.rhunk.snapenhance.features.impl.tweaks.AntiAutoSave +import me.rhunk.snapenhance.features.impl.tweaks.AutoSave +import me.rhunk.snapenhance.features.impl.tweaks.DisableVideoLengthRestriction +import me.rhunk.snapenhance.features.impl.tweaks.GalleryMediaSendOverride +import me.rhunk.snapenhance.features.impl.tweaks.LocationSpoofer +import me.rhunk.snapenhance.features.impl.tweaks.MediaQualityLevelOverride +import me.rhunk.snapenhance.features.impl.tweaks.Notifications +import me.rhunk.snapenhance.features.impl.tweaks.SnapchatPlus +import me.rhunk.snapenhance.features.impl.tweaks.UnlimitedSnapViewTime import me.rhunk.snapenhance.features.impl.privacy.DisableMetrics import me.rhunk.snapenhance.features.impl.privacy.PreventMessageSending import me.rhunk.snapenhance.features.impl.spying.AnonymousStoryViewing diff --git a/app/src/main/res/layout/map.xml b/app/src/main/res/layout/map.xml @@ -11,15 +11,34 @@ </org.osmdroid.views.MapView> - <Button - android:id="@+id/apply_location_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="bottom|right" - android:layout_marginEnd="20dp" - android:layout_marginBottom="20dp" - android:textSize="20sp" - android:background="@android:color/white" - android:text="Apply" - tools:ignore="HardcodedText,RtlHardcoded" /> + <FrameLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <Button + android:id="@+id/set_precise_location_button" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="left" + android:layout_marginStart="20dp" + android:layout_marginTop="20dp" + android:padding="10dp" + android:background="@android:color/white" + android:text="Set Precise Location" + android:textSize="20sp" + tools:ignore="HardcodedText,RtlHardcoded" /> + + <Button + android:id="@+id/apply_location_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:layout_marginTop="20dp" + android:layout_marginRight="20dp" + android:background="@android:color/white" + android:text="Apply" + android:textSize="20sp" + tools:ignore="HardcodedText,RtlHardcoded" /> + </FrameLayout> + </FrameLayout> diff --git a/app/src/main/res/layout/precise_location_dialog.xml b/app/src/main/res/layout/precise_location_dialog.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="20dp" + android:orientation="vertical" + tools:ignore="HardcodedText"> + + <EditText + android:id="@+id/dialog_latitude" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:inputType="numberDecimal" + android:hint="Latitude" + android:autofillHints="" /> + + <EditText + android:id="@+id/dialog_longitude" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:inputType="numberDecimal" + android:hint="Longitude" + android:autofillHints="" /> + +</LinearLayout>+ \ No newline at end of file