From 065068666783d9df699b0b8203f93b70dcc3b4d4 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 15 May 2023 00:37:29 +0200 Subject: [PATCH] initial commit --- .gitignore | 10 + README.md | 28 ++ app/.gitignore | 16 + app/build.gradle | 74 +++ app/libs/LSPosed-api-1.0-SNAPSHOT-javadoc.jar | Bin 0 -> 114810 bytes app/libs/LSPosed-api-1.0-SNAPSHOT-sources.jar | Bin 0 -> 18359 bytes app/libs/LSPosed-api-1.0-SNAPSHOT.jar | Bin 0 -> 21561 bytes app/proguard-rules.pro | 21 + app/src/main/AndroidManifest.xml | 47 ++ app/src/main/assets/xposed_init | 1 + .../me/rhunk/snapenhance/XposedLoader.java | 14 + .../kotlin/me/rhunk/snapenhance/Constants.kt | 21 + .../kotlin/me/rhunk/snapenhance/Logger.kt | 42 ++ .../kotlin/me/rhunk/snapenhance/ModContext.kt | 96 ++++ .../me/rhunk/snapenhance/SnapEnhance.kt | 65 +++ .../snapenhance/bridge/client/BridgeClient.kt | 238 ++++++++++ .../bridge/common/BridgeMessage.kt | 16 + .../bridge/common/BridgeMessageType.kt | 22 + .../common/impl/DownloadContentRequest.kt | 20 + .../common/impl/DownloadContentResult.kt | 17 + .../bridge/common/impl/FileAccessRequest.kt | 43 ++ .../bridge/common/impl/FileAccessResult.kt | 20 + .../bridge/common/impl/LocaleRequest.kt | 17 + .../bridge/common/impl/LocaleResult.kt | 19 + .../common/impl/MessageLoggerRequest.kt | 29 ++ .../bridge/common/impl/MessageLoggerResult.kt | 20 + .../bridge/service/BridgeService.kt | 180 ++++++++ .../bridge/service/MainActivity.kt | 20 + .../snapenhance/config/ConfigAccessor.kt | 58 +++ .../snapenhance/config/ConfigCategory.kt | 14 + .../snapenhance/config/ConfigProperty.kt | 181 ++++++++ .../me/rhunk/snapenhance/data/FileType.kt | 50 ++ .../rhunk/snapenhance/data/SnapClassCache.kt | 25 + .../me/rhunk/snapenhance/data/SnapEnums.kt | 41 ++ .../data/wrapper/AbstractWrapper.kt | 29 ++ .../snapenhance/data/wrapper/impl/Message.kt | 12 + .../data/wrapper/impl/MessageContent.kt | 15 + .../data/wrapper/impl/MessageDescriptor.kt | 9 + .../data/wrapper/impl/MessageMetadata.kt | 16 + .../snapenhance/data/wrapper/impl/SnapUUID.kt | 40 ++ .../wrapper/impl/media/EncryptionWrapper.kt | 73 +++ .../data/wrapper/impl/media/MediaInfo.kt | 34 ++ .../data/wrapper/impl/media/opera/Layer.kt | 21 + .../impl/media/opera/LayerController.kt | 18 + .../data/wrapper/impl/media/opera/ParamMap.kt | 37 ++ .../snapenhance/database/DatabaseAccess.kt | 199 ++++++++ .../snapenhance/database/DatabaseObject.kt | 7 + .../database/objects/ConversationMessage.kt | 44 ++ .../database/objects/FriendFeedInfo.kt | 33 ++ .../database/objects/FriendInfo.kt | 58 +++ .../database/objects/StoryEntry.kt | 23 + .../database/objects/UserConversationLink.kt | 19 + .../me/rhunk/snapenhance/event/EventBus.kt | 62 +++ .../me/rhunk/snapenhance/event/Events.kt | 3 + .../me/rhunk/snapenhance/features/Feature.kt | 31 ++ .../snapenhance/features/FeatureLoadParams.kt | 11 + .../features/impl/ConfigEnumKeys.kt | 67 +++ .../snapenhance/features/impl/Messaging.kt | 71 +++ .../impl/downloader/MediaDownloader.kt | 430 ++++++++++++++++++ .../features/impl/extras/AutoSave.kt | 133 ++++++ .../features/impl/extras/Notifications.kt | 183 ++++++++ .../features/impl/extras/SnapchatPlus.kt | 28 ++ .../features/impl/privacy/DisableMetrics.kt | 45 ++ .../privacy/PreventScreenshotDetections.kt | 16 + .../impl/spy/AnonymousStoryViewing.kt | 20 + .../features/impl/spy/MessageLogger.kt | 64 +++ .../features/impl/spy/PreventReadReceipts.kt | 28 ++ .../impl/spy/PreventStatusNotifications.kt | 28 ++ .../features/impl/spy/StealthMode.kt | 62 +++ .../snapenhance/features/impl/ui/UITweaks.kt | 61 +++ .../features/impl/ui/menus/AbstractMenu.kt | 9 + .../impl/ui/menus/MenuViewInjector.kt | 139 ++++++ .../impl/ui/menus/ViewAppearanceHelper.kt | 53 +++ .../impl/ui/menus/impl/ChatActionMenu.kt | 91 ++++ .../impl/ui/menus/impl/FriendFeedInfoMenu.kt | 231 ++++++++++ .../ui/menus/impl/OperaContextActionMenu.kt | 82 ++++ .../impl/ui/menus/impl/SettingsMenu.kt | 133 ++++++ .../me/rhunk/snapenhance/hook/HookAdapter.kt | 72 +++ .../me/rhunk/snapenhance/hook/HookStage.kt | 6 + .../me/rhunk/snapenhance/hook/Hooker.kt | 94 ++++ .../me/rhunk/snapenhance/manager/Manager.kt | 6 + .../snapenhance/manager/impl/ConfigManager.kt | 63 +++ .../manager/impl/FeatureManager.kt | 93 ++++ .../manager/impl/MappingManager.kt | 178 ++++++++ .../manager/impl/TranslationManager.kt | 19 + .../me/rhunk/snapenhance/mapping/Mapper.kt | 13 + .../mapping/impl/CallbackMapper.kt | 29 ++ .../snapenhance/mapping/impl/EnumMapper.kt | 41 ++ .../impl/OperaPageViewControllerMapper.kt | 77 ++++ .../mapping/impl/PlusSubscriptionMapper.kt | 32 ++ .../rhunk/snapenhance/util/CallbackBuilder.kt | 93 ++++ .../snapenhance/util/EncryptionHelper.kt | 67 +++ .../rhunk/snapenhance/util/PreviewCreator.kt | 41 ++ .../snapenhance/util/ReflectionHelper.kt | 118 +++++ .../snapenhance/util/XposedHelperMacros.kt | 20 + .../util/download/CdnDownloader.kt | 83 ++++ .../util/download/DownloadServer.kt | 116 +++++ .../snapenhance/util/protobuf/ProtoReader.kt | 122 +++++ .../snapenhance/util/protobuf/ProtoWriter.kt | 66 +++ .../rhunk/snapenhance/util/snap/SnapUUID.kt | 2 + app/src/main/res/values-fr/strings.xml | 4 + app/src/main/res/values/arrays.xml | 6 + app/src/main/res/values/strings.xml | 33 ++ build.gradle | 10 + gradle.properties | 23 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++++ gradlew.bat | 89 ++++ settings.gradle | 16 + 110 files changed, 6056 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/libs/LSPosed-api-1.0-SNAPSHOT-javadoc.jar create mode 100644 app/libs/LSPosed-api-1.0-SNAPSHOT-sources.jar create mode 100644 app/libs/LSPosed-api-1.0-SNAPSHOT.jar create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/xposed_init create mode 100644 app/src/main/java/me/rhunk/snapenhance/XposedLoader.java create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/Constants.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/Logger.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/BridgeClient.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessage.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessageType.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/DownloadContentRequest.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/DownloadContentResult.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/FileAccessRequest.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/FileAccessResult.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/LocaleRequest.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/LocaleResult.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/MessageLoggerRequest.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/MessageLoggerResult.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/MainActivity.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigAccessor.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/EncryptionWrapper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/event/EventBus.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/event/Events.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/AutoSave.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/SnapchatPlus.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventScreenshotDetections.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/AnonymousStoryViewing.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/MessageLogger.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventReadReceipts.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventStatusNotifications.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/StealthMode.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/AbstractMenu.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/MenuViewInjector.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/ViewAppearanceHelper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/ChatActionMenu.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/FriendFeedInfoMenu.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/OperaContextActionMenu.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/SettingsMenu.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/hook/HookStage.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/manager/Manager.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/TranslationManager.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/Mapper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/CallbackMapper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/OperaPageViewControllerMapper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlusSubscriptionMapper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/EncryptionHelper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/PreviewCreator.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperMacros.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/download/CdnDownloader.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapUUID.kt create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..faf530b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/README.md b/README.md new file mode 100644 index 00000000..e32d46ae --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# SnapEnhance +A xposed mod to enhance the Snapchat experience +The project is currently in development, so expect bugs and crashes. Feel free to open an issue if you find any bug. + +## build + 1. make sure you have the latest version of [LSPosed](https://github.com/LSPosed/LSPosed) + 2. clone this repo using ``git clone`` + 3. run ``./gradlew assembleDebug`` + 4. install the apk using adb ``adb install -r app/build/outputs/apk/debug/app-debug.apk`` + +## features +- media downloader (+ overlay merging) +- message auto save +- message in notifications +- message logger +- snapchat plus features +- anonymous story viewing +- stealth mode +- screenshot detection bypass +- conversation preview +- prevent status notifications +- UI tweaks (remove call button, record button, ...) +- ad blocker + +## todo +- [] localization +- [] ui improvements +- [] snap splitting diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..c9db3d0b --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..ac941a87 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,74 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +def appVersionName = "0.0.1" +def appVersionCode = 1 + +android { + compileSdk 32 + + defaultConfig { + applicationId "me.rhunk.snapenhance" + minSdk 29 + targetSdk 32 + versionCode appVersionCode + versionName appVersionName + multiDexEnabled true + } + + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + //keep arm64-v8a native libs + packagingOptions { + exclude "META-INF/**" + exclude 'lib/x86/**' + exclude 'lib/x86_64/**' + exclude 'lib/armeabi-v7a/**' + } + + kotlinOptions { + jvmTarget = '1.8' + } +} + +afterEvaluate { + //auto install for debug purpose + getTasks().getByPath(":app:assembleDebug").doLast { + try { + println "Killing Snapchat" + exec { + commandLine "adb", "shell", "am", "force-stop", "com.snapchat.android" + } + println "Installing debug build" + exec() { + commandLine "adb", "install", "-r", "-d", "${buildDir}/outputs/apk/debug/app-debug.apk" + } + println "Starting Snapchat" + exec { + commandLine "adb", "shell", "am", "start", "com.snapchat.android" + } + } catch (Throwable t) { + println "Failed to install debug build" + t.printStackTrace() + } + } +} + +dependencies { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' + compileOnly files('libs/LSPosed-api-1.0-SNAPSHOT.jar') + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.arthenica:ffmpeg-kit-min-gpl:5.1' +} \ No newline at end of file diff --git a/app/libs/LSPosed-api-1.0-SNAPSHOT-javadoc.jar b/app/libs/LSPosed-api-1.0-SNAPSHOT-javadoc.jar new file mode 100644 index 0000000000000000000000000000000000000000..139c471788f6385d33405fb61d0325efd0331551 GIT binary patch literal 114810 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

6s+Vz@ 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∋mzobng=h@{9GH3Y?=}hLpI%_xXsOnV9E)aZec;J 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 literal 0 HcmV?d00001 diff --git a/app/libs/LSPosed-api-1.0-SNAPSHOT-sources.jar b/app/libs/LSPosed-api-1.0-SNAPSHOT-sources.jar new file mode 100644 index 0000000000000000000000000000000000000000..f77b0ffd23ed58be59c656c005e390b9b5bfe7d1 GIT binary patch literal 18359 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 WumacEFfb?qVK=aWG6Qr*BLe`iVg!@` literal 0 HcmV?d00001 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..236adf1b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init new file mode 100644 index 00000000..6f814117 --- /dev/null +++ b/app/src/main/assets/xposed_init @@ -0,0 +1 @@ +me.rhunk.snapenhance.XposedLoader \ No newline at end of file diff --git a/app/src/main/java/me/rhunk/snapenhance/XposedLoader.java b/app/src/main/java/me/rhunk/snapenhance/XposedLoader.java new file mode 100644 index 00000000..492e70a3 --- /dev/null +++ b/app/src/main/java/me/rhunk/snapenhance/XposedLoader.java @@ -0,0 +1,14 @@ +package me.rhunk.snapenhance; + +import de.robv.android.xposed.IXposedHookLoadPackage; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +public class XposedLoader implements IXposedHookLoadPackage { + @Override + public void handleLoadPackage(XC_LoadPackage.LoadPackageParam packageParam) throws Throwable { + if (!packageParam.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return; + new SnapEnhance(); + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/Constants.kt b/app/src/main/kotlin/me/rhunk/snapenhance/Constants.kt new file mode 100644 index 00000000..b29297c5 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/Constants.kt @@ -0,0 +1,21 @@ +package me.rhunk.snapenhance + +object Constants { + const val TAG = "SnapEnhance" + const val SNAPCHAT_PACKAGE_NAME = "com.snapchat.android" + + const val VIEW_INJECTED_CODE = 0x7FFFFF02 + const val VIEW_DRAWER = 0x7FFFFF03 + + val ARROYO_NOTE_ENCRYPTION_PROTO_PATH = intArrayOf(4, 4, 6, 1, 1) + val ARROYO_SNAP_ENCRYPTION_PROTO_PATH = intArrayOf(4, 4, 11, 5, 1, 1) + val MESSAGE_SNAP_ENCRYPTION_PROTO_PATH = intArrayOf(11, 5, 1, 1) + val ARROYO_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH = intArrayOf(4, 4, 3, 3, 5, 1, 1) + val ARROYO_STRING_CHAT_MESSAGE_PROTO = intArrayOf(4, 4, 2, 1) + val ARROYO_URL_KEY_PROTO_PATH = intArrayOf(4, 5, 1, 3, 2, 2) + + const val ARROYO_ENCRYPTION_PROTO_INDEX = 19 + const val ARROYO_ENCRYPTION_PROTO_INDEX_V2 = 4 + + const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/Logger.kt b/app/src/main/kotlin/me/rhunk/snapenhance/Logger.kt new file mode 100644 index 00000000..9cc4b89d --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/Logger.kt @@ -0,0 +1,42 @@ +package me.rhunk.snapenhance + +import android.util.Log +import de.robv.android.xposed.XposedBridge + +object Logger { + private const val TAG = "SnapEnhance" + + fun log(message: Any?) { + Log.i(TAG, message.toString()) + } + + fun debug(message: Any?) { + if (!BuildConfig.DEBUG) return + Log.d(TAG, message.toString()) + } + + fun error(throwable: Throwable) { + Log.e(TAG, "",throwable) + } + + fun error(message: Any?) { + Log.e(TAG, message.toString()) + } + + fun error(message: Any?, throwable: Throwable) { + Log.e(TAG, message.toString(), throwable) + } + + fun xposedLog(message: Any?) { + XposedBridge.log(message.toString()) + } + + fun xposedLog(message: Any?, throwable: Throwable?) { + XposedBridge.log(message.toString()) + XposedBridge.log(throwable) + } + + fun xposedLog(throwable: Throwable) { + XposedBridge.log(throwable) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt new file mode 100644 index 00000000..7744688d --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -0,0 +1,96 @@ +package me.rhunk.snapenhance + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.os.Handler +import android.os.Looper +import android.os.Process +import android.widget.Toast +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import me.rhunk.snapenhance.bridge.client.BridgeClient +import me.rhunk.snapenhance.database.DatabaseAccess +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.manager.impl.ConfigManager +import me.rhunk.snapenhance.manager.impl.FeatureManager +import me.rhunk.snapenhance.manager.impl.MappingManager +import me.rhunk.snapenhance.manager.impl.TranslationManager +import me.rhunk.snapenhance.util.download.DownloadServer +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.reflect.KClass +import kotlin.system.exitProcess + +class ModContext { + private val executorService: ExecutorService = Executors.newCachedThreadPool() + + lateinit var androidContext: Context + var mainActivity: Activity? = null + + val gson: Gson = GsonBuilder().create() + + val bridgeClient = BridgeClient(this) + val translation = TranslationManager(this) + val features = FeatureManager(this) + val mappings = MappingManager(this) + val config = ConfigManager(this) + val database = DatabaseAccess(this) + val downloadServer = DownloadServer(this) + val classCache get() = SnapEnhance.classCache + val resources: Resources get() = androidContext.resources + + fun feature(featureClass: KClass): T { + return features.get(featureClass)!! + } + + fun runOnUiThread(runnable: () -> Unit) { + Handler(Looper.getMainLooper()).post { + runCatching(runnable).onFailure { + Logger.xposedLog("UI thread runnable failed", it) + } + } + } + + fun executeAsync(runnable: () -> Unit) { + executorService.submit { + runCatching { + runnable() + }.onFailure { + Logger.xposedLog("Async task failed", it) + } + } + } + + fun shortToast(message: Any) { + runOnUiThread { + Toast.makeText(androidContext, message.toString(), Toast.LENGTH_SHORT).show() + } + } + + fun longToast(message: Any) { + runOnUiThread { + Toast.makeText(androidContext, message.toString(), Toast.LENGTH_LONG).show() + } + } + + fun restartApp() { + androidContext.packageManager.getLaunchIntentForPackage( + Constants.SNAPCHAT_PACKAGE_NAME + )?.let { + val intent = Intent.makeRestartActivityTask(it.component) + androidContext.startActivity(intent) + Runtime.getRuntime().exit(0) + } + } + + fun softRestartApp() { + exitProcess(0) + } + + fun forceCloseApp() { + Process.killProcess(Process.myPid()) + exitProcess(1) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt new file mode 100644 index 00000000..d235f2b2 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -0,0 +1,65 @@ +package me.rhunk.snapenhance + +import android.app.Activity +import android.app.Application +import android.content.Context +import me.rhunk.snapenhance.data.SnapClassCache +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker + +class SnapEnhance { + companion object { + lateinit var classLoader: ClassLoader + val classCache: SnapClassCache by lazy { + SnapClassCache(classLoader) + } + } + private val appContext = ModContext() + + init { + Hooker.hook(Application::class.java, "attach", HookStage.BEFORE) { param -> + appContext.androidContext = param.arg(0).also { + classLoader = it.classLoader + } + + appContext.bridgeClient.start { bridgeResult -> + if (!bridgeResult) { + Logger.xposedLog("Cannot connect to bridge service") + appContext.restartApp() + return@start + } + runCatching { + init() + }.onFailure { + Logger.xposedLog("Failed to initialize", it) + } + } + } + + Hooker.hook(Activity::class.java, "onCreate", HookStage.AFTER) { + val activity = it.thisObject() as Activity + if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook + val isMainActivityNotNull = appContext.mainActivity != null + appContext.mainActivity = activity + if (isMainActivityNotNull) return@hook + onActivityCreate() + } + } + + private fun init() { + val time = System.currentTimeMillis() + with(appContext) { + translation.init() + config.init() + mappings.init() + features.init() + } + Logger.debug("initialized in ${System.currentTimeMillis() - time} ms") + } + + private fun onActivityCreate() { + with(appContext) { + features.onActivityCreate() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/BridgeClient.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/BridgeClient.kt new file mode 100644 index 00000000..ef13f8b9 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/BridgeClient.kt @@ -0,0 +1,238 @@ +package me.rhunk.snapenhance.bridge.client + + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.* +import me.rhunk.snapenhance.BuildConfig +import me.rhunk.snapenhance.Logger.log +import me.rhunk.snapenhance.Logger.xposedLog +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.bridge.common.BridgeMessage +import me.rhunk.snapenhance.bridge.common.BridgeMessageType +import me.rhunk.snapenhance.bridge.common.impl.* +import me.rhunk.snapenhance.bridge.service.BridgeService +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import kotlin.reflect.KClass +import kotlin.system.exitProcess + + +class BridgeClient( + private val context: ModContext +) : ServiceConnection { + private val handlerThread = HandlerThread("BridgeClient") + + private lateinit var messenger: Messenger + private lateinit var future: CompletableFuture + + fun start(callback: (Boolean) -> Unit = {}) { + this.future = CompletableFuture() + this.handlerThread.start() + + with(context.androidContext) { + val intent = Intent() + .setClassName(BuildConfig.APPLICATION_ID, BridgeService::class.java.name) + bindService( + intent, + Context.BIND_AUTO_CREATE, + Executors.newSingleThreadExecutor(), + this@BridgeClient + ) + } + callback(future.get()) + } + + private fun handleResponseMessage( + msg: Message, + future: CompletableFuture + ) { + val message: BridgeMessage = when (BridgeMessageType.fromValue(msg.what)) { + BridgeMessageType.FILE_ACCESS_RESULT -> FileAccessResult() + BridgeMessageType.DOWNLOAD_CONTENT_RESULT -> DownloadContentResult() + BridgeMessageType.MESSAGE_LOGGER_RESULT -> MessageLoggerResult() + else -> { + log("Unknown message type: ${msg.what}") + null + } + } ?: return + + with(message) { + read(msg.data) + future.complete(this) + } + } + + @Suppress("UNCHECKED_CAST", "UNUSED_PARAMETER") + private fun sendMessage( + messageType: BridgeMessageType, + message: BridgeMessage, + resultType: KClass? = null + ): T { + val future = CompletableFuture() + + val replyMessenger = Messenger(object : Handler(handlerThread.looper) { + override fun handleMessage(msg: Message) { + handleResponseMessage(msg, future) + } + }) + + runCatching { + with(Message.obtain()) { + what = messageType.value + replyTo = replyMessenger + data = Bundle() + message.write(data) + messenger.send(this) + } + } + + return future.get() as T + } + + /** + * Create a file if it doesn't exist, and read it + * + * @param fileType the type of file to create and read + * @param defaultContent the default content to write to the file if it doesn't exist + * @return the content of the file + */ + fun createAndReadFile( + fileType: FileAccessRequest.FileType, + defaultContent: ByteArray + ): ByteArray { + sendMessage( + BridgeMessageType.FILE_ACCESS_REQUEST, + FileAccessRequest(FileAccessRequest.FileAccessAction.EXISTS, fileType, null), + FileAccessResult::class + ).run { + if (state!!) { + return readFile(fileType) + } + writeFile(fileType, defaultContent) + return defaultContent + } + } + + /** + * Read a file + * + * @param fileType the type of file to read + * @return the content of the file + */ + fun readFile(fileType: FileAccessRequest.FileType): ByteArray { + sendMessage( + BridgeMessageType.FILE_ACCESS_REQUEST, + FileAccessRequest(FileAccessRequest.FileAccessAction.READ, fileType, null), + FileAccessResult::class + ).run { + return content!! + } + } + + /** + * Write a file + * + * @param fileType the type of file to write + * @param content the content to write to the file + * @return true if the file was written successfully + */ + fun writeFile( + fileType: FileAccessRequest.FileType, + content: ByteArray? + ): Boolean { + sendMessage( + BridgeMessageType.FILE_ACCESS_REQUEST, + FileAccessRequest(FileAccessRequest.FileAccessAction.WRITE, fileType, content), + FileAccessResult::class + ).run { + return state!! + } + } + + /** + * Delete a file + * + * @param fileType the type of file to delete + * @return true if the file was deleted successfully + */ + fun deleteFile(fileType: FileAccessRequest.FileType): Boolean { + sendMessage( + BridgeMessageType.FILE_ACCESS_REQUEST, + FileAccessRequest(FileAccessRequest.FileAccessAction.DELETE, fileType, null), + FileAccessResult::class + ).run { + return state!! + } + } + + /** + * Check if a file exists + * + * @param fileType the type of file to check + * @return true if the file exists + */ + + fun isFileExists(fileType: FileAccessRequest.FileType): Boolean { + sendMessage( + BridgeMessageType.FILE_ACCESS_REQUEST, + FileAccessRequest(FileAccessRequest.FileAccessAction.EXISTS, fileType, null), + FileAccessResult::class + ).run { + return state!! + } + } + + /** + * Download content from a URL and save it to a file + * + * @param url the URL to download content from + * @param path the path to save the content to + * @return true if the content was downloaded successfully + */ + fun downloadContent(url: String, path: String): Boolean { + sendMessage( + BridgeMessageType.DOWNLOAD_CONTENT_REQUEST, + DownloadContentRequest(url, path), + DownloadContentResult::class + ).run { + return state!! + } + } + + fun getMessageLoggerMessage(id: Long): ByteArray? { + sendMessage( + BridgeMessageType.MESSAGE_LOGGER_REQUEST, + MessageLoggerRequest(MessageLoggerRequest.Action.GET, id), + MessageLoggerResult::class + ).run { + return message + } + } + + fun addMessageLoggerMessage(id: Long, message: ByteArray) { + sendMessage( + BridgeMessageType.MESSAGE_LOGGER_REQUEST, + MessageLoggerRequest(MessageLoggerRequest.Action.ADD, id, message), + MessageLoggerResult::class + ) + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + messenger = Messenger(service) + future.complete(true) + } + + override fun onNullBinding(name: ComponentName) { + xposedLog("failed to connect to bridge service") + future.complete(false) + } + + override fun onServiceDisconnected(name: ComponentName) { + context.longToast("Bridge service disconnected") + Thread.sleep(1000) + exitProcess(0) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessage.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessage.kt new file mode 100644 index 00000000..e12e59d7 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessage.kt @@ -0,0 +1,16 @@ +package me.rhunk.snapenhance.bridge.common + +import android.os.Bundle +import android.os.Message + +abstract class BridgeMessage { + abstract fun write(bundle: Bundle) + abstract fun read(bundle: Bundle) + + fun toMessage(what: Int): Message { + val message = Message.obtain(null, what) + message.data = Bundle() + write(message.data) + return message + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessageType.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessageType.kt new file mode 100644 index 00000000..496123ae --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessageType.kt @@ -0,0 +1,22 @@ +package me.rhunk.snapenhance.bridge.common + + +enum class BridgeMessageType( + val value: Int = 0 +) { + UNKNOWN(-1), + FILE_ACCESS_REQUEST(0), + FILE_ACCESS_RESULT(1), + DOWNLOAD_CONTENT_REQUEST(2), + DOWNLOAD_CONTENT_RESULT(3), + LOCALE_REQUEST(4), + LOCALE_RESULT(5), + MESSAGE_LOGGER_REQUEST(6), + MESSAGE_LOGGER_RESULT(7); + + companion object { + fun fromValue(value: Int): BridgeMessageType { + return values().firstOrNull { it.value == value } ?: UNKNOWN + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/DownloadContentRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/DownloadContentRequest.kt new file mode 100644 index 00000000..7c8f9c53 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/DownloadContentRequest.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.bridge.common.impl + +import android.os.Bundle +import me.rhunk.snapenhance.bridge.common.BridgeMessage + +class DownloadContentRequest( + var url: String? = null, + var path: String? = null +) : BridgeMessage() { + + override fun write(bundle: Bundle) { + bundle.putString("url", url) + bundle.putString("path", path) + } + + override fun read(bundle: Bundle) { + url = bundle.getString("url") + path = bundle.getString("path") + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/DownloadContentResult.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/DownloadContentResult.kt new file mode 100644 index 00000000..ef7f33f5 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/DownloadContentResult.kt @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.bridge.common.impl + +import android.os.Bundle +import me.rhunk.snapenhance.bridge.common.BridgeMessage + +class DownloadContentResult( + var state: Boolean? = null +) : BridgeMessage() { + + override fun write(bundle: Bundle) { + bundle.putBoolean("state", state!!) + } + + override fun read(bundle: Bundle) { + state = bundle.getBoolean("state") + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/FileAccessRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/FileAccessRequest.kt new file mode 100644 index 00000000..5ba5b83d --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/FileAccessRequest.kt @@ -0,0 +1,43 @@ +package me.rhunk.snapenhance.bridge.common.impl + +import android.os.Bundle +import me.rhunk.snapenhance.bridge.common.BridgeMessage + +class FileAccessRequest( + var action: FileAccessAction? = null, + var fileType: FileType? = null, + var content: ByteArray? = null +) : BridgeMessage() { + + override fun write(bundle: Bundle) { + bundle.putInt("action", action!!.value) + bundle.putInt("fileType", fileType!!.value) + bundle.putByteArray("content", content) + } + + override fun read(bundle: Bundle) { + action = FileAccessAction.fromValue(bundle.getInt("action")) + fileType = FileType.fromValue(bundle.getInt("fileType")) + content = bundle.getByteArray("content") + } + + enum class FileType(val value: Int) { + CONFIG(0), MAPPINGS(1), STEALTH(2); + + companion object { + fun fromValue(value: Int): FileType? { + return values().firstOrNull { it.value == value } + } + } + } + + enum class FileAccessAction(val value: Int) { + READ(0), WRITE(1), DELETE(2), EXISTS(3); + + companion object { + fun fromValue(value: Int): FileAccessAction? { + return values().firstOrNull { it.value == value } + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/FileAccessResult.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/FileAccessResult.kt new file mode 100644 index 00000000..ff616a40 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/FileAccessResult.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.bridge.common.impl + +import android.os.Bundle +import me.rhunk.snapenhance.bridge.common.BridgeMessage + +class FileAccessResult( + var state: Boolean? = null, + var content: ByteArray? = null +) : BridgeMessage() { + + override fun write(bundle: Bundle) { + bundle.putBoolean("state", state!!) + bundle.putByteArray("content", content) + } + + override fun read(bundle: Bundle) { + state = bundle.getBoolean("state") + content = bundle.getByteArray("content") + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/LocaleRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/LocaleRequest.kt new file mode 100644 index 00000000..da2a1bc0 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/LocaleRequest.kt @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.bridge.common.impl + +import android.os.Bundle +import me.rhunk.snapenhance.bridge.common.BridgeMessage + +class LocaleRequest( + var locale: String? = null +) : BridgeMessage() { + + override fun write(bundle: Bundle) { + bundle.putString("locale", locale) + } + + override fun read(bundle: Bundle) { + locale = bundle.getString("locale") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/LocaleResult.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/LocaleResult.kt new file mode 100644 index 00000000..0e1fa191 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/LocaleResult.kt @@ -0,0 +1,19 @@ +package me.rhunk.snapenhance.bridge.common.impl + +import android.os.Bundle +import me.rhunk.snapenhance.bridge.common.BridgeMessage + +class LocaleResult( + var locale: String? = null, + var content: ByteArray? = null +) : BridgeMessage(){ + override fun write(bundle: Bundle) { + bundle.putString("locale", locale) + bundle.putByteArray("content", content) + } + + override fun read(bundle: Bundle) { + locale = bundle.getString("locale") + content = bundle.getByteArray("content") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/MessageLoggerRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/MessageLoggerRequest.kt new file mode 100644 index 00000000..3040af58 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/MessageLoggerRequest.kt @@ -0,0 +1,29 @@ +package me.rhunk.snapenhance.bridge.common.impl + +import android.os.Bundle +import me.rhunk.snapenhance.bridge.common.BridgeMessage + +class MessageLoggerRequest( + var action: Action? = null, + var messageId: Long? = null, + var message: ByteArray? = null +) : BridgeMessage(){ + + override fun write(bundle: Bundle) { + bundle.putString("action", action!!.name) + bundle.putLong("messageId", messageId!!) + bundle.putByteArray("message", message) + } + + override fun read(bundle: Bundle) { + action = Action.valueOf(bundle.getString("action")!!) + messageId = bundle.getLong("messageId") + message = bundle.getByteArray("message") + } + + enum class Action { + ADD, + GET, + CLEAR + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/MessageLoggerResult.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/MessageLoggerResult.kt new file mode 100644 index 00000000..b904c2a6 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/MessageLoggerResult.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.bridge.common.impl + +import android.os.Bundle +import me.rhunk.snapenhance.bridge.common.BridgeMessage + +class MessageLoggerResult( + var state: Boolean? = null, + var message: ByteArray? = null +) : BridgeMessage() { + + override fun write(bundle: Bundle) { + bundle.putBoolean("state", state!!) + bundle.putByteArray("message", message) + } + + override fun read(bundle: Bundle) { + state = bundle.getBoolean("state") + message = bundle.getByteArray("message") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt new file mode 100644 index 00000000..5f32a424 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt @@ -0,0 +1,180 @@ +package me.rhunk.snapenhance.bridge.service + +import android.app.DownloadManager +import android.app.Service +import android.content.* +import android.database.sqlite.SQLiteDatabase +import android.net.Uri +import android.os.* +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.bridge.common.BridgeMessageType +import me.rhunk.snapenhance.bridge.common.impl.* +import java.io.File + +class BridgeService : Service() { + companion object { + const val CONFIG_FILE = "config.json" + const val MAPPINGS_FILE = "mappings.json" + const val STEALTH_FILE = "stealth.txt" + const val MESSAGE_LOGGER_DATABASE = "message_logger" + } + + lateinit var messageLoggerDatabase: SQLiteDatabase + + override fun onBind(intent: Intent): IBinder { + with(openOrCreateDatabase(MESSAGE_LOGGER_DATABASE, Context.MODE_PRIVATE, null)) { + messageLoggerDatabase = this + execSQL("CREATE TABLE IF NOT EXISTS messages (message_id INTEGER PRIMARY KEY, serialized_message BLOB)") + } + + return Messenger(object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + runCatching { + this@BridgeService.handleMessage(msg) + }.onFailure { + Logger.error("Failed to handle message", it) + } + } + }).binder + } + + + private fun handleMessage(msg: Message) { + val replyMessenger = msg.replyTo + when (BridgeMessageType.fromValue(msg.what)) { + BridgeMessageType.FILE_ACCESS_REQUEST -> { + with(FileAccessRequest()) { + read(msg.data) + handleFileAccess(this) { message -> + replyMessenger.send(message) + } + } + } + BridgeMessageType.DOWNLOAD_CONTENT_REQUEST -> { + with(DownloadContentRequest()) { + read(msg.data) + handleDownloadContent(this) { message -> + replyMessenger.send(message) + } + } + } + BridgeMessageType.LOCALE_REQUEST -> { + with(LocaleRequest()) { + read(msg.data) + handleLocaleRequest(this) { message -> + replyMessenger.send(message) + } + } + } + BridgeMessageType.MESSAGE_LOGGER_REQUEST -> { + with(MessageLoggerRequest()) { + read(msg.data) + handleMessageLoggerRequest(this) { message -> + replyMessenger.send(message) + } + } + } + + else -> Logger.error("Unknown message type: " + msg.what) + } + } + + private fun handleMessageLoggerRequest(msg: MessageLoggerRequest, reply: (Message) -> Unit) { + when (msg.action) { + MessageLoggerRequest.Action.ADD -> { + messageLoggerDatabase.insert("messages", null, ContentValues().apply { + put("message_id", msg.messageId) + put("serialized_message", msg.message) + }) + } + MessageLoggerRequest.Action.CLEAR -> { + messageLoggerDatabase.execSQL("DELETE FROM messages") + } + MessageLoggerRequest.Action.GET -> { + val messageId = msg.messageId + val cursor = messageLoggerDatabase.rawQuery("SELECT serialized_message FROM messages WHERE message_id = ?", arrayOf(messageId.toString())) + val state = cursor.moveToFirst() + val message: ByteArray? = if (state) { + cursor.getBlob(0) + } else { + null + } + cursor.close() + reply(MessageLoggerResult(state, message).toMessage(BridgeMessageType.MESSAGE_LOGGER_RESULT.value)) + } + else -> { + Logger.error(Exception("Unknown message logger action: ${msg.action}")) + } + } + + reply(MessageLoggerResult(true).toMessage(BridgeMessageType.MESSAGE_LOGGER_RESULT.value)) + } + + private fun handleLocaleRequest(msg: LocaleRequest, reply: (Message) -> Unit) { + val locale = resources.configuration.locales[0] + Logger.log("Locale: ${locale.language}_${locale.country}") + TODO() + } + + private fun handleDownloadContent(msg: DownloadContentRequest, reply: (Message) -> Unit) { + if (!msg.url!!.startsWith("http://127.0.0.1:")) return + + val outputFile = File(msg.path!!) + outputFile.parentFile?.let { + if (!it.exists()) it.mkdirs() + } + val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager + val request = DownloadManager.Request(Uri.parse(msg.url)) + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) + .setAllowedOverMetered(true) + .setAllowedOverRoaming(true) + .setDestinationUri(Uri.fromFile(outputFile)) + val downloadId = downloadManager.enqueue(request) + registerReceiver(object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != downloadId) return + unregisterReceiver(this) + reply(DownloadContentResult(true).toMessage(BridgeMessageType.DOWNLOAD_CONTENT_RESULT.value)) + } + }, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) + } + + private fun handleFileAccess(msg: FileAccessRequest, reply: (Message) -> Unit) { + val file = when (msg.fileType) { + FileAccessRequest.FileType.CONFIG -> CONFIG_FILE + FileAccessRequest.FileType.MAPPINGS -> MAPPINGS_FILE + FileAccessRequest.FileType.STEALTH -> STEALTH_FILE + else -> throw Exception("Unknown file type: " + msg.fileType) + }.let { File(filesDir, it) } + + val result: FileAccessResult = when (msg.action) { + FileAccessRequest.FileAccessAction.READ -> { + if (!file.exists()) { + FileAccessResult(false, null) + } else { + FileAccessResult(true, file.readBytes()) + } + } + FileAccessRequest.FileAccessAction.WRITE -> { + if (!file.exists()) { + file.createNewFile() + } + file.writeBytes(msg.content!!) + FileAccessResult(true, null) + } + FileAccessRequest.FileAccessAction.DELETE -> { + if (!file.exists()) { + FileAccessResult(false, null) + } else { + file.delete() + FileAccessResult(true, null) + } + } + FileAccessRequest.FileAccessAction.EXISTS -> FileAccessResult(file.exists(), null) + else -> throw Exception("Unknown action: " + msg.action) + } + + reply(result.toMessage(BridgeMessageType.FILE_ACCESS_RESULT.value)) + } + +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/MainActivity.kt new file mode 100644 index 00000000..9725222b --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/MainActivity.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.bridge.service + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import me.rhunk.snapenhance.Constants + +class MainActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (intent.getBooleanExtra("is_from_bridge", false)) { + finish() + return + } + val intent = packageManager.getLaunchIntentForPackage(Constants.SNAPCHAT_PACKAGE_NAME) + intent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + finish() + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigAccessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigAccessor.kt new file mode 100644 index 00000000..4644aaf2 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigAccessor.kt @@ -0,0 +1,58 @@ +package me.rhunk.snapenhance.config + +open class ConfigAccessor( + private val configMap: MutableMap +) { + fun bool(key: ConfigProperty): Boolean { + return get(key) as Boolean + } + + fun int(key: ConfigProperty): Int { + return get(key) as Int + } + + fun string(key: ConfigProperty): String { + return get(key) as String + } + + fun double(key: ConfigProperty): Double { + return get(key) as Double + } + + fun float(key: ConfigProperty): Float { + return get(key) as Float + } + + fun long(key: ConfigProperty): Long { + return get(key) as Long + } + + fun short(key: ConfigProperty): Short { + return get(key) as Short + } + + fun byte(key: ConfigProperty): Byte { + return get(key) as Byte + } + + fun char(key: ConfigProperty): Char { + return get(key) as Char + } + + @Suppress("UNCHECKED_CAST") + fun list(key: ConfigProperty): List { + return get(key) as List + } + + fun get(key: ConfigProperty): Any? { + return configMap[key] + } + + fun set(key: ConfigProperty, value: Any?) { + configMap[key] = value + } + + fun entries(): Set> { + return configMap.entries + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt new file mode 100644 index 00000000..590af4ac --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt @@ -0,0 +1,14 @@ +package me.rhunk.snapenhance.config + +enum class ConfigCategory( + val key: String +) { + GENERAL("general"), + SPY("spy"), + MEDIA_DOWNLOADER("media_download"), + PRIVACY("privacy"), + UI("ui"), + EXTRAS("extras"), + TWEAKS("tweaks"), + EXPERIMENTS("experiments"); +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt new file mode 100644 index 00000000..0f399b10 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt @@ -0,0 +1,181 @@ +package me.rhunk.snapenhance.config + +import android.os.Environment +import java.io.File + +enum class ConfigProperty( + val nameKey: String, + val descriptionKey: String, + val category: ConfigCategory, + val defaultValue: Any +) { + SAVE_FOLDER( + "save_folder", "description.save_folder", ConfigCategory.GENERAL, + File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).absolutePath + "/Snapchat", + "SnapEnhance" + ).absolutePath + ), + + PREVENT_READ_RECEIPTS( + "prevent_read_receipts", + "description.prevent_read_receipts", + ConfigCategory.SPY, + false + ), + HIDE_BITMOJI_PRESENCE( + "hide_bitmoji_presence", + "description.hide_bitmoji_presence", + ConfigCategory.SPY, + false + ), + SHOW_MESSAGE_CONTENT( + "show_message_content", + "description.show_message_content", + ConfigCategory.SPY, + false + ), + MESSAGE_LOGGER("message_logger", "description.message_logger", ConfigCategory.SPY, false), + + MEDIA_DOWNLOADER_FEATURE( + "media_downloader_feature", + "description.media_downloader_feature", + ConfigCategory.MEDIA_DOWNLOADER, + true + ), + DOWNLOAD_STORIES( + "download_stories", + "description.download_stories", + ConfigCategory.MEDIA_DOWNLOADER, + false + ), + DOWNLOAD_PUBLIC_STORIES( + "download_public_stories", + "description.download_public_stories", + ConfigCategory.MEDIA_DOWNLOADER, + false + ), + DOWNLOAD_SPOTLIGHT( + "download_spotlight", + "description.download_spotlight", + ConfigCategory.MEDIA_DOWNLOADER, + false + ), + OVERLAY_MERGE( + "overlay_merge", + "description.overlay_merge", + ConfigCategory.MEDIA_DOWNLOADER, + true + ), + DOWNLOAD_INCHAT_SNAPS( + "download_inchat_snaps", + "description.download_inchat_snaps", + ConfigCategory.MEDIA_DOWNLOADER, + true + ), + + DISABLE_METRICS("disable_metrics", "description.disable_metrics", ConfigCategory.PRIVACY, true), + PREVENT_SCREENSHOTS( + "prevent_screenshots", + "description.prevent_screenshots", + ConfigCategory.PRIVACY, + true + ), + PREVENT_STATUS_NOTIFICATIONS( + "prevent_status_notifications", + "description.prevent_status_notifications", + ConfigCategory.PRIVACY, + true + ), + ANONYMOUS_STORY_VIEW( + "anonymous_story_view", + "description.anonymous_story_view", + ConfigCategory.PRIVACY, + false + ), + HIDE_TYPING_NOTIFICATION( + "hide_typing_notification", + "description.hide_typing_notification", + ConfigCategory.PRIVACY, + false + ), + + MENU_SLOT_ID("menu_slot_id", "description.menu_slot_id", ConfigCategory.UI, 1), + MESSAGE_PREVIEW_LENGTH( + "message_preview_length", + "description.message_preview_length", + ConfigCategory.UI, + 20 + ), + + AUTO_SAVE("auto_save", "description.auto_save", ConfigCategory.EXTRAS, false), + /*EXTERNAL_MEDIA_AS_SNAP( + "external_media_as_snap", + "description.external_media_as_snap", + ConfigCategory.EXTRAS, + false + ), + CONVERSATION_EXPORT( + "conversation_export", + "description.conversation_export", + ConfigCategory.EXTRAS, + false + ),*/ + SNAPCHAT_PLUS("snapchat_plus", "description.snapchat_plus", ConfigCategory.EXTRAS, false), + + REMOVE_VOICE_RECORD_BUTTON( + "remove_voice_record_button", + "description.remove_voice_record_button", + ConfigCategory.TWEAKS, + false + ), + REMOVE_STICKERS_BUTTON( + "remove_stickers_button", + "description.remove_stickers_button", + ConfigCategory.TWEAKS, + false + ), + REMOVE_COGNAC_BUTTON( + "remove_cognac_button", + "description.remove_cognac_button", + ConfigCategory.TWEAKS, + false + ), + REMOVE_CALLBUTTONS( + "remove_callbuttons", + "description.remove_callbuttons", + ConfigCategory.TWEAKS, + false + ), + LONG_SNAP_SENDING( + "long_snap_sending", + "description.long_snap_sending", + ConfigCategory.TWEAKS, + false + ), + BLOCK_ADS("block_ads", "description.block_ads", ConfigCategory.TWEAKS, false), + STREAKEXPIRATIONINFO( + "streakexpirationinfo", + "description.streakexpirationinfo", + ConfigCategory.TWEAKS, + false + ), + NEW_MAP_UI("new_map_ui", "description.new_map_ui", ConfigCategory.TWEAKS, false), + + USE_DOWNLOAD_MANAGER( + "use_download_manager", + "description.use_download_manager", + ConfigCategory.EXPERIMENTS, + false + ); + + companion object { + fun fromNameKey(nameKey: String): ConfigProperty? { + return values().find { it.nameKey == nameKey } + } + + fun sortedByCategory(): List { + return values().sortedBy { it.category.ordinal } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt new file mode 100644 index 00000000..0b8c506f --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt @@ -0,0 +1,50 @@ +package me.rhunk.snapenhance.data + +enum class FileType( + val fileExtension: String? = null, + val isVideo: Boolean = false, + val isImage: Boolean = false, + val isAudio: Boolean = false +) { + GIF("gif", false, false, false), + PNG("png", false, true, false), + MP4("mp4", true, false, false), + MP3("mp3", false, false, true), + JPG("jpg", false, true, false), + ZIP("zip", false, false, false), + WEBP("webp", false, true, false), + UNKNOWN("dat", false, false, false); + + companion object { + private val fileSignatures = HashMap() + + init { + fileSignatures["52494646"] = WEBP + fileSignatures["504b0304"] = ZIP + fileSignatures["89504e47"] = PNG + fileSignatures["00000020"] = MP4 + fileSignatures["00000018"] = MP4 + fileSignatures["0000001c"] = MP4 + fileSignatures["ffd8ffe0"] = JPG + } + + fun fromString(string: String?): FileType { + return values().firstOrNull { it.fileExtension.equals(string, ignoreCase = true) } ?: UNKNOWN + } + + private fun bytesToHex(bytes: ByteArray): String { + val result = StringBuilder() + for (b in bytes) { + result.append(String.format("%02x", b)) + } + return result.toString() + } + + fun fromByteArray(array: ByteArray): FileType { + val headerBytes = ByteArray(16) + System.arraycopy(array, 0, headerBytes, 0, 16) + val hex = bytesToHex(headerBytes) + return fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value ?: UNKNOWN + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt new file mode 100644 index 00000000..be6eb266 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt @@ -0,0 +1,25 @@ +package me.rhunk.snapenhance.data + +class SnapClassCache ( + private val classLoader: ClassLoader +) { + val snapUUID by lazy { findClass("com.snapchat.client.messaging.UUID") } + val composerLocalSubscriptionStore by lazy { findClass("com.snap.plus.lib.common.ComposerLocalSubscriptionStore") } + val snapManager by lazy { findClass("com.snapchat.client.messaging.SnapManager\$CppProxy") } + val conversationManager by lazy { findClass("com.snapchat.client.messaging.ConversationManager\$CppProxy") } + val feedManager by lazy { findClass("com.snapchat.client.messaging.FeedManager\$CppProxy") } + val presenceSession by lazy { findClass("com.snapchat.talkcorev3.PresenceSession\$CppProxy") } + val message by lazy { findClass("com.snapchat.client.messaging.Message") } + val messageUpdateEnum by lazy { findClass("com.snapchat.client.messaging.MessageUpdate") } + val bestFriendWidgetProvider by lazy { findClass("com.snap.widgets.core.BestFriendsWidgetProvider") } + val unifiedGrpcService by lazy { findClass("com.snapchat.client.grpc.UnifiedGrpcService\$CppProxy") } + val networkApi by lazy { findClass("com.snapchat.client.network_api.NetworkApi\$CppProxy") } + + private fun findClass(className: String): Class<*> { + return try { + classLoader.loadClass(className) + } catch (e: ClassNotFoundException) { + throw RuntimeException("Failed to find class $className", e) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt new file mode 100644 index 00000000..ea684430 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt @@ -0,0 +1,41 @@ +package me.rhunk.snapenhance.data + +enum class MessageState { + PREPARING, SENDING, COMMITTED, FAILED, CANCELING +} + +enum class ContentType(val id: Int) { + UNKNOWN(-1), + SNAP(0), + CHAT(1), + EXTERNAL_MEDIA(2), + SHARE(3), + NOTE(4), + STICKER(5), + STATUS(6), + LOCATION(7), + STATUS_SAVE_TO_CAMERA_ROLL(8), + STATUS_CONVERSATION_CAPTURE_SCREENSHOT(9), + STATUS_CONVERSATION_CAPTURE_RECORD(10), + STATUS_CALL_MISSED_VIDEO(11), + STATUS_CALL_MISSED_AUDIO(12), + LIVE_LOCATION_SHARE(13), + CREATIVE_TOOL_ITEM(14), + FAMILY_CENTER_INVITE(15), + FAMILY_CENTER_ACCEPT(16), + FAMILY_CENTER_LEAVE(17); + + companion object { + fun fromId(i: Int): ContentType { + return values().firstOrNull { it.id == i } ?: UNKNOWN + } + } +} + +enum class PlayableSnapState { + NOTDOWNLOADED, DOWNLOADING, DOWNLOADFAILED, PLAYABLE, VIEWEDREPLAYABLE, PLAYING, VIEWEDNOTREPLAYABLE +} + +enum class MediaReferenceType { + UNASSIGNED, OVERLAY, IMAGE, VIDEO, ASSET_BUNDLE, AUDIO, ANIMATED_IMAGE, FONT, WEB_VIEW_CONTENT, VIDEO_NO_AUDIO +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt new file mode 100644 index 00000000..e8c4cdb3 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt @@ -0,0 +1,29 @@ +package me.rhunk.snapenhance.data.wrapper + +import de.robv.android.xposed.XposedHelpers + +abstract class AbstractWrapper( + protected var instance: Any +) { + fun instance() = instance + + override fun hashCode(): Int { + return instance.hashCode() + } + + override fun toString(): String { + return instance.toString() + } + + + fun > getEnumValue(fieldName: String, defaultValue: T): T { + val mContentType = XposedHelpers.getObjectField(instance, fieldName) as Enum<*> + return java.lang.Enum.valueOf(defaultValue::class.java, mContentType.name) as T + } + + @Suppress("UNCHECKED_CAST") + fun setEnumValue(fieldName: String, value: Enum<*>) { + val type = instance.javaClass.fields.find { it.name == fieldName }?.type as Class> + XposedHelpers.setObjectField(instance, fieldName, java.lang.Enum.valueOf(type, value.name)) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt new file mode 100644 index 00000000..3c84aff0 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt @@ -0,0 +1,12 @@ +package me.rhunk.snapenhance.data.wrapper.impl + +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import me.rhunk.snapenhance.util.getObjectField + +class Message(obj: Any) : AbstractWrapper(obj) { + val orderKey get() = instance.getObjectField("mOrderKey") as Long + val senderId get() = SnapUUID(instance.getObjectField("mSenderId")) + val messageContent get() = MessageContent(instance.getObjectField("mMessageContent")) + val messageDescriptor get() = MessageDescriptor(instance.getObjectField("mDescriptor")) + val messageMetadata get() = MessageMetadata(instance.getObjectField("mMetadata")) +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt new file mode 100644 index 00000000..7c3b010f --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt @@ -0,0 +1,15 @@ +package me.rhunk.snapenhance.data.wrapper.impl + +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.setObjectField + +class MessageContent(obj: Any) : AbstractWrapper(obj) { + var content + get() = instance.getObjectField("mContent") as ByteArray + set(value) = instance.setObjectField("mContent", value) + var contentType + get() = getEnumValue("mContentType", ContentType.UNKNOWN) + set(value) = setEnumValue("mContentType", value) +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt new file mode 100644 index 00000000..ee2a37cc --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt @@ -0,0 +1,9 @@ +package me.rhunk.snapenhance.data.wrapper.impl + +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import me.rhunk.snapenhance.util.getObjectField + +class MessageDescriptor(obj: Any) : AbstractWrapper(obj) { + val messageId: Long get() = instance.getObjectField("mMessageId") as Long + val conversationId: SnapUUID get() = SnapUUID(instance.getObjectField("mConversationId")) +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt new file mode 100644 index 00000000..b32954e5 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt @@ -0,0 +1,16 @@ +package me.rhunk.snapenhance.data.wrapper.impl + +import me.rhunk.snapenhance.data.PlayableSnapState +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import me.rhunk.snapenhance.util.getObjectField + +class MessageMetadata(obj: Any) : AbstractWrapper(obj){ + val createdAt: Long get() = instance.getObjectField("mCreatedAt") as Long + val readAt: Long get() = instance.getObjectField("mReadAt") as Long + var playableSnapState: PlayableSnapState + get() = getEnumValue("mPlayableSnapState", PlayableSnapState.PLAYABLE) + set(value) { + setEnumValue("mPlayableSnapState", value) + } + val savedBy: List = (instance.getObjectField("mSavedBy") as List<*>).map { SnapUUID(it!!) } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt new file mode 100644 index 00000000..f462509e --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt @@ -0,0 +1,40 @@ +package me.rhunk.snapenhance.data.wrapper.impl + +import me.rhunk.snapenhance.SnapEnhance +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import me.rhunk.snapenhance.util.getObjectField +import java.nio.ByteBuffer +import java.util.* + +class SnapUUID(instance: Any) : AbstractWrapper(instance) { + private val uuidString by lazy { toUUID().toString() } + + val bytes: ByteArray get() { + return instance.getObjectField("mId") as ByteArray + } + + private fun toUUID(): UUID { + val buffer = ByteBuffer.wrap(bytes) + return UUID(buffer.long, buffer.long) + } + + override fun toString(): String { + return uuidString + } + + companion object { + fun fromString(uuid: String): SnapUUID { + return fromUUID(UUID.fromString(uuid)) + } + fun fromBytes(bytes: ByteArray): SnapUUID { + val constructor = SnapEnhance.classCache.snapUUID.getConstructor(ByteArray::class.java) + return SnapUUID(constructor.newInstance(bytes)) + } + fun fromUUID(uuid: UUID): SnapUUID { + val buffer = ByteBuffer.allocate(16) + buffer.putLong(uuid.mostSignificantBits) + buffer.putLong(uuid.leastSignificantBits) + return fromBytes(buffer.array()) + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/EncryptionWrapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/EncryptionWrapper.kt new file mode 100644 index 00000000..316baf64 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/EncryptionWrapper.kt @@ -0,0 +1,73 @@ +package me.rhunk.snapenhance.data.wrapper.impl.media + +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import java.io.InputStream +import java.io.OutputStream +import java.lang.reflect.Field +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class EncryptionWrapper(instance: Any) : AbstractWrapper(instance) { + fun decrypt(data: ByteArray?): ByteArray { + return newCipher(Cipher.DECRYPT_MODE).doFinal(data) + } + + fun decrypt(inputStream: InputStream?): InputStream { + return CipherInputStream(inputStream, newCipher(Cipher.DECRYPT_MODE)) + } + + fun decrypt(outputStream: OutputStream?): OutputStream { + return CipherOutputStream(outputStream, newCipher(Cipher.DECRYPT_MODE)) + } + + /** + * Search for a byte[] field with the specified length + * + * @param arrayLength the length of the byte[] field + * @return the field + */ + private fun searchByteArrayField(arrayLength: Int): Field { + return instance::class.java.fields.first { f -> + try { + if (!f.type.isArray || f.type + .componentType != Byte::class.javaPrimitiveType + ) return@first false + return@first (f.get(instance) as ByteArray).size == arrayLength + } catch (e: Exception) { + return@first false + } + } + } + + /** + * Create a new cipher with the specified mode + */ + fun newCipher(mode: Int): Cipher { + val cipher = cipher + cipher.init(mode, SecretKeySpec(keySpec, "AES"), IvParameterSpec(ivKeyParameterSpec)) + return cipher + } + + /** + * Get the cipher from the encryption wrapper + */ + private val cipher: Cipher + get() = Cipher.getInstance("AES/CBC/PKCS5Padding") + + /** + * Get the key spec from the encryption wrapper + */ + val keySpec: ByteArray by lazy { + searchByteArrayField(32)[instance] as ByteArray + } + + /** + * Get the iv key parameter spec from the encryption wrapper + */ + val ivKeyParameterSpec: ByteArray by lazy { + searchByteArrayField(16)[instance] as ByteArray + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt new file mode 100644 index 00000000..85139a76 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt @@ -0,0 +1,34 @@ +package me.rhunk.snapenhance.data.wrapper.impl.media + +import android.os.Parcelable +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import me.rhunk.snapenhance.util.getObjectField +import java.lang.reflect.Field + + +class MediaInfo(obj: Any) : AbstractWrapper(obj) { + val uri: String + get() { + val firstStringUriField = instance.javaClass.fields.first { f: Field -> f.type == String::class.java } + return instance.getObjectField(firstStringUriField.name) as String + } + + init { + var mediaInfo: Any = instance + if (mediaInfo is List<*>) { + if (mediaInfo.size == 0) { + throw RuntimeException("MediaInfo is empty") + } + mediaInfo = mediaInfo[0]!! + } + instance = mediaInfo + } + + val encryption: EncryptionWrapper? + get() { + val encryptionAlgorithmField = instance.javaClass.fields.first { f: Field -> + f.type.isInterface && Parcelable::class.java.isAssignableFrom(f.type) + } + return encryptionAlgorithmField[instance]?.let { EncryptionWrapper(it) } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt new file mode 100644 index 00000000..81c2679e --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt @@ -0,0 +1,21 @@ +package me.rhunk.snapenhance.data.wrapper.impl.media.opera + +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import me.rhunk.snapenhance.util.ReflectionHelper + +class Layer(obj: Any) : AbstractWrapper(obj) { + val paramMap: ParamMap + get() { + val layerControllerField = ReflectionHelper.searchFieldContainsToString( + instance::class.java, + instance, + "OperaPageModel" + )!! + + val paramsMapHashMap = ReflectionHelper.searchFieldStartsWithToString( + layerControllerField.type, + layerControllerField[instance] as Any, "OperaPageModel" + )!! + return ParamMap(paramsMapHashMap[layerControllerField[instance]]!!) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt new file mode 100644 index 00000000..a3258edd --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt @@ -0,0 +1,18 @@ +package me.rhunk.snapenhance.data.wrapper.impl.media.opera + +import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import me.rhunk.snapenhance.util.ReflectionHelper +import java.lang.reflect.Field +import java.util.concurrent.ConcurrentHashMap + +class LayerController(obj: Any) : AbstractWrapper(obj) { + val paramMap: ParamMap + get() { + val paramMapField: Field = ReflectionHelper.searchFieldTypeInSuperClasses( + instance::class.java, + ConcurrentHashMap::class.java + ) ?: throw RuntimeException("Could not find paramMap field") + return ParamMap(XposedHelpers.getObjectField(instance, paramMapField.name)) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt new file mode 100644 index 00000000..8b20c26e --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt @@ -0,0 +1,37 @@ +package me.rhunk.snapenhance.data.wrapper.impl.media.opera + +import me.rhunk.snapenhance.data.wrapper.AbstractWrapper +import me.rhunk.snapenhance.util.ReflectionHelper +import me.rhunk.snapenhance.util.getObjectField +import java.lang.reflect.Field +import java.util.concurrent.ConcurrentHashMap + +@Suppress("UNCHECKED_CAST") +class ParamMap(obj: Any) : AbstractWrapper(obj) { + private val paramMapField: Field by lazy { + ReflectionHelper.searchFieldTypeInSuperClasses( + instance.javaClass, + ConcurrentHashMap::class.java + )!! + } + + private val concurrentHashMap: ConcurrentHashMap + get() = instance.getObjectField(paramMapField.name) as ConcurrentHashMap + + operator fun get(key: String): Any? { + return concurrentHashMap.keys.firstOrNull{ k: Any -> k.toString() == key }?.let { concurrentHashMap[it] } + } + + fun put(key: String, value: Any) { + val keyObject = concurrentHashMap.keys.firstOrNull { k: Any -> k.toString() == key } ?: key + concurrentHashMap[keyObject] = value + } + + fun containsKey(key: String): Boolean { + return concurrentHashMap.keys.any { k: Any -> k.toString() == key } + } + + override fun toString(): String { + return concurrentHashMap.toString() + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt b/app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt new file mode 100644 index 00000000..d65b039c --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt @@ -0,0 +1,199 @@ +package me.rhunk.snapenhance.database + +import android.annotation.SuppressLint +import android.database.sqlite.SQLiteDatabase +import de.robv.android.xposed.XposedBridge +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.database.objects.* +import me.rhunk.snapenhance.manager.Manager +import java.io.File + +@SuppressLint("Range") +class DatabaseAccess(private val context: ModContext) : Manager { + private val databaseLock = Any() + + private val arroyoDatabase: File by lazy { + context.androidContext.getDatabasePath("arroyo.db") + } + + private val mainDatabase: File by lazy { + context.androidContext.getDatabasePath("main.db") + } + + private fun openMain(): SQLiteDatabase { + return SQLiteDatabase.openDatabase( + mainDatabase.absolutePath, + null, + SQLiteDatabase.OPEN_READONLY + )!! + } + + private fun openArroyo(): SQLiteDatabase { + return SQLiteDatabase.openDatabase( + arroyoDatabase.absolutePath, + null, + SQLiteDatabase.OPEN_READONLY + )!! + } + + private fun safeDatabaseOperation( + database: SQLiteDatabase, + query: (SQLiteDatabase) -> T? + ): T? { + synchronized(databaseLock) { + return runCatching { + query(database) + }.onFailure { + Logger.xposedLog("Database operation failed", it) + }.getOrNull() + } + } + + private fun readDatabaseObject( + obj: T, + database: SQLiteDatabase, + table: String, + where: String, + args: Array + ): T? { + val cursor = database.rawQuery("SELECT * FROM $table WHERE $where", args) + if (!cursor.moveToFirst()) { + cursor.close() + return null + } + try { + obj.write(cursor) + } catch (e: Throwable) { + Logger.xposedLog(e) + } + cursor.close() + return obj + } + + fun getFriendFeedInfoByUserId(userId: String): FriendFeedInfo? { + return safeDatabaseOperation(openMain()) { database -> + readDatabaseObject( + FriendFeedInfo(), + database, + "FriendsFeedView", + "friendUserId = ?", + arrayOf(userId) + ) + } + } + + fun getFriendFeedInfoByConversationId(conversationId: String): FriendFeedInfo? { + return safeDatabaseOperation(openMain()) { + readDatabaseObject( + FriendFeedInfo(), + it, + "FriendsFeedView", + "key = ?", + arrayOf(conversationId) + ) + } + } + + fun getFriendInfo(userId: String): FriendInfo? { + return safeDatabaseOperation(openMain()) { + readDatabaseObject( + FriendInfo(), + it, + "FriendWithUsername", + "userId = ?", + arrayOf(userId) + ) + } + } + + fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? { + return safeDatabaseOperation(openArroyo()) { + readDatabaseObject( + ConversationMessage(), + it, + "conversation_message", + "client_message_id = ?", + arrayOf(clientMessageId.toString()) + ) + } + } + + fun getDMConversationIdFromUserId(userId: String): UserConversationLink? { + return safeDatabaseOperation(openArroyo()) { + readDatabaseObject( + UserConversationLink(), + it, + "user_conversation", + "user_id = ? AND conversation_type = 0", + arrayOf(userId) + ) + } + } + + fun getStoryEntryFromId(storyId: String): StoryEntry? { + return safeDatabaseOperation(openMain()) { + readDatabaseObject(StoryEntry(), it, "Story", "storyId = ?", arrayOf(storyId)) + } + } + + fun getConversationParticipants(conversationId: String): List? { + return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> + val cursor = arroyoDatabase.rawQuery( + "SELECT * FROM user_conversation WHERE client_conversation_id = ?", + arrayOf(conversationId) + ) + if (!cursor.moveToFirst()) { + cursor.close() + return@safeDatabaseOperation emptyList() + } + val participants = mutableListOf() + do { + participants.add(cursor.getString(cursor.getColumnIndex("user_id"))) + } while (cursor.moveToNext()) + cursor.close() + participants + } + } + + fun getMyUserId(): String? { + return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> + val cursor = arroyoDatabase.rawQuery(buildString { + append("SELECT * FROM required_values WHERE key = 'USERID'") + }, null) + + if (!cursor.moveToFirst()) { + cursor.close() + return@safeDatabaseOperation null + } + + val userId = cursor.getString(cursor.getColumnIndex("value")) + cursor.close() + userId + } + } + + fun getMessagesFromConversationId( + conversationId: String, + limit: Int + ): List? { + return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> + val cursor = arroyoDatabase.rawQuery( + "SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?", + arrayOf(conversationId, limit.toString()) + ) + if (!cursor.moveToFirst()) { + cursor.close() + return@safeDatabaseOperation emptyList() + } + val messages = mutableListOf() + do { + val message = ConversationMessage() + message.write(cursor) + messages.add(message) + } while (cursor.moveToNext()) + cursor.close() + messages + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt b/app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt new file mode 100644 index 00000000..d54f2553 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt @@ -0,0 +1,7 @@ +package me.rhunk.snapenhance.database + +import android.database.Cursor + +interface DatabaseObject { + fun write(cursor: Cursor) +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt new file mode 100644 index 00000000..0e97373d --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt @@ -0,0 +1,44 @@ +package me.rhunk.snapenhance.database.objects + +import android.annotation.SuppressLint +import android.database.Cursor +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.database.DatabaseObject +import me.rhunk.snapenhance.util.protobuf.ProtoReader + +@Suppress("ArrayInDataClass") +data class ConversationMessage( + var client_conversation_id: String? = null, + var client_message_id: Int = 0, + var server_message_id: Int = 0, + var message_content: ByteArray? = null, + var is_saved: Int = 0, + var is_viewed_by_user: Int = 0, + var content_type: Int = 0, + var creation_timestamp: Long = 0, + var read_timestamp: Long = 0, + var sender_id: String? = null +) : DatabaseObject { + + @SuppressLint("Range") + override fun write(cursor: Cursor) { + client_conversation_id = cursor.getString(cursor.getColumnIndex("client_conversation_id")) + client_message_id = cursor.getInt(cursor.getColumnIndex("client_message_id")) + server_message_id = cursor.getInt(cursor.getColumnIndex("server_message_id")) + message_content = cursor.getBlob(cursor.getColumnIndex("message_content")) + is_saved = cursor.getInt(cursor.getColumnIndex("is_saved")) + is_viewed_by_user = cursor.getInt(cursor.getColumnIndex("is_viewed_by_user")) + content_type = cursor.getInt(cursor.getColumnIndex("content_type")) + creation_timestamp = cursor.getLong(cursor.getColumnIndex("creation_timestamp")) + read_timestamp = cursor.getLong(cursor.getColumnIndex("read_timestamp")) + sender_id = cursor.getString(cursor.getColumnIndex("sender_id")) + } + + fun getMessageAsString(): String? { + return when (ContentType.fromId(content_type)) { + ContentType.CHAT -> message_content?.let { ProtoReader(it).getString(*Constants.ARROYO_STRING_CHAT_MESSAGE_PROTO) } + else -> null + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt new file mode 100644 index 00000000..03ad5574 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt @@ -0,0 +1,33 @@ +package me.rhunk.snapenhance.database.objects + +import android.annotation.SuppressLint +import android.database.Cursor +import me.rhunk.snapenhance.database.DatabaseObject + +data class FriendFeedInfo( + var id: Int = 0, + var feedDisplayName: String? = null, + var participantsSize: Int = 0, + var lastInteractionTimestamp: Long = 0, + var displayTimestamp: Long = 0, + var displayInteractionType: String? = null, + var lastInteractionUserId: Int = 0, + var key: String? = null, + var friendUserId: String? = null, + var friendDisplayName: String? = null, +) : DatabaseObject { + + @SuppressLint("Range") + override fun write(cursor: Cursor) { + id = cursor.getInt(cursor.getColumnIndex("_id")) + feedDisplayName = cursor.getString(cursor.getColumnIndex("feedDisplayName")) + participantsSize = cursor.getInt(cursor.getColumnIndex("participantsSize")) + lastInteractionTimestamp = cursor.getLong(cursor.getColumnIndex("lastInteractionTimestamp")) + displayTimestamp = cursor.getLong(cursor.getColumnIndex("displayTimestamp")) + displayInteractionType = cursor.getString(cursor.getColumnIndex("displayInteractionType")) + lastInteractionUserId = cursor.getInt(cursor.getColumnIndex("lastInteractionUserId")) + key = cursor.getString(cursor.getColumnIndex("key")) + friendUserId = cursor.getString(cursor.getColumnIndex("friendUserId")) + friendDisplayName = cursor.getString(cursor.getColumnIndex("friendDisplayUsername")) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt new file mode 100644 index 00000000..ac1b29c7 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt @@ -0,0 +1,58 @@ +package me.rhunk.snapenhance.database.objects + +import android.annotation.SuppressLint +import android.database.Cursor +import me.rhunk.snapenhance.database.DatabaseObject + +data class FriendInfo( + var id: Int = 0, + var lastModifiedTimestamp: Long = 0, + var username: String? = null, + var userId: String? = null, + var displayName: String? = null, + var bitmojiAvatarId: String? = null, + var bitmojiSelfieId: String? = null, + var bitmojiSceneId: String? = null, + var bitmojiBackgroundId: String? = null, + var friendmojis: String? = null, + var friendmojiCategories: String? = null, + var snapScore: Int = 0, + var birthday: Long = 0, + var addedTimestamp: Long = 0, + var reverseAddedTimestamp: Long = 0, + var serverDisplayName: String? = null, + var streakLength: Int = 0, + var streakExpirationTimestamp: Long = 0, + var reverseBestFriendRanking: Int = 0, + var isPinnedBestFriend: Int = 0, + var plusBadgeVisibility: Int = 0, + var usernameForSorting: String? = null +) : DatabaseObject { + @SuppressLint("Range") + override fun write(cursor: Cursor) { + id = cursor.getInt(cursor.getColumnIndex("_id")) + lastModifiedTimestamp = cursor.getLong(cursor.getColumnIndex("_lastModifiedTimestamp")) + username = cursor.getString(cursor.getColumnIndex("username")) + userId = cursor.getString(cursor.getColumnIndex("userId")) + displayName = cursor.getString(cursor.getColumnIndex("displayName")) + bitmojiAvatarId = cursor.getString(cursor.getColumnIndex("bitmojiAvatarId")) + bitmojiSelfieId = cursor.getString(cursor.getColumnIndex("bitmojiSelfieId")) + bitmojiSceneId = cursor.getString(cursor.getColumnIndex("bitmojiSceneId")) + bitmojiBackgroundId = cursor.getString(cursor.getColumnIndex("bitmojiBackgroundId")) + friendmojis = cursor.getString(cursor.getColumnIndex("friendmojis")) + friendmojiCategories = cursor.getString(cursor.getColumnIndex("friendmojiCategories")) + snapScore = cursor.getInt(cursor.getColumnIndex("score")) + birthday = cursor.getLong(cursor.getColumnIndex("birthday")) + addedTimestamp = cursor.getLong(cursor.getColumnIndex("addedTimestamp")) + reverseAddedTimestamp = cursor.getLong(cursor.getColumnIndex("reverseAddedTimestamp")) + serverDisplayName = cursor.getString(cursor.getColumnIndex("serverDisplayName")) + streakLength = cursor.getInt(cursor.getColumnIndex("streakLength")) + streakExpirationTimestamp = cursor.getLong(cursor.getColumnIndex("streakExpiration")) + reverseBestFriendRanking = cursor.getInt(cursor.getColumnIndex("reverseBestFriendRanking")) + usernameForSorting = cursor.getString(cursor.getColumnIndex("usernameForSorting")) + if (cursor.getColumnIndex("isPinnedBestFriend") != -1) isPinnedBestFriend = + cursor.getInt(cursor.getColumnIndex("isPinnedBestFriend")) + if (cursor.getColumnIndex("plusBadgeVisibility") != -1) plusBadgeVisibility = + cursor.getInt(cursor.getColumnIndex("plusBadgeVisibility")) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt new file mode 100644 index 00000000..f0001ca5 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt @@ -0,0 +1,23 @@ +package me.rhunk.snapenhance.database.objects + +import android.annotation.SuppressLint +import android.database.Cursor +import me.rhunk.snapenhance.database.DatabaseObject + +data class StoryEntry( + var id: Int = 0, + var storyId: String? = null, + var displayName: String? = null, + var isLocal: Boolean? = null, + var userId: String? = null +) : DatabaseObject { + + @SuppressLint("Range") + override fun write(cursor: Cursor) { + id = cursor.getInt(cursor.getColumnIndex("_id")) + storyId = cursor.getString(cursor.getColumnIndex("storyId")) + displayName = cursor.getString(cursor.getColumnIndex("displayName")) + isLocal = cursor.getInt(cursor.getColumnIndex("isLocal")) == 1 + userId = cursor.getString(cursor.getColumnIndex("userId")) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt new file mode 100644 index 00000000..e44c019e --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt @@ -0,0 +1,19 @@ +package me.rhunk.snapenhance.database.objects + +import android.annotation.SuppressLint +import android.database.Cursor +import me.rhunk.snapenhance.database.DatabaseObject + +class UserConversationLink( + var user_id: String? = null, + var client_conversation_id: String? = null, + var conversation_type: Int = 0 +) : DatabaseObject { + + @SuppressLint("Range") + override fun write(cursor: Cursor) { + user_id = cursor.getString(cursor.getColumnIndex("user_id")) + client_conversation_id = cursor.getString(cursor.getColumnIndex("client_conversation_id")) + conversation_type = cursor.getInt(cursor.getColumnIndex("conversation_type")) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/event/EventBus.kt b/app/src/main/kotlin/me/rhunk/snapenhance/event/EventBus.kt new file mode 100644 index 00000000..dca07ff9 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/event/EventBus.kt @@ -0,0 +1,62 @@ +package me.rhunk.snapenhance.event + +import me.rhunk.snapenhance.ModContext +import kotlin.reflect.KClass + +abstract class Event { + lateinit var context: ModContext +} + +interface IListener { + fun handle(event: T) +} + +class EventBus( + private val context: ModContext +) { + private val subscribers = mutableMapOf, MutableList>>() + + fun subscribe(event: KClass, listener: IListener) { + if (!subscribers.containsKey(event)) { + subscribers[event] = mutableListOf() + } + subscribers[event]!!.add(listener) + } + + fun subscribe(event: KClass, listener: (T) -> Unit) { + subscribe(event, object : IListener { + override fun handle(event: T) { + listener(event) + } + }) + } + + fun unsubscribe(event: KClass, listener: IListener) { + if (!subscribers.containsKey(event)) { + return + } + subscribers[event]!!.remove(listener) + } + + fun post(event: T) { + if (!subscribers.containsKey(event::class)) { + return + } + + event.context = context + + subscribers[event::class]!!.forEach { listener -> + @Suppress("UNCHECKED_CAST") + try { + (listener as IListener).handle(event) + } catch (t: Throwable) { + println("Error while handling event ${event::class.simpleName} by ${listener::class.simpleName}") + t.printStackTrace() + } + } + } + + fun clear() { + subscribers.clear() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/event/Events.kt b/app/src/main/kotlin/me/rhunk/snapenhance/event/Events.kt new file mode 100644 index 00000000..aae56c2d --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/event/Events.kt @@ -0,0 +1,3 @@ +package me.rhunk.snapenhance.event + +//TODO: addView event \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt new file mode 100644 index 00000000..ab632492 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt @@ -0,0 +1,31 @@ +package me.rhunk.snapenhance.features + +import me.rhunk.snapenhance.ModContext + +abstract class Feature( + val nameKey: String, + val loadParams: Int = FeatureLoadParams.INIT_SYNC +) { + lateinit var context: ModContext + + /** + * called on the main thread when the mod initialize + */ + open fun init() {} + + /** + * called on a dedicated thread when the mod initialize + */ + open fun asyncInit() {} + + /** + * called when the Snapchat Activity is created + */ + open fun onActivityCreate() {} + + + /** + * called on a dedicated thread when the Snapchat Activity is created + */ + open fun asyncOnActivityCreate() {} +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt new file mode 100644 index 00000000..fbbbc2f4 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.features + +object FeatureLoadParams { + const val NO_INIT = 0 + + const val INIT_SYNC = 1 + const val ACTIVITY_CREATE_SYNC = 2 + + const val INIT_ASYNC = 3 + const val ACTIVITY_CREATE_ASYNC = 4 +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt new file mode 100644 index 00000000..7565c52d --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt @@ -0,0 +1,67 @@ +package me.rhunk.snapenhance.features.impl + +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.util.setObjectField +import java.lang.reflect.Field +import java.lang.reflect.Modifier +import java.util.concurrent.atomic.AtomicReference + +class ConfigEnumKeys : Feature("Config enum keys", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + private fun hookAllEnums(enumClass: Class<*>, callback: (String, AtomicReference) -> Unit) { + //Enum(String, int, ?) + //or Enum(?) + val enumDataClass = enumClass.constructors[0].parameterTypes.first { clazz: Class<*> -> clazz != String::class.java && !clazz.isPrimitive } + + //get the field which contains the enum data class + val enumDataField = enumClass.declaredFields.first { field: Field -> field.type == enumDataClass } + + //get the field value of the enum data class (the first field of the class with the desc Object) + val objectDataField = enumDataField.type.fields.first { field: Field -> + field.type == Any::class.java && Modifier.isPublic( + field.modifiers + ) && Modifier.isFinal(field.modifiers) + } + + enumClass.enumConstants.forEach { enum -> + enumDataField.get(enum)?.let { enumData -> + val key = objectDataField.get(enumData)!!.toString() + val value = AtomicReference(objectDataField.get(enumData)) + callback(key, value) + enumData.setObjectField(objectDataField.name, value.get()) + } + } + } + + override fun onActivityCreate() { + if (context.config.bool(ConfigProperty.NEW_MAP_UI)) { + hookAllEnums(context.mappings.getMappedClass("enums", "PLUS")) { key, atomicValue -> + if (key == "REDUCE_MY_PROFILE_UI_COMPLEXITY") atomicValue.set(true) + } + } + + if (context.config.bool(ConfigProperty.LONG_SNAP_SENDING)) { + hookAllEnums(context.mappings.getMappedClass("enums", "ARROYO")) { key, atomicValue -> + if (key == "ENABLE_LONG_SNAP_SENDING") atomicValue.set(true) + } + } + + if (context.config.bool(ConfigProperty.STREAKEXPIRATIONINFO)) { + hookAllEnums(context.mappings.getMappedClass("enums", "FRIENDS_FEED")) { key, atomicValue -> + if (key == "STREAK_EXPIRATION_INFO") atomicValue.set(true) + } + } + + if (context.config.bool(ConfigProperty.BLOCK_ADS)) { + hookAllEnums(context.mappings.getMappedClass("enums", "SNAPADS")) { key, atomicValue -> + if (key == "BYPASS_AD_FEATURE_GATE") { + atomicValue.set(true) + } + if (key == "CUSTOM_AD_SERVER_URL" || key == "CUSTOM_AD_INIT_SERVER_URL" || key == "CUSTOM_AD_TRACKER_URL") { + atomicValue.set("http://127.0.0.1") + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt new file mode 100644 index 00000000..fd0f4b9f --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt @@ -0,0 +1,71 @@ +package me.rhunk.snapenhance.features.impl + +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker + +class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { + lateinit var conversationManager: Any + + var lastOpenedConversationUUID: SnapUUID? = null + var lastFetchConversationUserUUID: SnapUUID? = null + var lastFetchConversationUUID: SnapUUID? = null + var lastFocusedMessageId: Long = -1 + + override fun init() { + Hooker.hookConstructor(context.classCache.conversationManager, HookStage.BEFORE) { + conversationManager = it.thisObject() + } + } + + override fun onActivityCreate() { + with(context.classCache.conversationManager) { + Hooker.hook(this, "enterConversation", HookStage.BEFORE) { + lastOpenedConversationUUID = SnapUUID(it.arg(0)) + } + + Hooker.hook(this, "getOneOnOneConversationIds", HookStage.BEFORE) { param -> + val conversationIds: List = param.arg(0) + if (conversationIds.isNotEmpty()) { + lastFetchConversationUserUUID = SnapUUID(conversationIds[0]) + } + } + + Hooker.hook(this, "exitConversation", HookStage.BEFORE) { + lastOpenedConversationUUID = null + } + + Hooker.hook(this, "fetchConversation", HookStage.BEFORE) { + lastFetchConversationUUID = SnapUUID(it.arg(0)) + } + } + + } + + override fun asyncInit() { + arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook -> + Hooker.hook(context.classCache.presenceSession, hook, HookStage.BEFORE, { context.config.bool(ConfigProperty.HIDE_BITMOJI_PRESENCE) }) { + it.setResult(null) + } + } + + //get last opened snap for media downloader + Hooker.hook(context.classCache.snapManager, "onSnapInteraction", HookStage.BEFORE) { param -> + lastOpenedConversationUUID = SnapUUID(param.arg(1)) + lastFocusedMessageId = param.arg(2) + } + + Hooker.hook(context.classCache.conversationManager, "fetchMessage", HookStage.BEFORE) { param -> + lastFetchConversationUserUUID = SnapUUID((param.arg(0) as Any)) + lastFocusedMessageId = param.arg(1) + } + + Hooker.hook(context.classCache.conversationManager, "sendTypingNotification", HookStage.BEFORE, + {context.config.bool(ConfigProperty.HIDE_TYPING_NOTIFICATION)}) { + it.setResult(null) + } + } +} \ 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 new file mode 100644 index 00000000..c6b55613 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -0,0 +1,430 @@ +package me.rhunk.snapenhance.features.impl.downloader + +import android.app.AlertDialog +import android.content.DialogInterface +import android.graphics.Bitmap +import android.media.MediaScannerConnection +import android.net.Uri +import android.widget.ImageView +import com.arthenica.ffmpegkit.FFmpegKit +import de.robv.android.xposed.XposedBridge +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.Constants.ARROYO_URL_KEY_PROTO_PATH +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.Logger.xposedLog +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.FileType +import me.rhunk.snapenhance.data.wrapper.impl.media.MediaInfo +import me.rhunk.snapenhance.data.wrapper.impl.media.opera.Layer +import me.rhunk.snapenhance.data.wrapper.impl.media.opera.ParamMap +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.features.impl.Messaging +import me.rhunk.snapenhance.features.impl.spy.MessageLogger +import me.rhunk.snapenhance.hook.HookAdapter +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.util.EncryptionUtils +import me.rhunk.snapenhance.util.PreviewUtils +import me.rhunk.snapenhance.util.download.CdnDownloader +import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.util.protobuf.ProtoReader +import java.io.* +import java.net.HttpURLConnection +import java.net.URL +import java.nio.file.Paths +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import java.util.zip.ZipInputStream +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import kotlin.io.path.inputStream + +enum class MediaType { + ORIGINAL, OVERLAY +} + +class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + private var lastSeenMediaInfoMap: MutableMap? = null + private var lastSeenMapParams: ParamMap? = null + private var isFFmpegPresent: Boolean? = null + + private fun canMergeOverlay(): Boolean { + if (!context.config.bool(ConfigProperty.OVERLAY_MERGE)) return false + if (isFFmpegPresent != null) { + return isFFmpegPresent!! + } + //check if ffmpeg is correctly installed + isFFmpegPresent = runCatching { FFmpegKit.execute("-version") }.isSuccess + return isFFmpegPresent!! + } + + private fun createNewFilePath(hash: Int, author: String, fileType: FileType): String? { + val hexHash = Integer.toHexString(hash) + return author + "/" + hexHash + "." + fileType.fileExtension + } + + private fun downloadFile(outputFile: File, content: ByteArray): Boolean { + val onDownloadComplete = { + context.shortToast( + "Saved to " + outputFile.absolutePath.replace(context.config.string(ConfigProperty.SAVE_FOLDER), "") + .substring(1) + ) + } + if (!context.config.bool(ConfigProperty.USE_DOWNLOAD_MANAGER)) { + try { + val fos = FileOutputStream(outputFile) + fos.write(content) + fos.close() + MediaScannerConnection.scanFile( + context.androidContext, + arrayOf(outputFile.absolutePath), + null, + null + ) + onDownloadComplete() + } catch (e: Throwable) { + Logger.xposedLog(e) + context.longToast("Failed to save file: " + e.message) + return false + } + return true + } + context.downloadServer.startFileDownload(outputFile, content) { result -> + if (result) { + onDownloadComplete() + return@startFileDownload + } + context.longToast("Failed to save file. Check logs for more info.") + } + return true + } + + + private fun mergeOverlay(original: ByteArray, overlay: ByteArray, isPreviewMode: Boolean): ByteArray? { + context.longToast("Merging current media with overlay. This may take a while.") + val originalFileType = FileType.fromByteArray(original) + val overlayFileType = FileType.fromByteArray(overlay) + //merge files + val mergedFile = File.createTempFile("merged", "." + originalFileType.fileExtension) + val tempVideoFile = File.createTempFile("original", "." + originalFileType.fileExtension).also { + with(FileOutputStream(it)) { + write(original) + close() + } + } + val tempOverlayFile = File.createTempFile("overlay", "." + overlayFileType.fileExtension).also { + with(FileOutputStream(it)) { + write(overlay) + close() + } + } + + //TODO: improve ffmpeg speed + val fFmpegSession = FFmpegKit.execute( + "-y -i " + + tempVideoFile.absolutePath + + " -i " + + tempOverlayFile.absolutePath + + " -filter_complex \"[0]scale2ref[img][vid];[img]setsar=1[img];[vid]nullsink; [img][1]overlay=(W-w)/2:(H-h)/2,scale=2*trunc(iw*sar/2):2*trunc(ih/2)\" -c:v libx264 -q:v 13 -c:a copy " + + " -threads 6 ${(if (isPreviewMode) "-frames:v 1" else "")} " + + mergedFile.absolutePath + ) + tempVideoFile.delete() + tempOverlayFile.delete() + if (fFmpegSession.returnCode.value != 0) { + mergedFile.delete() + context.longToast("Failed to merge video and overlay. See logs for more details.") + Logger.xposedLog(fFmpegSession.output) + return null + } + val mergedFileData: ByteArray = FileInputStream(mergedFile).readBytes() + mergedFile.delete() + return mergedFileData + } + + private fun queryMediaData(mediaInfo: MediaInfo): ByteArray { + val mediaUri = Uri.parse(mediaInfo.uri) + val mediaInputStream = AtomicReference() + if (mediaUri.scheme == "file") { + mediaInputStream.set(Paths.get(mediaUri.path).inputStream()) + } else { + val url = URL(mediaUri.toString()) + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "GET" + connection.setRequestProperty("User-Agent", Constants.USER_AGENT) + connection.connect() + mediaInputStream.set(connection.inputStream) + } + mediaInfo.encryption?.let { encryption -> + mediaInputStream.set(CipherInputStream(mediaInputStream.get(), encryption.newCipher(Cipher.DECRYPT_MODE))) + } + return mediaInputStream.get().readBytes() + } + + private fun createNeededDirectories(file: File): File { + val directory = file.parentFile ?: return file + if (!directory.exists()) { + directory.mkdirs() + } + return file + } + + private fun isFileExists(hash: Int, author: String, fileType: FileType): Boolean { + val fileName: String = createNewFilePath(hash, author, fileType) ?: return false + val outputFile: File = + createNeededDirectories(File(context.config.string(ConfigProperty.SAVE_FOLDER), fileName)) + return outputFile.exists() + } + + + /* + * Download the last seen media + */ + fun downloadLastOperaMediaAsync() { + if (lastSeenMapParams == null || lastSeenMediaInfoMap == null) return + context.executeAsync { + handleOperaMedia( + lastSeenMapParams!!, + lastSeenMediaInfoMap!!, true + ) + } + } + + private fun downloadOperaMedia(mediaInfoMap: Map, author: String) { + if (mediaInfoMap.isEmpty()) return + val originalMediaInfo = mediaInfoMap[MediaType.ORIGINAL]!! + if (mediaInfoMap.containsKey(MediaType.OVERLAY)) { + context.shortToast("Downloading split snap") + } + var mediaContent: ByteArray? = queryMediaData(originalMediaInfo) + val hash = Arrays.hashCode(mediaContent) + if (mediaInfoMap.containsKey(MediaType.OVERLAY)) { + //prevent converting the same media twice + if (isFileExists(hash, author, FileType.fromByteArray(mediaContent!!))) { + context.shortToast("Media already exists") + return + } + val overlayMediaInfo = mediaInfoMap[MediaType.OVERLAY]!! + val overlayContent: ByteArray = queryMediaData(overlayMediaInfo) + mediaContent = mergeOverlay(mediaContent, overlayContent, false) + } + val fileType = FileType.fromByteArray(mediaContent!!) + downloadMediaContent(mediaContent, hash, author, fileType) + } + + private fun downloadMediaContent( + data: ByteArray, + hash: Int, + messageAuthor: String, + fileType: FileType + ): Boolean { + val fileName: String = createNewFilePath(hash, messageAuthor, fileType) ?: return false + val outputFile: File = createNeededDirectories(File(context.config.string(ConfigProperty.SAVE_FOLDER), fileName)) + if (outputFile.exists()) { + context.shortToast("Media already exists") + return false + } + return downloadFile(outputFile, data) + } + + /** + * Handles the media from the opera viewer + * + * @param paramMap the parameters from the opera viewer + * @param mediaInfoMap the media info map + * @param forceDownload if the media should be downloaded + */ + private fun handleOperaMedia( + paramMap: ParamMap, + mediaInfoMap: Map, + forceDownload: Boolean + ) { + //messages + if (paramMap.containsKey("MESSAGE_ID")) { + val id = paramMap["MESSAGE_ID"].toString() + val messageId = id.substring(id.lastIndexOf(":") + 1).toLong() + val senderId: String = context.database.getConversationMessageFromId(messageId)!!.sender_id!! + val author = context.database.getFriendInfo(senderId)!!.usernameForSorting!! + downloadOperaMedia(mediaInfoMap, author) + return + } + + //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.DOWNLOAD_STORIES)) + ) { + val storyIdStartIndex = playlistV2Group.indexOf("storyUserId=") + 12 + val storyUserId = playlistV2Group.substring(storyIdStartIndex, playlistV2Group.indexOf(",", storyIdStartIndex)) + val author = context.database.getFriendInfo(storyUserId) + downloadOperaMedia(mediaInfoMap, author!!.usernameForSorting!!) + return + } + val snapSource = paramMap["SNAP_SOURCE"].toString() + + //public stories + if (snapSource == "PUBLIC_USER" && (forceDownload || context.config.bool(ConfigProperty.DOWNLOAD_PUBLIC_STORIES))) { + val userDisplayName = (if (paramMap.containsKey("USER_DISPLAY_NAME")) paramMap["USER_DISPLAY_NAME"].toString() else "").replace( + "[^\\x00-\\x7F]".toRegex(), + "") + downloadOperaMedia(mediaInfoMap, "Public-Stories/$userDisplayName") + } + + //spotlight + if (snapSource == "SINGLE_SNAP_STORY" && (forceDownload || context.config.bool(ConfigProperty.DOWNLOAD_SPOTLIGHT))) { + downloadOperaMedia(mediaInfoMap, "Spotlight") + } + } + + override fun asyncOnActivityCreate() { + val operaViewerControllerClass: Class<*> = context.mappings.getMappedClass("OperaPageViewController", "Class") + + val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> + val viewState = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "viewStateField")).toString() + if (viewState != "FULLY_DISPLAYED") { + return@onOperaViewStateCallback + } + val operaLayerList = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "layerListField")) as ArrayList<*> + val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap + + if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) + return@onOperaViewStateCallback + + val mediaInfoMap = mutableMapOf() + val isVideo = mediaParamMap.containsKey("video_media_info_list") + mediaInfoMap[MediaType.ORIGINAL] = MediaInfo( + (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!! + ) + if (canMergeOverlay() && mediaParamMap.containsKey("overlay_image_media_info")) { + mediaInfoMap[MediaType.OVERLAY] = + MediaInfo(mediaParamMap["overlay_image_media_info"]!!) + } + lastSeenMapParams = mediaParamMap + lastSeenMediaInfoMap = mediaInfoMap + if (!context.config.bool(ConfigProperty.MEDIA_DOWNLOADER_FEATURE)) return@onOperaViewStateCallback + + context.executeAsync { + try { + handleOperaMedia(mediaParamMap, mediaInfoMap, false) + } catch (e: Throwable) { + Logger.xposedLog(e) + context.longToast(e.message!!) + } + } + } + + arrayOf("onDisplayStateChange", "onDisplayStateChange2").forEach { methodName -> + Hooker.hook( + operaViewerControllerClass, + context.mappings.getMappedValue("OperaPageViewController", methodName), + HookStage.AFTER, onOperaViewStateCallback + ) + } + } + + /** + * Called when a message is focused in chat + */ + //TODO: use snapchat classes instead of database (when content is deleted) + fun onMessageActionMenu(isPreviewMode: Boolean) { + //check if the message was focused in a conversation + val messaging = context.feature(Messaging::class) + if (messaging.lastOpenedConversationUUID == null) return + val message = context.database.getConversationMessageFromId(messaging.lastFocusedMessageId) ?: return + + //get the message author + val messageAuthor: String = context.database.getFriendInfo(message.sender_id!!)!!.usernameForSorting!! + + //check if the messageId + val contentType: ContentType = ContentType.fromId(message.content_type) + if (context.feature(MessageLogger::class).isMessageRemoved(message.client_message_id.toLong())) { + context.shortToast("Preview/Download are not yet available for deleted messages") + return + } + if (contentType != ContentType.NOTE && + contentType != ContentType.SNAP && + contentType != ContentType.EXTERNAL_MEDIA) { + context.shortToast("Unsupported content type $contentType") + return + } + val messageReader = ProtoReader(message.message_content!!) + val urlKey: String = messageReader.getString(*ARROYO_URL_KEY_PROTO_PATH)!! + + //download the message content + try { + context.shortToast("Retriving message media") + var inputStream: InputStream = CdnDownloader.downloadWithDefaultEndpoints(urlKey) ?: return + inputStream = EncryptionUtils.decryptInputStreamFromArroyo( + inputStream, + contentType, + messageReader + ) + + var mediaData: ByteArray = inputStream.readBytes() + var fileType = FileType.fromByteArray(mediaData) + val isZipFile = fileType == FileType.ZIP + + //videos with overlay are packed in a zip file + //there are 2 files in the zip file, the video (webm) and the overlay (png) + if (isZipFile) { + var videoData: ByteArray? = null + var overlayData: ByteArray? = null + val zipInputStream = ZipInputStream(ByteArrayInputStream(mediaData)) + while (zipInputStream.nextEntry != null) { + val zipEntryData: ByteArray = zipInputStream.readBytes() + val entryFileType = FileType.fromByteArray(zipEntryData) + if (entryFileType.isVideo) { + videoData = zipEntryData + } else if (entryFileType.isImage) { + overlayData = zipEntryData + } + } + if (videoData == null || overlayData == null) { + Logger.xposedLog("Invalid data in zip file") + return + } + val mergedVideo = mergeOverlay(videoData, overlayData, isPreviewMode) + val videoFileType = FileType.fromByteArray(videoData) + if (!isPreviewMode) { + downloadMediaContent( + mergedVideo!!, + Arrays.hashCode(videoData), + messageAuthor, + videoFileType + ) + return + } + mediaData = mergedVideo!! + fileType = videoFileType + } + if (isPreviewMode) { + runCatching { + val bitmap: Bitmap = PreviewUtils.createPreview(mediaData, fileType.isVideo)!! + val builder = AlertDialog.Builder(context.mainActivity) + builder.setTitle("Preview") + val imageView = ImageView(builder.context) + imageView.setImageBitmap(bitmap) + builder.setView(imageView) + builder.setPositiveButton( + "Close" + ) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + context.runOnUiThread { builder.show() } + }.onFailure { + context.shortToast("Failed to create preview: ${it.message}") + xposedLog(it) + } + return + } + downloadMediaContent(mediaData, mediaData.contentHashCode(), messageAuthor, fileType) + } catch (e: FileNotFoundException) { + context.shortToast("Unable to get $urlKey from cdn list. Check the logs for more info") + } catch (e: Throwable) { + context.shortToast("Failed to download " + e.message) + xposedLog(e) + } + } +} \ 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 new file mode 100644 index 00000000..aa640b16 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/AutoSave.kt @@ -0,0 +1,133 @@ +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.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.spy.MessageLogger +import me.rhunk.snapenhance.features.impl.spy.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 + + val callback = CallbackBuilder(callbackClass) + .override("onError") { + Logger.xposedLog("Error saving message $messageId") + }.build() + + runCatching { + updateMessageMethod.invoke( + context.feature(Messaging::class).conversationManager, + conversationId.instance(), + 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 || context.feature(StealthMode::class).isStealth(lastOpenedConversationUUID.toString())) 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(0).getObjectField("mConversationId")) + val messages = param.arg>(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!!.instance(), + 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/Notifications.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt new file mode 100644 index 00000000..735dbc4f --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt @@ -0,0 +1,183 @@ +package me.rhunk.snapenhance.features.impl.extras + +import android.app.Notification +import android.app.NotificationManager +import android.content.Context +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.PreviewUtils +import me.rhunk.snapenhance.util.download.CdnDownloader +import me.rhunk.snapenhance.util.protobuf.ProtoReader + +class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) { + private val notificationDataQueue = mutableMapOf() + private val cachedNotifications = mutableMapOf>() + + 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 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: NotificationData, text: String) { + with(notification.notification.extras) { + putString("android.text", text) + putString("android.bigText", text) + } + } + + private fun computeNotificationText(conversationId: String): String { + val messageBuilder = StringBuilder() + cachedNotifications.computeIfAbsent(conversationId) { mutableListOf() }.forEach { + if (messageBuilder.isNotEmpty()) messageBuilder.append("\n") + messageBuilder.append(it) + } + return messageBuilder.toString() + } + + private fun fetchMessagesResult(conversationId: String, messages: List) { + val sendNotificationData = { it: NotificationData -> + XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( + it.tag, it.id, it.notification, it.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 = cachedNotifications.let { it.computeIfAbsent(conversationId) { mutableListOf() } } + val appendNotifications: () -> Unit = { setNotificationText(notificationData, 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 -> { + //serialize the message content into a json object + val serializedMessageContent = context.gson.toJsonTree(snapMessage.messageContent.instance()).asJsonObject + val mediaReferences = serializedMessageContent["mRemoteMediaReferences"] + .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } + .flatten() + + mediaReferences.forEach { media -> + val mediaContent = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() + val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) + val urlKey = ProtoReader(mediaContent).getString(2, 2) ?: return@forEach + runCatching { + //download the media + var mediaInputStream = CdnDownloader.downloadWithDefaultEndpoints(urlKey)!! + val mediaInfo = ProtoReader(contentData).readPath(*Constants.MESSAGE_SNAP_ENCRYPTION_PROTO_PATH) ?: return@runCatching + //decrypt if necessary + if (mediaInfo.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX)) { + mediaInputStream = EncryptionUtils.decryptInputStream(mediaInputStream, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX) + } + + val mediaByteArray = mediaInputStream.readBytes() + val bitmapPreview = PreviewUtils.createPreview(mediaByteArray, 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(id = System.nanoTime().toInt(), notification = notificationBuilder.build())) + return@onEach + }.onFailure { + Logger.error("Failed to send preview notification", it) + } + } + } + else -> { + notificationCache.add(formatUsername("sent $contentType")) + } + } + + sendNotificationData(notificationData) + }.clear() + } + + override fun init() { + val fetchConversationWithMessagesCallback = context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") + + Hooker.hook(notifyAsUserMethod, HookStage.BEFORE, { context.config.bool(ConfigProperty.SHOW_MESSAGE_CONTENT) }) { + val notificationData = NotificationData(it.argNullable(0), it.arg(1), it.arg(2), it.arg(3)) + + if (!notificationData.notification.extras.containsKey("system_notification_extras")) { + return@hook + } + val extras: Bundle = notificationData.notification.extras.getBundle("system_notification_extras")!! + + val messageId = extras.getString("message_id")!! + val notificationType = extras.getString("notification_type")!! + val conversationId = extras.getString("conversation_id")!! + + 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") { param -> + val messageList = (param.arg(1) as List).map { msg -> Message(msg) } + fetchMessagesResult(conversationId, messageList) + } + .override("onError") { param -> + Logger.xposedLog("Failed to fetch message ${param.arg(0) as Any}") + }.build() + + fetchConversationWithMessagesMethod.invoke(conversationManager, SnapUUID.fromString(conversationId).instance(), callback) + it.setResult(null) + } + } + + 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 new file mode 100644 index 00000000..4b89914c --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/SnapchatPlus.kt @@ -0,0 +1,28 @@ +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) { + override fun asyncOnActivityCreate() { + if (!context.config.bool(ConfigProperty.SNAPCHAT_PLUS)) return + + Hooker.hookConstructor(context.mappings.getMappedClass("SubscriptionInfoClass"), HookStage.BEFORE) { param -> + //check if the user is already premium + if (param.arg(0) as Int == 2) { + return@hookConstructor + } + //subscription info tier + param.setArg(0, 2) + //subscription status + param.setArg(1, 2) + //subscription time + param.setArg(2, System.currentTimeMillis() - 7776000000L) + //expiration time + param.setArg(3, System.currentTimeMillis() + 15552000000L) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt new file mode 100644 index 00000000..f0f12d55 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt @@ -0,0 +1,45 @@ +package me.rhunk.snapenhance.features.impl.privacy + +import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.Logger.debug +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookAdapter +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import java.nio.charset.StandardCharsets +import java.util.* + +class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams.INIT_SYNC) { + override fun init() { + val disableMetricsFilter: (HookAdapter) -> Boolean = { + context.config.bool(ConfigProperty.DISABLE_METRICS) + } + + Hooker.hook(context.classCache.unifiedGrpcService, "unaryCall", HookStage.BEFORE, disableMetricsFilter) { param -> + val url: String = param.arg(0) + if (url.endsWith("snapchat.valis.Valis/SendClientUpdate") || + url.endsWith("targetingQuery") + ) { + param.setResult(null) + } + } + + Hooker.hook(context.classCache.networkApi, "submit", HookStage.BEFORE, disableMetricsFilter) { param -> + val httpRequest: Any = param.arg(0) + val url = XposedHelpers.getObjectField(httpRequest, "mUrl").toString() + if (url.contains("resolve?co=")) { + val index = url.indexOf("co=") + val end = url.lastIndexOf("&") + val co = url.substring(index + 3, end) + val decoded = Base64.getDecoder().decode(co.toByteArray(StandardCharsets.UTF_8)) + debug("decoded : " + decoded.toString(Charsets.UTF_8)) + debug("content: $co") + } + if (url.contains("app-analytics") || url.endsWith("v1/metrics")) { + param.setResult(null) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventScreenshotDetections.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventScreenshotDetections.kt new file mode 100644 index 00000000..57491e88 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventScreenshotDetections.kt @@ -0,0 +1,16 @@ +package me.rhunk.snapenhance.features.impl.privacy + +import android.database.ContentObserver +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 PreventScreenshotDetections : Feature("Prevent Screenshot Detections", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { + Hooker.hook(ContentObserver::class.java,"dispatchChange", HookStage.BEFORE, { context.config.bool(ConfigProperty.PREVENT_SCREENSHOTS) }) { + it.setResult(null) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/AnonymousStoryViewing.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/AnonymousStoryViewing.kt new file mode 100644 index 00000000..86028fe2 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/AnonymousStoryViewing.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.features.impl.spy + +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.util.getObjectField + +class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { + Hooker.hook(context.classCache.networkApi,"submit", HookStage.BEFORE, { context.config.bool(ConfigProperty.ANONYMOUS_STORY_VIEW) }) { + val httpRequest: Any = it.arg(0) + val url = httpRequest.getObjectField("mUrl") as String + if (url.endsWith("readreceipt-indexer/batchuploadreadreceipts") || url.endsWith("v2/batch_cta")) { + it.setResult(null) + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/MessageLogger.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/MessageLogger.kt new file mode 100644 index 00000000..1cdfc8ee --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/MessageLogger.kt @@ -0,0 +1,64 @@ +package me.rhunk.snapenhance.features.impl.spy + +import com.google.gson.JsonParser +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.MessageState +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.util.getObjectField + +class MessageLogger : Feature("MessageLogger", loadParams = FeatureLoadParams.INIT_SYNC) { + private val messageCache = mutableMapOf() + private val removedMessages = linkedSetOf() + + fun isMessageRemoved(messageId: Long) = removedMessages.contains(messageId) + + override fun init() { + Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { + context.config.bool(ConfigProperty.MESSAGE_LOGGER) + }) { + val message = it.thisObject() + val messageId = message.getObjectField("mDescriptor").getObjectField("mMessageId") as Long + val contentType = ContentType.valueOf(message.getObjectField("mMessageContent").getObjectField("mContentType").toString()) + val messageState = MessageState.valueOf(message.getObjectField("mState").toString()) + + if (messageState != MessageState.COMMITTED) return@hookConstructor + + if (contentType == ContentType.STATUS) { + //query the deleted message + val deletedMessage: String = if (messageCache.containsKey(messageId)) messageCache[messageId] else { + context.bridgeClient.getMessageLoggerMessage(messageId)?.toString(Charsets.UTF_8) + } ?: return@hookConstructor + + val messageJsonObject = JsonParser.parseString(deletedMessage).asJsonObject + + //if the message is a snap make it playable + if (messageJsonObject["mMessageContent"].asJsonObject["mContentType"].asString == "SNAP") { + messageJsonObject["mMetadata"].asJsonObject.addProperty("mPlayableSnapState", "PLAYABLE") + } + + //serialize all properties of messageJsonObject and put in the message object + message.javaClass.declaredFields.forEach { field -> + field.isAccessible = true + val fieldName = field.name + val fieldValue = messageJsonObject[fieldName] + if (fieldValue != null) { + field.set(message, context.gson.fromJson(fieldValue, field.type)) + } + } + + removedMessages.add(messageId) + return@hookConstructor + } + + if (!messageCache.containsKey(messageId)) { + val serializedMessage = context.gson.toJson(message) + messageCache[messageId] = serializedMessage + context.bridgeClient.addMessageLoggerMessage(messageId, serializedMessage.toByteArray(Charsets.UTF_8)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventReadReceipts.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventReadReceipts.kt new file mode 100644 index 00000000..47285ebb --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventReadReceipts.kt @@ -0,0 +1,28 @@ +package me.rhunk.snapenhance.features.impl.spy + +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker + +class PreventReadReceipts : Feature("PreventReadReceipts", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + override fun onActivityCreate() { + val isConversationInStealthMode: (SnapUUID) -> Boolean = hook@{ + if (context.config.bool(ConfigProperty.PREVENT_READ_RECEIPTS)) return@hook true + context.feature(StealthMode::class).isStealth(it.toString()) + } + + arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String -> + Hooker.hook(context.classCache.conversationManager, methodName, HookStage.BEFORE, { isConversationInStealthMode(SnapUUID(it.arg(0))) }) { + it.setResult(null) + } + } + Hooker.hook(context.classCache.snapManager, "onSnapInteraction", HookStage.BEFORE) { + if (isConversationInStealthMode(SnapUUID(it.arg(1) as Any))) { + it.setResult(null) + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventStatusNotifications.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventStatusNotifications.kt new file mode 100644 index 00000000..d30f239a --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/PreventStatusNotifications.kt @@ -0,0 +1,28 @@ +package me.rhunk.snapenhance.features.impl.spy + +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.util.getObjectField + + +class PreventStatusNotifications : Feature("PreventStatusNotifications", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { + Hooker.hook( + context.classCache.conversationManager, + "sendMessageWithContent", + HookStage.BEFORE, + {context.config.bool(ConfigProperty.PREVENT_STATUS_NOTIFICATIONS) }) { param -> + val contentTypeString = (param.arg(1) as Any).getObjectField("mContentType") + + if (contentTypeString == ContentType.STATUS_SAVE_TO_CAMERA_ROLL.name || + contentTypeString == ContentType.STATUS_CONVERSATION_CAPTURE_SCREENSHOT.name || + contentTypeString == ContentType.STATUS_CONVERSATION_CAPTURE_RECORD.name) { + param.setResult(null) + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/StealthMode.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/StealthMode.kt new file mode 100644 index 00000000..a2bc6ee4 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spy/StealthMode.kt @@ -0,0 +1,62 @@ +package me.rhunk.snapenhance.features.impl.spy + +import me.rhunk.snapenhance.bridge.common.impl.FileAccessRequest +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets + + +class StealthMode : Feature("StealthMode", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + private val stealthConversations = mutableListOf() + + override fun onActivityCreate() { + readStealthFile() + } + + private fun writeStealthFile() { + val sb = StringBuilder() + for (stealthConversation in stealthConversations) { + sb.append(stealthConversation).append("\n") + } + context.bridgeClient.writeFile( + FileAccessRequest.FileType.STEALTH, + sb.toString().toByteArray(StandardCharsets.UTF_8) + ) + } + + private fun readStealthFile() { + val conversations = mutableListOf() + val stealthFileData: ByteArray = context.bridgeClient.createAndReadFile(FileAccessRequest.FileType.STEALTH, ByteArray(0)) + //read conversations + with(BufferedReader(InputStreamReader( + ByteArrayInputStream(stealthFileData), + StandardCharsets.UTF_8 + ))) { + var line: String = "" + while (readLine()?.also { line = it } != null) { + conversations.add(line) + } + close() + } + stealthConversations.clear() + stealthConversations.addAll(conversations) + } + + fun setStealth(conversationId: String, stealth: Boolean) { + conversationId.hashCode().toLong().toString(16).let { + if (stealth) { + stealthConversations.add(it) + } else { + stealthConversations.remove(it) + } + } + writeStealthFile() + } + + fun isStealth(conversationId: String): Boolean { + return stealthConversations.contains(conversationId.hashCode().toLong().toString(16)) + } +} 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 new file mode 100644 index 00000000..c887f951 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt @@ -0,0 +1,61 @@ +package me.rhunk.snapenhance.features.impl.ui + +import android.view.View +import android.view.ViewGroup +import me.rhunk.snapenhance.Constants +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 UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + override fun onActivityCreate() { + val resources = context.resources + + val callButtonsStub = resources.getIdentifier("call_buttons_stub", "id", Constants.SNAPCHAT_PACKAGE_NAME) + val callButton1 = resources.getIdentifier("friend_action_button3", "id", Constants.SNAPCHAT_PACKAGE_NAME) + val callButton2 = resources.getIdentifier("friend_action_button4", "id", Constants.SNAPCHAT_PACKAGE_NAME) + + 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) + + 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)) { + methodParam.setArg(0, View.GONE) + } + } + + //TODO: use the event bus to dispatch a addView event + val addViewMethod = ViewGroup::class.java.getMethod( + "addView", + View::class.java, + Int::class.javaPrimitiveType, + ViewGroup.LayoutParams::class.java + ) + Hooker.hook(addViewMethod, HookStage.BEFORE) { param -> + val view: View = param.arg(0) + val viewId = view.id + + if (chatInputBarCognac == viewId && context.config.bool(ConfigProperty.REMOVE_COGNAC_BUTTON)) { + view.visibility = View.GONE + } + if (chatInputBarSticker == viewId && context.config.bool(ConfigProperty.REMOVE_STICKERS_BUTTON)) { + view.visibility = View.GONE + } + if (context.config.bool(ConfigProperty.REMOVE_CALLBUTTONS)) { + if (viewId == callButton1 || viewId == callButton2) { + if (view.visibility == View.GONE) return@hook + Hooker.ephemeralHookObjectMethod(View::class.java, view, "setVisibility", HookStage.BEFORE) { param -> + param.setArg(0, View.GONE) + } + } + if (viewId == callButtonsStub) { + param.setResult(null) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/AbstractMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/AbstractMenu.kt new file mode 100644 index 00000000..a00abe23 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/AbstractMenu.kt @@ -0,0 +1,9 @@ +package me.rhunk.snapenhance.features.impl.ui.menus + +import me.rhunk.snapenhance.ModContext + +abstract class AbstractMenu() { + lateinit var context: ModContext + + open fun init() {} +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/MenuViewInjector.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/MenuViewInjector.kt new file mode 100644 index 00000000..9eef82e3 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/MenuViewInjector.kt @@ -0,0 +1,139 @@ +package me.rhunk.snapenhance.features.impl.ui.menus + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import de.robv.android.xposed.XposedBridge +import me.rhunk.snapenhance.Constants.VIEW_DRAWER +import me.rhunk.snapenhance.Constants.VIEW_INJECTED_CODE +import me.rhunk.snapenhance.config.ConfigProperty +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.ui.menus.impl.ChatActionMenu +import me.rhunk.snapenhance.features.impl.ui.menus.impl.FriendFeedInfoMenu +import me.rhunk.snapenhance.features.impl.ui.menus.impl.OperaContextActionMenu +import me.rhunk.snapenhance.features.impl.ui.menus.impl.SettingsMenu +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import java.lang.reflect.Field +import java.lang.reflect.Modifier + +class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + private val friendFeedInfoMenu = FriendFeedInfoMenu() + private val operaContextActionMenu = OperaContextActionMenu() + private val chatActionMenu = ChatActionMenu() + private val settingMenu = SettingsMenu() + + private fun wasInjectedView(view: View): Boolean { + if (view.getTag(VIEW_INJECTED_CODE) != null) return true + view.setTag(VIEW_INJECTED_CODE, true) + return false + } + + @SuppressLint("ResourceType") + override fun asyncOnActivityCreate() { + friendFeedInfoMenu.context = context + operaContextActionMenu.context = context + chatActionMenu.context = context + settingMenu.context = context + + val addViewMethod = ViewGroup::class.java.getMethod( + "addView", + View::class.java, + Int::class.javaPrimitiveType, + ViewGroup.LayoutParams::class.java + ) + + //catch the card view instance in the action drawer + Hooker.hook( + LinearLayout::class.java.getConstructor( + Context::class.java, + AttributeSet::class.java, + Int::class.javaPrimitiveType + ), HookStage.AFTER + ) { param -> + val viewGroup: LinearLayout = param.thisObject() + val attribute: Int = param.arg(2) + if (attribute == 0) return@hook + val resourceName = viewGroup.resources.getResourceName(attribute) + if (!resourceName.endsWith("snapCardContentLayoutStyle")) return@hook + viewGroup.setTag(VIEW_DRAWER, Any()) + } + + Hooker.hook(addViewMethod, HookStage.BEFORE) { param -> + val viewGroup: ViewGroup = param.thisObject() + val originalAddView: (View) -> Unit = { view: View -> + XposedBridge.invokeOriginalMethod( + addViewMethod, + viewGroup, + arrayOf( + view, + -1, + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + ) + } + + val childView: View = param.arg(0) + operaContextActionMenu.inject(viewGroup, childView) + + //download in chat snaps and notes from the chat action menu + if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer")) { + if (viewGroup.parent == null || viewGroup.parent + .parent == null + ) return@hook + chatActionMenu.inject(viewGroup) + return@hook + } + + //TODO : preview group chats + if (viewGroup !is LinearLayout) return@hook + if (viewGroup.getTag(VIEW_DRAWER) == null) return@hook + val itemStringInterface =childView.javaClass.declaredFields.filter { field: Field -> + !field.type.isPrimitive && Modifier.isAbstract( + field.type.modifiers + ) + } + .map { field: Field -> + try { + field.isAccessible = true + return@map field[childView] + } catch (e: IllegalAccessException) { + e.printStackTrace() + } + null + }.firstOrNull() + + //the 3 dot button shows a menu which contains the first item as a Plain object + //FIXME: better way to detect the 3 dot button + if (viewGroup.getChildCount() == 0 && itemStringInterface != null && itemStringInterface.toString().startsWith("Plain(primaryText=")) { + if (wasInjectedView(viewGroup)) return@hook + + settingMenu.inject(viewGroup, originalAddView) + viewGroup.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View?) {} + override fun onViewDetachedFromWindow(v: View?) { + context.config.writeConfig() + } + }) + return@hook + } + if (context.feature(Messaging::class).lastFetchConversationUserUUID == null) return@hook + + //filter by the slot index + if (viewGroup.getChildCount() != context.config.int(ConfigProperty.MENU_SLOT_ID)) return@hook + + friendFeedInfoMenu.inject(viewGroup, originalAddView) + childView.setTag(VIEW_DRAWER, null) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/ViewAppearanceHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/ViewAppearanceHelper.kt new file mode 100644 index 00000000..93e86a41 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/ViewAppearanceHelper.kt @@ -0,0 +1,53 @@ +package me.rhunk.snapenhance.features.impl.ui.menus + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.Typeface +import android.view.Gravity +import android.view.View +import android.widget.Switch +import android.widget.TextView + +object ViewAppearanceHelper { + fun applyIndentation(view: TextView) { + view.setPadding(70, 0, 55, 0) + } + + @SuppressLint("UseSwitchCompatOrMaterialCode", "RtlHardcoded") + fun applyTheme(viewModel: View, view: TextView) { + //remove the shadow + view.setBackgroundColor(0x00000000) + view.setTextColor(Color.parseColor("#000000")) + view.setShadowLayer(0f, 0f, 0f, 0) + view.outlineProvider = null + view.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL + view.width = viewModel.width + //FIXME: hardcoded dimensions + view.height = 160 + view.setPadding(35, 0, 55, 0) + view.isAllCaps = false + view.textSize = 15f + view.typeface = Typeface.DEFAULT + + //remove click effect + if (view.javaClass == TextView::class.java) { + view.setBackgroundColor(0) + } + if (view is Switch) { + //set the switch color to blue + val colorStateList = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_checked), intArrayOf( + android.R.attr.state_checked + ) + ), intArrayOf( + Color.parseColor("#000000"), + Color.parseColor("#2196F3") + ) + ) + view.trackTintList = colorStateList + view.thumbTintList = colorStateList + } + } +} 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 new file mode 100644 index 00000000..b4bd0317 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/ChatActionMenu.kt @@ -0,0 +1,91 @@ +package me.rhunk.snapenhance.features.impl.ui.menus.impl + +import android.annotation.SuppressLint +import android.content.res.Resources +import android.graphics.BlendMode +import android.graphics.BlendModeColorFilter +import android.graphics.Color +import android.os.SystemClock +import android.util.TypedValue +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.widget.Button +import me.rhunk.snapenhance.Constants.VIEW_INJECTED_CODE +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader +import me.rhunk.snapenhance.features.impl.ui.menus.AbstractMenu + + +class ChatActionMenu : AbstractMenu() { + private fun wasInjectedView(view: View): Boolean { + if (view.getTag(VIEW_INJECTED_CODE) != null) return true + view.setTag(VIEW_INJECTED_CODE, true) + return false + } + + private fun applyButtonTheme(parent: View, button: Button) { + button.background.colorFilter = BlendModeColorFilter(Color.WHITE, BlendMode.SRC_ATOP) + button.setTextColor(Color.BLACK) + button.transformationMethod = null + val margin = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 20f, + Resources.getSystem().displayMetrics + ).toInt() + val params = MarginLayoutParams(parent.layoutParams) + params.setMargins(margin, 5, margin, 5) + params.marginEnd = margin + params.marginStart = margin + button.layoutParams = params + button.height = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 50f, + Resources.getSystem().displayMetrics + ).toInt() + } + + @SuppressLint("SetTextI18n") + fun inject(viewGroup: ViewGroup) { + val parent = viewGroup.parent.parent as ViewGroup + if (wasInjectedView(parent)) return + //close the action menu using a touch event + val closeActionMenu = { + viewGroup.dispatchTouchEvent( + MotionEvent.obtain( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_DOWN, + 0f, + 0f, + 0 + ) + ) + } + 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) + } + + //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) + } + + //TODO: delete logged message button + } +} 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 new file mode 100644 index 00000000..d1335cad --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/FriendFeedInfoMenu.kt @@ -0,0 +1,231 @@ +package me.rhunk.snapenhance.features.impl.ui.menus.impl + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.content.Context +import android.content.DialogInterface +import android.content.res.Resources +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.Button +import android.widget.CompoundButton +import android.widget.Switch +import android.widget.Toast +import de.robv.android.xposed.XposedBridge +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.database.objects.ConversationMessage +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.spy.StealthMode +import me.rhunk.snapenhance.features.impl.ui.menus.AbstractMenu +import me.rhunk.snapenhance.features.impl.ui.menus.ViewAppearanceHelper.applyTheme +import java.net.HttpURLConnection +import java.net.URL +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +class FriendFeedInfoMenu : AbstractMenu() { + private fun getImageDrawable(url: String): Drawable { + val connection = URL(url).openConnection() as HttpURLConnection + connection.connect() + val input = connection.inputStream + return BitmapDrawable(Resources.getSystem(), BitmapFactory.decodeStream(input)) + } + + private fun formatDate(timestamp: Long): String? { + return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).format(Date(timestamp)) + } + + fun showProfileInfo(profile: FriendInfo) { + var icon: Drawable? = null + try { + if (profile.bitmojiSelfieId != null && profile.bitmojiAvatarId != null) { + icon = getImageDrawable( + "https://sdk.bitmoji.com/render/panel/" + profile.bitmojiSelfieId + .toString() + "-" + profile.bitmojiAvatarId + .toString() + "-v1.webp?transparent=1&scale=0" + ) + } + } catch (e: Throwable) { + Logger.xposedLog(e) + } + val finalIcon = icon + context.runOnUiThread { + val addedTimestamp: Long = profile.addedTimestamp.coerceAtLeast(profile.reverseAddedTimestamp) + val builder = AlertDialog.Builder(context.mainActivity) + builder.setIcon(finalIcon) + builder.setTitle(profile.displayName) + val birthday = Calendar.getInstance() + birthday[Calendar.MONTH] = (profile.birthday shr 32).toInt() - 1 + val message: String = """ + ${context.translation.get("info.username")}: ${profile.username} + ${context.translation.get("info.display_name")}: ${profile.displayName} + ${context.translation.get("info.added_date")}: ${formatDate(addedTimestamp)} + ${birthday.getDisplayName( + Calendar.MONTH, + Calendar.LONG, + context.translation.getLocale() + )?.let { + context.translation.get("info.birthday") + .replace("{month}", it) + .replace("{day}", profile.birthday.toInt().toString()) + } + } + """.trimIndent() + builder.setMessage(message) + builder.setPositiveButton( + "OK" + ) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + builder.show() + } + } + + fun showPreview(userId: String?, conversationId: String, androidCtx: Context?) { + //query message + val messages: List? = context.database.getMessagesFromConversationId( + conversationId, + context.config.int(ConfigProperty.MESSAGE_PREVIEW_LENGTH) + )?.reversed() + + if (messages == null || messages.isEmpty()) { + Toast.makeText(androidCtx, "No messages found", Toast.LENGTH_SHORT).show() + return + } + val participants: Map = context.database.getConversationParticipants(conversationId)!! + .map { context.database.getFriendInfo(it)!! } + .associateBy { it.userId!! } + + val messageBuilder = StringBuilder() + + messages.forEach{ message: ConversationMessage -> + val sender: FriendInfo? = participants[message.sender_id] + + var messageString: String = message.getMessageAsString() ?: ContentType.fromId(message.content_type).name + + if (message.content_type == ContentType.SNAP.id) { + val readTimeStamp: Long = message.read_timestamp + messageString = "\uD83D\uDFE5" //red square + if (readTimeStamp > 0) { + messageString += " \uD83D\uDC40 " //eyes + messageString += DateFormat.getDateTimeInstance( + DateFormat.SHORT, + DateFormat.SHORT + ).format(Date(readTimeStamp)) + } + } + + var displayUsername = sender?.displayName ?: "Unknown user" + + if (displayUsername.length > 12) { + displayUsername = displayUsername.substring(0, 13) + "... " + } + + messageBuilder.append(displayUsername).append(": ").append(messageString).append("\n") + } + + val targetPerson: FriendInfo? = + if (userId == null) null else participants[userId] + + targetPerson?.let { + val timeSecondDiff = ((it.streakExpirationTimestamp - System.currentTimeMillis()) / 1000 / 60).toInt() + messageBuilder.append("\n\n") + .append("\uD83D\uDD25 ") //fire emoji + .append(context.translation.get("streak_expiration").format( + timeSecondDiff / 60 / 24, + timeSecondDiff / 60 % 24, + timeSecondDiff % 60 + )) + } + + //alert dialog + val builder = AlertDialog.Builder(context.mainActivity) + builder.setTitle(context.translation.get("preview")) + builder.setMessage(messageBuilder.toString()) + builder.setPositiveButton( + "OK" + ) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + targetPerson?.let { + builder.setNegativeButton(context.translation.get("profile_info")) {_, _ -> + context.executeAsync { + showProfileInfo(it) + } + } + } + builder.show() + } + + @SuppressLint("SetTextI18n", "UseSwitchCompatOrMaterialCode", "DefaultLocale") + fun inject(viewModel: View, viewConsumer: ((View) -> Unit)) { + val messaging = context.feature(Messaging::class) + var focusedConversationTargetUser: String? = null + val conversationId: String + if (messaging.lastFetchConversationUserUUID != null) { + focusedConversationTargetUser = messaging.lastFetchConversationUserUUID.toString() + val conversation: UserConversationLink = context.database.getDMConversationIdFromUserId(focusedConversationTargetUser) ?: return + conversationId = conversation.client_conversation_id!!.trim().lowercase() + } else { + conversationId = messaging.lastFetchConversationUUID.toString() + } + + //preview button + val previewButton = Button(viewModel.context) + previewButton.text = context.translation.get("preview") + applyTheme(viewModel, previewButton) + val finalFocusedConversationTargetUser = focusedConversationTargetUser + previewButton.setOnClickListener { v: View? -> + showPreview( + finalFocusedConversationTargetUser, + conversationId, + previewButton.context + ) + } + + //export conversation + /*val exportButton = Button(viewModel.context) + exportButton.setText(context.translation.get("conversation_export")) + applyTheme(viewModel, exportButton) + exportButton.setOnClickListener { event: View? -> + conversationExport.exportConversation( + SnapUUID(conversationId) + ) + }*/ + + //stealth switch + val stealthSwitch = Switch(viewModel.context) + stealthSwitch.text = context.translation.get("stealth_mode") + stealthSwitch.isChecked = context.feature(StealthMode::class).isStealth(conversationId) + applyTheme(viewModel, stealthSwitch) + stealthSwitch.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + context.feature(StealthMode::class).setStealth( + conversationId, + isChecked + ) + } + + /*//click to delete switch + val clickToDeleteSwitch = Switch(viewModel.context) + clickToDeleteSwitch.setText(context.translation.get("click_to_delete")) + clickToDeleteSwitch.isChecked = clickToDelete.isClickToDelete(conversationId) + applyTheme(viewModel, clickToDeleteSwitch) + clickToDeleteSwitch.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> + clickToDelete.setClickToDelete( + conversationId, + isChecked + ) + }*/ + /* if (configManager.getBoolean(ConfigCategory.EXTRAS, "conversation_export") + .isState() + ) viewConsumer.accept(exportButton) + if (configManager.getBoolean(ConfigCategory.PRIVACY, "click_to_delete") + .isState() + ) viewConsumer.accept(clickToDeleteSwitch)*/ + viewConsumer(stealthSwitch) + viewConsumer(previewButton) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/OperaContextActionMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/OperaContextActionMenu.kt new file mode 100644 index 00000000..ef4dbf25 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/OperaContextActionMenu.kt @@ -0,0 +1,82 @@ +package me.rhunk.snapenhance.features.impl.ui.menus.impl + +import android.annotation.SuppressLint +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.LinearLayout +import android.widget.ScrollView +import de.robv.android.xposed.XposedBridge +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader +import me.rhunk.snapenhance.features.impl.ui.menus.AbstractMenu +import me.rhunk.snapenhance.features.impl.ui.menus.ViewAppearanceHelper.applyTheme + +class OperaContextActionMenu : AbstractMenu() { + private val contextCardsScrollView by lazy { + context.resources.getIdentifier("context_cards_scroll_view", "id", Constants.SNAPCHAT_PACKAGE_NAME) + } + + /* + LinearLayout : + - LinearLayout: + - SnapFontTextView + - ImageView + - LinearLayout: + - SnapFontTextView + - ImageView + - LinearLayout: + - SnapFontTextView + - ImageView + */ + private fun isViewGroupButtonMenuContainer(viewGroup: ViewGroup): Boolean { + if (viewGroup !is LinearLayout) return false + val children = ArrayList() + for (i in 0 until viewGroup.getChildCount()) + children.add(viewGroup.getChildAt(i)) + return if (children.any { view: View? -> view !is LinearLayout }) + false + else children.map { view: View -> view as LinearLayout } + .any { linearLayout: LinearLayout -> + val viewChildren = ArrayList() + for (i in 0 until linearLayout.childCount) viewChildren.add( + linearLayout.getChildAt( + i + ) + ) + viewChildren.any { viewChild: View -> + viewChild.javaClass.name.endsWith("SnapFontTextView") + } + } + } + + @SuppressLint("SetTextI18n") + fun inject(viewGroup: ViewGroup, childView: View) { + try { + if (viewGroup.parent !is ScrollView) return + val parent = viewGroup.parent as ScrollView + if (parent.id != contextCardsScrollView) return + if (childView !is LinearLayout) return + if (!isViewGroupButtonMenuContainer(childView as ViewGroup)) return + + val linearLayout = LinearLayout(childView.getContext()) + linearLayout.orientation = LinearLayout.VERTICAL + linearLayout.gravity = Gravity.CENTER + linearLayout.layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + val button = Button(childView.getContext()) + button.text = context.translation.get("download_opera") + button.setOnClickListener { context.feature(MediaDownloader::class).downloadLastOperaMediaAsync() } + applyTheme(linearLayout, button) + linearLayout.addView(button) + (childView as ViewGroup).addView(linearLayout, 0) + } catch (e: Throwable) { + Logger.xposedLog(e) + } + } +} 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 new file mode 100644 index 00000000..073f3e57 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/menus/impl/SettingsMenu.kt @@ -0,0 +1,133 @@ +package me.rhunk.snapenhance.features.impl.ui.menus.impl + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.text.InputType +import android.view.View +import android.widget.Button +import android.widget.EditText +import android.widget.Switch +import android.widget.TextView +import me.rhunk.snapenhance.BuildConfig +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.features.impl.ui.menus.AbstractMenu +import me.rhunk.snapenhance.features.impl.ui.menus.ViewAppearanceHelper + +class SettingsMenu : AbstractMenu() { + private fun createCategoryTitle(viewModel: View, key: String): TextView { + val categoryText = TextView(viewModel.context) + categoryText.text = context.translation.get(key) + ViewAppearanceHelper.applyTheme(viewModel, categoryText) + categoryText.textSize = 18f + return categoryText + } + + @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" + } + + val textEditor: ((String) -> Unit) -> Unit = { updateValue -> + val builder = AlertDialog.Builder(viewModel.context) + builder.setTitle(context.translation.get(property.nameKey)) + + val input = EditText(viewModel.context) + input.inputType = InputType.TYPE_CLASS_TEXT + input.setText(context.config.string(property)) + + builder.setView(input) + builder.setPositiveButton("OK") { _, _ -> + updateValue(input.text.toString()) + } + + builder.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } + builder.show() + } + + val resultView: View = when (property.defaultValue) { + is String -> { + val textView = TextView(viewModel.context) + updateButtonText(textView, context.config.string(property)) + ViewAppearanceHelper.applyTheme(viewModel, textView) + textView.setOnClickListener { + textEditor { value -> + context.config.set(property, value) + updateButtonText(textView, value) + } + } + textView + } + is Number -> { + val button = Button(viewModel.context) + updateButtonText(button, context.config.get(property).toString()) + button.setOnClickListener { + textEditor { value -> + runCatching { + context.config.set(property, when (property.defaultValue) { + is Int -> value.toInt() + is Double -> value.toDouble() + is Float -> value.toFloat() + is Long -> value.toLong() + is Short -> value.toShort() + is Byte -> value.toByte() + else -> throw IllegalArgumentException() + }) + updateButtonText(button, value) + }.onFailure { + context.shortToast("Invalid value") + } + } + } + ViewAppearanceHelper.applyTheme(viewModel, button) + button + } + is Boolean -> { + val switch = Switch(viewModel.context) + switch.text = context.translation.get(property.nameKey) + switch.isChecked = context.config.bool(property) + switch.setOnCheckedChangeListener { _, isChecked -> + context.config.set(property, isChecked) + } + ViewAppearanceHelper.applyTheme(viewModel, switch) + switch + } + else -> { + TextView(viewModel.context) + } + } + return resultView + } + + @SuppressLint("SetTextI18n") + fun inject(viewModel: View, addView: (View) -> Unit) { + val packageInfo = viewModel.context.packageManager.getPackageInfo( + Constants.SNAPCHAT_PACKAGE_NAME, + 0 + ) + val versionTextBuilder = StringBuilder() + versionTextBuilder.append("SnapEnhance ").append(BuildConfig.VERSION_NAME) + .append(" by rhunk") + if (BuildConfig.DEBUG) { + versionTextBuilder.append("\n").append("Snapchat ").append(packageInfo.versionName) + .append(" (").append(packageInfo.longVersionCode).append(")") + } + val titleText = TextView(viewModel.context) + titleText.text = versionTextBuilder.toString() + ViewAppearanceHelper.applyTheme(viewModel, titleText) + titleText.textSize = 18f + titleText.minHeight = 80 * versionTextBuilder.chars().filter { ch: Int -> ch == '\n'.code } + .count().coerceAtLeast(2).toInt() + addView(titleText) + + context.config.entries().groupBy { + it.key.category + }.forEach { (category, value) -> + addView(createCategoryTitle(viewModel, category.key)) + value.forEach { + addView(createPropertyView(viewModel, it.key)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt b/app/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt new file mode 100644 index 00000000..2765b6ee --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt @@ -0,0 +1,72 @@ +package me.rhunk.snapenhance.hook + +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XposedBridge +import java.lang.reflect.Member +import java.util.function.Consumer + +@Suppress("UNCHECKED_CAST") +class HookAdapter( + private val methodHookParam: XC_MethodHook.MethodHookParam<*> +) { + fun thisObject(): T { + return methodHookParam.thisObject as T + } + + fun method(): Member { + return methodHookParam.method + } + + fun arg(index: Int): T { + return methodHookParam.args[index] as T + } + + fun argNullable(index: Int): T? { + return methodHookParam.args[index] as T? + } + + fun setArg(index: Int, value: Any?) { + if (index < 0 || index >= methodHookParam.args.size) return + methodHookParam.args[index] = value + } + + fun args(): Array { + return methodHookParam.args + } + + fun getResult(): Any? { + return methodHookParam.result + } + + fun setResult(result: Any?) { + methodHookParam.result = result + } + + fun setThrowable(throwable: Throwable) { + methodHookParam.throwable = throwable + } + + fun throwable(): Throwable? { + return methodHookParam.throwable + } + + fun invokeOriginal(): Any? { + return XposedBridge.invokeOriginalMethod(method(), thisObject(), args()) + } + + fun invokeOriginal(args: Array): Any? { + return XposedBridge.invokeOriginalMethod(method(), thisObject(), args) + } + + fun invokeOriginalSafe(errorCallback: Consumer) { + invokeOriginalSafe(args(), errorCallback) + } + + fun invokeOriginalSafe(args: Array, errorCallback: Consumer) { + runCatching { + setResult(XposedBridge.invokeOriginalMethod(method(), thisObject(), args)) + }.onFailure { + errorCallback.accept(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/hook/HookStage.kt b/app/src/main/kotlin/me/rhunk/snapenhance/hook/HookStage.kt new file mode 100644 index 00000000..dddf1342 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/hook/HookStage.kt @@ -0,0 +1,6 @@ +package me.rhunk.snapenhance.hook + +enum class HookStage { + BEFORE, + AFTER +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt b/app/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt new file mode 100644 index 00000000..92f558e4 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt @@ -0,0 +1,94 @@ +package me.rhunk.snapenhance.hook + +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XposedBridge +import java.lang.reflect.Member + +object Hooker { + private fun newMethodHook( + stage: HookStage, + consumer: (HookAdapter) -> Unit, + filter: ((HookAdapter) -> Boolean) = { true } + ) = object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam<*>) { + if (stage != HookStage.BEFORE) return + with(HookAdapter(param)) { + if (!filter(this)) return + consumer(this) + } + } + + override fun afterHookedMethod(param: MethodHookParam<*>) { + if (stage != HookStage.AFTER) return + with(HookAdapter(param)) { + if (!filter(this)) return + consumer(this) + } + } + } + + fun hook( + clazz: Class<*>, + methodName: String, + stage: HookStage, + consumer: (HookAdapter) -> Unit + ): Set = XposedBridge.hookAllMethods(clazz, methodName, newMethodHook(stage, consumer)) + + fun hook( + clazz: Class<*>, + methodName: String, + stage: HookStage, + filter: (HookAdapter) -> Boolean, + consumer: (HookAdapter) -> Unit + ): Set = XposedBridge.hookAllMethods(clazz, methodName, newMethodHook(stage, consumer, filter)) + + fun hook( + member: Member, + stage: HookStage, + consumer: (HookAdapter) -> Unit + ): XC_MethodHook.Unhook { + return XposedBridge.hookMethod(member, newMethodHook(stage, consumer)) + } + + fun hook( + member: Member, + stage: HookStage, + filter: ((HookAdapter) -> Boolean), + consumer: (HookAdapter) -> Unit + ): XC_MethodHook.Unhook { + return XposedBridge.hookMethod(member, newMethodHook(stage, consumer, filter)) + } + + + fun hookConstructor( + clazz: Class<*>, + stage: HookStage, + consumer: (HookAdapter) -> Unit + ) { + XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer)) + } + + fun hookConstructor( + clazz: Class<*>, + stage: HookStage, + filter: ((HookAdapter) -> Boolean), + consumer: (HookAdapter) -> Unit + ) { + XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer, filter)) + } + + fun ephemeralHookObjectMethod( + clazz: Class<*>, + instance: Any, + methodName: String, + stage: HookStage, + hookConsumer: (HookAdapter) -> Unit + ) { + val unhooks: MutableSet = HashSet() + hook(clazz, methodName, stage) { param-> + if (param.thisObject() != instance) return@hook + hookConsumer(param) + unhooks.forEach{ it.unhook() } + }.also { unhooks.addAll(it) } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Manager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Manager.kt new file mode 100644 index 00000000..ba244213 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Manager.kt @@ -0,0 +1,6 @@ +package me.rhunk.snapenhance.manager + +interface Manager { + fun init() {} + fun onActivityCreate() {} +} \ No newline at end of file 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 new file mode 100644 index 00000000..dbd8540a --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt @@ -0,0 +1,63 @@ +package me.rhunk.snapenhance.manager.impl + +import com.google.gson.JsonObject +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.bridge.common.impl.FileAccessRequest +import me.rhunk.snapenhance.config.ConfigAccessor +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.manager.Manager +import java.nio.charset.StandardCharsets + +class ConfigManager( + private val context: ModContext, + config: MutableMap = mutableMapOf() +) : ConfigAccessor(config), Manager { + + private val propertyList = ConfigProperty.sortedByCategory() + + override fun init() { + //generate default config + propertyList.forEach { key -> + set(key, key.defaultValue) + } + + if (!context.bridgeClient.isFileExists(FileAccessRequest.FileType.CONFIG)) { + writeConfig() + return + } + + runCatching { + loadConfig() + }.onFailure { + Logger.xposedLog("Failed to load config", it) + writeConfig() + } + } + + private fun loadConfig() { + val configContent = context.bridgeClient.createAndReadFile( + FileAccessRequest.FileType.CONFIG, + "{}".toByteArray(Charsets.UTF_8) + ) + val configObject: JsonObject = context.gson.fromJson( + String(configContent, StandardCharsets.UTF_8), + JsonObject::class.java + ) + propertyList.forEach { key -> + val value = context.gson.fromJson(configObject.get(key.name), key.defaultValue.javaClass) ?: key.defaultValue + set(key, value) + } + } + + fun writeConfig() { + val configObject = JsonObject() + propertyList.forEach { key -> + configObject.add(key.name, context.gson.toJsonTree(get(key))) + } + context.bridgeClient.writeFile( + FileAccessRequest.FileType.CONFIG, + context.gson.toJson(configObject).toByteArray(Charsets.UTF_8) + ) + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..83f3b979 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -0,0 +1,93 @@ +package me.rhunk.snapenhance.manager.impl + +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.features.impl.ConfigEnumKeys +import me.rhunk.snapenhance.features.impl.Messaging +import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader +import me.rhunk.snapenhance.features.impl.extras.AutoSave +import me.rhunk.snapenhance.features.impl.extras.Notifications +import me.rhunk.snapenhance.features.impl.extras.SnapchatPlus +import me.rhunk.snapenhance.features.impl.privacy.DisableMetrics +import me.rhunk.snapenhance.features.impl.privacy.PreventScreenshotDetections +import me.rhunk.snapenhance.features.impl.spy.* +import me.rhunk.snapenhance.features.impl.ui.UITweaks +import me.rhunk.snapenhance.features.impl.ui.menus.MenuViewInjector +import me.rhunk.snapenhance.manager.Manager +import java.util.concurrent.Executors +import kotlin.reflect.KClass + +class FeatureManager(private val context: ModContext) : Manager { + private val asyncLoadExecutorService = Executors.newCachedThreadPool() + private val features = mutableListOf() + + private fun register(featureClass: KClass) { + runCatching { + with(featureClass.java.newInstance()) { + if (loadParams and FeatureLoadParams.NO_INIT != 0) return@with + context = this@FeatureManager.context + features.add(this) + } + }.onFailure { + Logger.xposedLog("Failed to register feature ${featureClass.simpleName}", it) + } + } + + @Suppress("UNCHECKED_CAST") + fun get(featureClass: KClass): T? { + return features.find { it::class == featureClass } as? T + } + + override fun init() { + register(Messaging::class) + register(MediaDownloader::class) + register(StealthMode::class) + register(MenuViewInjector::class) + register(PreventReadReceipts::class) + register(AnonymousStoryViewing::class) + register(MessageLogger::class) + register(SnapchatPlus::class) + register(DisableMetrics::class) + register(PreventScreenshotDetections::class) + register(PreventStatusNotifications::class) + register(Notifications::class) + register(AutoSave::class) + register(UITweaks::class) + register(ConfigEnumKeys::class) + + initializeFeatures() + } + + private fun featureInitializer(isAsync: Boolean, param: Int, action: (Feature) -> Unit) { + features.forEach { feature -> + if (feature.loadParams and param == 0) return@forEach + val callback = { + runCatching { + action(feature) + }.onFailure { + Logger.xposedLog("Failed to init feature ${feature.nameKey}", it) + } + } + if (!isAsync) { + callback() + return@forEach + } + asyncLoadExecutorService.submit { + callback() + } + } + } + + private fun initializeFeatures() { + //TODO: async called when all features are initiated ? + featureInitializer(false, FeatureLoadParams.INIT_SYNC) { it.init() } + featureInitializer(true, FeatureLoadParams.INIT_ASYNC) { it.asyncInit() } + } + + override fun onActivityCreate() { + featureInitializer(false, FeatureLoadParams.ACTIVITY_CREATE_SYNC) { it.onActivityCreate() } + featureInitializer(true, FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { it.asyncOnActivityCreate() } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt new file mode 100644 index 00000000..f49e10fe --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt @@ -0,0 +1,178 @@ +package me.rhunk.snapenhance.manager.impl + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import dalvik.system.DexFile +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.bridge.common.impl.FileAccessRequest +import me.rhunk.snapenhance.manager.Manager +import me.rhunk.snapenhance.mapping.Mapper +import me.rhunk.snapenhance.mapping.impl.CallbackMapper +import me.rhunk.snapenhance.mapping.impl.EnumMapper +import me.rhunk.snapenhance.mapping.impl.OperaPageViewControllerMapper +import me.rhunk.snapenhance.mapping.impl.PlusSubscriptionMapper +import me.rhunk.snapenhance.util.getObjectField +import java.io.FileNotFoundException +import java.nio.charset.StandardCharsets +import java.util.concurrent.ConcurrentHashMap + +@Suppress("UNCHECKED_CAST") +class MappingManager(private val context: ModContext) : Manager { + private val mappers = mutableListOf().apply { + add(CallbackMapper()) + add(EnumMapper()) + add(OperaPageViewControllerMapper()) + add(PlusSubscriptionMapper()) + } + + private val mappings = ConcurrentHashMap() + private var snapBuildNumber = 0 + + override fun init() { + val currentBuildNumber = context.androidContext.packageManager.getPackageInfo( + Constants.SNAPCHAT_PACKAGE_NAME, + 0 + ).longVersionCode.toInt() + snapBuildNumber = currentBuildNumber + + if (context.bridgeClient.isFileExists(FileAccessRequest.FileType.MAPPINGS)) { + runCatching { + loadCached() + }.onFailure { + if (it is FileNotFoundException) { + Logger.xposedLog(it) + context.forceCloseApp() + } + Logger.error("Failed to load cached mappings", it) + } + return + } + refresh() + } + + private fun loadCached() { + if (!context.bridgeClient.isFileExists(FileAccessRequest.FileType.MAPPINGS)) { + Logger.xposedLog("Mappings file does not exist") + return + } + val mappingsObject = JsonParser.parseString( + String( + context.bridgeClient.readFile(FileAccessRequest.FileType.MAPPINGS), + StandardCharsets.UTF_8 + ) + ).asJsonObject.also { + snapBuildNumber = it["snap_build_number"].asInt + } + + mappingsObject.entrySet().forEach { (key, value): Map.Entry -> + if (value.isJsonArray) { + mappings[key] = context.gson.fromJson(value, ArrayList::class.java) + return@forEach + } + if (value.isJsonObject) { + mappings[key] = context.gson.fromJson(value, ConcurrentHashMap::class.java) + return@forEach + } + mappings[key] = value.asString + } + } + + private fun executeMappers(classes: List>) = runBlocking { + val jobs = mutableListOf() + mappers.forEach { mapper -> + mapper.context = context + launch { + runCatching { + mapper.useClasses(context.androidContext.classLoader, classes, mappings) + }.onFailure { + Logger.error("Failed to execute mapper ${mapper.javaClass.simpleName}", it) + } + }.also { jobs.add(it) } + } + jobs.forEach { it.join() } + } + + @Suppress("UNCHECKED_CAST", "DEPRECATION") + private fun refresh() { + context.shortToast("Loading mappings (this may take a while)") + val classes: MutableList> = ArrayList() + + val classLoader = context.androidContext.classLoader + val dexPathList = classLoader.getObjectField("pathList") + val dexElements = dexPathList.getObjectField("dexElements") as Array + + dexElements.forEach { dexElement: Any -> + val dexFile = dexElement.getObjectField("dexFile") as DexFile + dexFile.entries().toList().forEach fileList@{ className -> + //ignore classes without a dot in them + if (className.contains(".") && !className.startsWith("com.snap")) return@fileList + runCatching { + classLoader.loadClass(className)?.let { classes.add(it) } + } + } + } + + executeMappers(classes) + write() + } + + private fun write() { + val mappingsObject = JsonObject() + mappingsObject.addProperty("snap_build_number", snapBuildNumber) + mappings.forEach { (key, value) -> + if (value is List<*>) { + mappingsObject.add(key, context.gson.toJsonTree(value)) + return@forEach + } + if (value is Map<*, *>) { + mappingsObject.add(key, context.gson.toJsonTree(value)) + return@forEach + } + mappingsObject.addProperty(key, value.toString()) + } + + context.bridgeClient.writeFile( + FileAccessRequest.FileType.MAPPINGS, + mappingsObject.toString().toByteArray() + ) + } + + fun getMappedObject(key: String): Any { + if (mappings.containsKey(key)) { + return mappings[key]!! + } + Logger.xposedLog("Mapping not found deleting cache") + context.bridgeClient.deleteFile(FileAccessRequest.FileType.MAPPINGS) + throw Exception("No mapping found for $key") + } + + fun getMappedClass(className: String): Class<*> { + return context.androidContext.classLoader.loadClass(getMappedObject(className) as String) + } + + fun getMappedClass(key: String, subKey: String): Class<*> { + return context.androidContext.classLoader.loadClass(getMappedValue(key, subKey)) + } + + fun getMappedValue(key: String): String { + return getMappedObject(key) as String + } + + fun getMappedList(key: String): List { + return listOf(getMappedObject(key) as List).flatten() + } + + fun getMappedValue(key: String, subKey: String): String { + return getMappedMap(key)[subKey] as String + } + + fun getMappedMap(key: String): Map { + return getMappedObject(key) as Map + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/TranslationManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/TranslationManager.kt new file mode 100644 index 00000000..5f99d8a2 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/TranslationManager.kt @@ -0,0 +1,19 @@ +package me.rhunk.snapenhance.manager.impl + +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.manager.Manager +import java.util.* + +class TranslationManager( + private val context: ModContext +) : Manager { + override fun init() { + + } + + fun getLocale(): Locale = Locale.getDefault() + + fun get(key: String): String { + return key + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/Mapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/Mapper.kt new file mode 100644 index 00000000..c9f3df16 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/Mapper.kt @@ -0,0 +1,13 @@ +package me.rhunk.snapenhance.mapping + +import me.rhunk.snapenhance.ModContext + +abstract class Mapper { + lateinit var context: ModContext + + abstract fun useClasses( + classLoader: ClassLoader, + classes: List>, + mappings: MutableMap + ) +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/CallbackMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/CallbackMapper.kt new file mode 100644 index 00000000..6bca4730 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/CallbackMapper.kt @@ -0,0 +1,29 @@ +package me.rhunk.snapenhance.mapping.impl + +import me.rhunk.snapenhance.Logger.debug +import me.rhunk.snapenhance.mapping.Mapper +import java.lang.reflect.Method +import java.lang.reflect.Modifier + +class CallbackMapper : Mapper() { + override fun useClasses( + classLoader: ClassLoader, + classes: List>, + mappings: MutableMap + ) { + val callbackMappings = HashMap() + classes.forEach { clazz -> + val superClass = clazz.superclass ?: return@forEach + if (!superClass.name.endsWith("Callback") || superClass.name.endsWith("\$Callback")) return@forEach + if (!Modifier.isAbstract(superClass.modifiers)) return@forEach + + if (superClass.declaredMethods.any { method: Method -> + method.name == "onError" + }) { + callbackMappings[superClass.simpleName] = clazz.name + } + } + debug("found " + callbackMappings.size + " callbacks") + mappings["callbacks"] = callbackMappings + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt new file mode 100644 index 00000000..19b3c931 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt @@ -0,0 +1,41 @@ +package me.rhunk.snapenhance.mapping.impl + +import me.rhunk.snapenhance.Logger.debug +import me.rhunk.snapenhance.mapping.Mapper +import java.lang.reflect.Method +import java.util.* + + +class EnumMapper : Mapper() { + override fun useClasses( + classLoader: ClassLoader, + classes: List>, + mappings: MutableMap + ) { + val enumMappings = HashMap() + //settings classes have an interface that extends Serializable and contains the getName method + //this enum classes are used to store the settings values + //Setting enum class -> implements an interface -> getName method + classes.forEach { clazz -> + if (!clazz.isEnum) return@forEach + if (clazz.interfaces.isEmpty()) return@forEach + val serializableInterfaceClass = clazz.interfaces[0] + if (serializableInterfaceClass.methods + .filter { method: Method -> method.declaringClass == serializableInterfaceClass } + .none { method: Method -> method.name == "getName" } + ) return@forEach + + runCatching { + val getEnumNameMethod = + serializableInterfaceClass.methods.first { it!!.returnType.isEnum } + clazz.enumConstants?.onEach { enumConstant -> + val enumName = + Objects.requireNonNull(getEnumNameMethod.invoke(enumConstant)).toString() + enumMappings[enumName] = clazz.name + } + } + } + debug("found " + enumMappings.size + " enums") + mappings["enums"] = enumMappings + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/OperaPageViewControllerMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/OperaPageViewControllerMapper.kt new file mode 100644 index 00000000..82bd9d03 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/OperaPageViewControllerMapper.kt @@ -0,0 +1,77 @@ +package me.rhunk.snapenhance.mapping.impl + +import me.rhunk.snapenhance.mapping.Mapper +import me.rhunk.snapenhance.util.ReflectionHelper +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.* + + +class OperaPageViewControllerMapper : Mapper() { + override fun useClasses( + classLoader: ClassLoader, + classes: List>, + mappings: MutableMap + ) { + var operaPageViewControllerClass: Class<*>? = null + for (aClass in classes) { + if (!Modifier.isAbstract(aClass.modifiers)) continue + if (aClass.interfaces.isEmpty()) continue + val foundFields = Arrays.stream(aClass.declaredFields).filter { field: Field -> + val modifiers = field.modifiers + Modifier.isStatic(modifiers) && Modifier.isFinal( + modifiers + ) + }.filter { field: Field -> + try { + return@filter "ad_product_type" == String.format("%s", field[null]) + } catch (e: IllegalAccessException) { + e.printStackTrace() + } + false + }.count() + if (foundFields == 0L) continue + operaPageViewControllerClass = aClass + break + } + if (operaPageViewControllerClass == null) throw RuntimeException("OperaPageViewController not found") + + val members = HashMap() + members["Class"] = operaPageViewControllerClass.name + + operaPageViewControllerClass.fields.forEach { field -> + val fieldType = field.type + if (fieldType.isEnum) { + fieldType.enumConstants.firstOrNull { enumConstant: Any -> enumConstant.toString() == "FULLY_DISPLAYED" } + .let { members["viewStateField"] = field.name } + } + if (fieldType == ArrayList::class.java) { + members["layerListField"] = field.name + } + } + val enumViewStateClass = operaPageViewControllerClass.fields.first { field: Field -> + field.name == members["viewStateField"] + }.type + + //find the method that call the onDisplayStateChange method + members["onDisplayStateChange"] = + operaPageViewControllerClass.methods.first { method: Method -> + if (method.returnType != Void.TYPE || method.parameterTypes.size != 1) return@first false + val firstParameterClass = method.parameterTypes[0] + //check if the class contains a field with the enumViewStateClass type + ReflectionHelper.searchFieldByType(firstParameterClass, enumViewStateClass) != null + }.name + + //find the method that call the onDisplayStateChange method from gestures + members["onDisplayStateChange2"] = + operaPageViewControllerClass.methods.first { method: Method -> + if (method.returnType != Void.TYPE || method.parameterTypes.size != 2) return@first false + val firstParameterClass = method.parameterTypes[0] + val secondParameterClass = method.parameterTypes[1] + firstParameterClass.isEnum && secondParameterClass.isEnum + }.name + + mappings["OperaPageViewController"] = members + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlusSubscriptionMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlusSubscriptionMapper.kt new file mode 100644 index 00000000..96aecee6 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlusSubscriptionMapper.kt @@ -0,0 +1,32 @@ +package me.rhunk.snapenhance.mapping.impl + +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.mapping.Mapper +import java.lang.reflect.Field +import java.lang.reflect.Method + + +class PlusSubscriptionMapper : Mapper() { + override fun useClasses( + classLoader: ClassLoader, + classes: List>, + mappings: MutableMap + ) { + //find a method that contains annotations with isSubscribed + val loadSubscriptionMethod = context.classCache.composerLocalSubscriptionStore.declaredMethods.first { method: Method -> + val returnType = method.returnType + returnType.declaredFields.any { field: Field -> + field.declaredAnnotations.any { annotation: Annotation -> + annotation.toString().contains("isSubscribed") + } + } + } + //get the first param of the method which is the PlusSubscriptionState class + val plusSubscriptionStateClass = loadSubscriptionMethod.parameterTypes[0] + //get the first param of the constructor of PlusSubscriptionState which is the SubscriptionInfo class + val subscriptionInfoClass = plusSubscriptionStateClass.constructors[0].parameterTypes[0] + Logger.debug("subscriptionInfoClass ${subscriptionInfoClass.name}") + + mappings["SubscriptionInfoClass"] = subscriptionInfoClass.name + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt new file mode 100644 index 00000000..8559712d --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt @@ -0,0 +1,93 @@ +package me.rhunk.snapenhance.util + +import de.robv.android.xposed.XC_MethodHook +import me.rhunk.snapenhance.hook.HookAdapter +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.lang.reflect.Modifier + +class CallbackBuilder( + private val callbackClass: Class<*> +) { + internal class Override( + val methodName: String, + val callback: (HookAdapter) -> Unit + ) + + private val methodOverrides = mutableListOf() + + fun override(methodName: String, callback: (HookAdapter) -> Unit = {}): CallbackBuilder { + methodOverrides.add(Override(methodName, callback)) + return this + } + + fun build(): Any { + //get the first param of the first constructor to get the class of the invoker + val invokerClass: Class<*> = callbackClass.constructors[0].parameterTypes[0] + //get the invoker field based on the invoker class + val invokerField = callbackClass.fields.first { field: Field -> + field.type.isAssignableFrom(invokerClass) + } + //get the callback field based on the callback class + val callbackInstance = createEmptyObject(callbackClass.constructors[0])!! + val callbackInstanceHashCode: Int = callbackInstance.hashCode() + val callbackInstanceClass = callbackInstance.javaClass + + val unhooks = mutableListOf() + + callbackInstanceClass.methods.forEach { method -> + if (method.declaringClass != callbackInstanceClass) return@forEach + if (Modifier.isPrivate(method.modifiers)) return@forEach + + //default hook that unhooks the callback and returns null + val defaultHook: (HookAdapter) -> Boolean = defaultHook@{ + //checking invokerField ensure that's the callback was created by the CallbackBuilder + if (invokerField.get(it.thisObject()) != null) return@defaultHook false + if ((it.thisObject() as Any).hashCode() != callbackInstanceHashCode) return@defaultHook false + + it.setResult(null) + unhooks.forEach { unhook -> unhook.unhook() } + true + } + + var hook: (HookAdapter) -> Unit = { defaultHook(it) } + + //override the default hook if the method is in the override list + methodOverrides.find { it.methodName == method.name }?.run { + hook = { + if (defaultHook(it)) { + callback(it) + } + } + } + + unhooks.add(Hooker.hook(method, HookStage.BEFORE, hook)) + } + return callbackInstance + } + + companion object { + private fun createEmptyObject(constructor: Constructor<*>): Any? { + //compute the args for the constructor with null or default primitive values + val args = constructor.parameterTypes.map { type: Class<*> -> + if (type.isPrimitive) { + when (type.name) { + "boolean" -> return@map false + "byte" -> return@map 0.toByte() + "char" -> return@map 0.toChar() + "short" -> return@map 0.toShort() + "int" -> return@map 0 + "long" -> return@map 0L + "float" -> return@map 0f + "double" -> return@map 0.0 + } + } + null + }.toTypedArray() + return constructor.newInstance(*args) + } + + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/EncryptionHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/EncryptionHelper.kt new file mode 100644 index 00000000..5eee049d --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/EncryptionHelper.kt @@ -0,0 +1,67 @@ +package me.rhunk.snapenhance.util + +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.util.protobuf.ProtoReader +import java.io.InputStream +import java.util.* +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object EncryptionUtils { + fun decryptInputStreamFromArroyo( + inputStream: InputStream, + contentType: ContentType, + messageProto: ProtoReader + ): InputStream { + var resultInputStream = inputStream + val encryptionProtoPath: IntArray = when (contentType) { + ContentType.NOTE -> Constants.ARROYO_NOTE_ENCRYPTION_PROTO_PATH + ContentType.SNAP -> Constants.ARROYO_SNAP_ENCRYPTION_PROTO_PATH + ContentType.EXTERNAL_MEDIA -> Constants.ARROYO_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH + else -> throw IllegalArgumentException("Invalid content type: $contentType") + } + + //decrypt the content if needed + messageProto.readPath(*encryptionProtoPath)?.let { + val encryptionProtoIndex: Int = if (it.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX_V2)) { + Constants.ARROYO_ENCRYPTION_PROTO_INDEX_V2 + } else if (it.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX)) { + Constants.ARROYO_ENCRYPTION_PROTO_INDEX + } else { + return resultInputStream + } + resultInputStream = decryptInputStream( + resultInputStream, + encryptionProtoIndex == Constants.ARROYO_ENCRYPTION_PROTO_INDEX_V2, + it, + encryptionProtoIndex + ) + } + return resultInputStream + } + + fun decryptInputStream( + inputStream: InputStream, + base64Encryption: Boolean, + mediaInfoProto: ProtoReader, + encryptionProtoIndex: Int + ): InputStream { + val mediaEncryption = mediaInfoProto.readPath(encryptionProtoIndex)!! + var key: ByteArray = mediaEncryption.getByteArray(1)!! + var iv: ByteArray = mediaEncryption.getByteArray(2)!! + + //audio note and external medias have their key and iv encoded in base64 + if (base64Encryption) { + val decoder = Base64.getMimeDecoder() + key = decoder.decode(key) + iv = decoder.decode(iv) + } + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + return CipherInputStream(inputStream, cipher) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/PreviewCreator.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/PreviewCreator.kt new file mode 100644 index 00000000..a5a288b0 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/PreviewCreator.kt @@ -0,0 +1,41 @@ +package me.rhunk.snapenhance.util + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.MediaDataSource +import android.media.MediaMetadataRetriever + +object PreviewUtils { + fun createPreview(data: ByteArray, isVideo: Boolean): Bitmap? { + if (!isVideo) { + return BitmapFactory.decodeByteArray(data, 0, data.size) + } + val retriever = MediaMetadataRetriever() + retriever.setDataSource(object : MediaDataSource() { + override fun readAt( + position: Long, + buffer: ByteArray, + offset: Int, + size: Int + ): Int { + var newSize = size + val length = data.size + if (position >= length) { + return -1 + } + if (position + newSize > length) { + newSize = length - position.toInt() + } + System.arraycopy(data, position.toInt(), buffer, offset, newSize) + return newSize + } + + override fun getSize(): Long { + return data.size.toLong() + } + + override fun close() {} + }) + return retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt new file mode 100644 index 00000000..e3ed6b84 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt @@ -0,0 +1,118 @@ +package me.rhunk.snapenhance.util + +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.util.* + +object ReflectionHelper { + /** + * Searches for a field with a class that has a method with the specified name + */ + fun searchFieldWithClassMethod(clazz: Class<*>, methodName: String): Field? { + return clazz.declaredFields.firstOrNull { f: Field? -> + try { + return@firstOrNull Arrays.stream( + f!!.type.declaredMethods + ).anyMatch { method: Method -> method.name == methodName } + } catch (e: Exception) { + return@firstOrNull false + } + } + } + + fun searchFieldByType(clazz: Class<*>, type: Class<*>): Field? { + return clazz.declaredFields.firstOrNull { f: Field? -> f!!.type == type } + } + + fun searchFieldTypeInSuperClasses(clazz: Class<*>, type: Class<*>): Field? { + val field = searchFieldByType(clazz, type) + if (field != null) { + return field + } + val superclass = clazz.superclass + return superclass?.let { searchFieldTypeInSuperClasses(it, type) } + } + + fun searchFieldStartsWithToString( + clazz: Class<*>, + instance: Any, + toString: String? + ): Field? { + return clazz.declaredFields.firstOrNull { f: Field -> + try { + f.isAccessible = true + return@firstOrNull Objects.requireNonNull(f[instance]).toString() + .startsWith( + toString!! + ) + } catch (e: Throwable) { + return@firstOrNull false + } + } + } + + + fun searchFieldContainsToString( + clazz: Class<*>, + instance: Any?, + toString: String? + ): Field? { + return clazz.declaredFields.firstOrNull { f: Field -> + try { + f.isAccessible = true + return@firstOrNull Objects.requireNonNull(f[instance]).toString() + .contains(toString!!) + } catch (e: Throwable) { + return@firstOrNull false + } + } + } + + fun searchFirstFieldTypeInClassRecursive(clazz: Class<*>, type: Class<*>): Field? { + return clazz.declaredFields.firstOrNull { + val field = searchFieldByType(it.type, type) + return@firstOrNull field != null + } + } + + /** + * Searches for a field with a class that has a method with the specified return type + */ + fun searchMethodWithReturnType(clazz: Class<*>, returnType: Class<*>): Method? { + return clazz.declaredMethods.first { m: Method -> m.returnType == returnType } + } + + /** + * Searches for a field with a class that has a method with the specified return type and parameter types + */ + fun searchMethodWithParameterAndReturnType( + aClass: Class<*>, + returnType: Class<*>, + vararg parameters: Class<*> + ): Method? { + return aClass.declaredMethods.firstOrNull { m: Method -> + if (m.returnType != returnType) { + return@firstOrNull false + } + val parameterTypes = m.parameterTypes + if (parameterTypes.size != parameters.size) { + return@firstOrNull false + } + for (i in parameterTypes.indices) { + if (parameterTypes[i] != parameters[i]) { + return@firstOrNull false + } + } + true + } + } + + fun getDeclaredFieldsRecursively(clazz: Class<*>): List { + val fields = clazz.declaredFields.toMutableList() + val superclass = clazz.superclass + if (superclass != null) { + fields.addAll(getDeclaredFieldsRecursively(superclass)) + } + return fields + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperMacros.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperMacros.kt new file mode 100644 index 00000000..c73877a1 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperMacros.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.util + +import de.robv.android.xposed.XposedHelpers + +fun Any.getObjectField(fieldName: String): Any { + return XposedHelpers.getObjectField(this, fieldName) +} + +fun Any.setObjectField(fieldName: String, value: Any) { + XposedHelpers.setObjectField(this, fieldName, value) +} + +fun Any.getObjectFieldOrNull(fieldName: String): Any? { + return try { + getObjectField(fieldName) + } catch (e: Exception) { + null + } +} + diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/download/CdnDownloader.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/download/CdnDownloader.kt new file mode 100644 index 00000000..fbca8858 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/download/CdnDownloader.kt @@ -0,0 +1,83 @@ +package me.rhunk.snapenhance.util.download + +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import me.rhunk.snapenhance.Constants +import java.io.InputStream +import java.net.URL +import javax.net.ssl.HttpsURLConnection + +object CdnDownloader { + const val BOLT_CDN_U = "https://bolt-gcdn.sc-cdn.net/u/" + const val BOLT_CDN_X = "https://bolt-gcdn.sc-cdn.net/x/" + const val CF_ST_CDN_D = "https://cf-st.sc-cdn.net/d/" + const val CF_ST_CDN_F = "https://cf-st.sc-cdn.net/f/" + const val CF_ST_CDN_H = "https://cf-st.sc-cdn.net/h/" + const val CF_ST_CDN_G = "https://cf-st.sc-cdn.net/g/" + const val CF_ST_CDN_O = "https://cf-st.sc-cdn.net/o/" + const val CF_ST_CDN_I = "https://cf-st.sc-cdn.net/i/" + const val CF_ST_CDN_J = "https://cf-st.sc-cdn.net/j/" + const val CF_ST_CDN_C = "https://cf-st.sc-cdn.net/c/" + const val CF_ST_CDN_AA = "https://cf-st.sc-cdn.net/aa/" + + private val keyCache: MutableMap = mutableMapOf() + + fun downloadRemoteContent( + key: String, + vararg endpoints: String + ): InputStream? = runBlocking { + if (keyCache.containsKey(key)) { + return@runBlocking queryRemoteContent( + keyCache[key]!! + ) + } + val jobs = mutableListOf() + var inputStream: InputStream? = null + + endpoints.forEach { + launch { + val url = it + key + val result = queryRemoteContent(url) + if (result != null) { + keyCache[key] = url + inputStream = result + jobs.forEach { it.cancel() } + } + }.also { jobs.add(it) } + } + jobs.forEach { it.join() } + inputStream + } + + + private fun queryRemoteContent(url: String): InputStream? { + try { + val connection = URL(url).openConnection() as HttpsURLConnection + connection.requestMethod = "GET" + connection.connectTimeout = 5000 + connection.setRequestProperty("User-Agent", Constants.USER_AGENT) + return connection.inputStream + } catch (ignored: Throwable) { + } + return null + } + + //TODO: automatically detect the correct endpoint + fun downloadWithDefaultEndpoints(key: String): InputStream? { + return downloadRemoteContent( + key, + CF_ST_CDN_F, + CF_ST_CDN_H, + BOLT_CDN_U, + BOLT_CDN_X, + CF_ST_CDN_O, + CF_ST_CDN_I, + CF_ST_CDN_C, + CF_ST_CDN_J, + CF_ST_CDN_AA, + CF_ST_CDN_G, + CF_ST_CDN_D + ) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt new file mode 100644 index 00000000..712e24fc --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt @@ -0,0 +1,116 @@ +package me.rhunk.snapenhance.util.download + +import de.robv.android.xposed.XposedBridge +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.Logger.debug +import me.rhunk.snapenhance.ModContext +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.io.PrintWriter +import java.net.ServerSocket +import java.net.Socket +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ThreadLocalRandom +import java.util.function.Consumer + +class DownloadServer( + private val context: ModContext +) { + private val port = ThreadLocalRandom.current().nextInt(10000, 65535) + + private val cachedData = ConcurrentHashMap() + private var serverSocket: ServerSocket? = null + + fun startFileDownload(destination: File, content: ByteArray, callback: Consumer) { + val httpKey = java.lang.Long.toHexString(System.nanoTime()) + ensureServerStarted { + putDownloadableContent(httpKey, content) + val url = "http://127.0.0.1:$port/$httpKey" + context.executeAsync { + val result: Boolean = context.bridgeClient.downloadContent(url, destination.absolutePath) + callback.accept(result) + } + } + } + + private fun ensureServerStarted(callback: Runnable) { + if (serverSocket != null && !serverSocket!!.isClosed) { + callback.run() + return + } + Thread { + try { + debug("started web server on 127.0.0.1:$port") + serverSocket = ServerSocket(port) + callback.run() + while (!serverSocket!!.isClosed) { + try { + val socket = serverSocket!!.accept() + Thread { handleRequest(socket) }.start() + } catch (e: Throwable) { + Logger.xposedLog(e) + } + } + } catch (e: Throwable) { + Logger.xposedLog(e) + } + }.start() + } + + fun putDownloadableContent(key: String, data: ByteArray) { + cachedData[key] = data + } + + private fun handleRequest(socket: Socket) { + val reader = BufferedReader(InputStreamReader(socket.getInputStream())) + val outputStream = socket.getOutputStream() + val writer = PrintWriter(outputStream) + val line = reader.readLine() ?: return + val close = Runnable { + try { + reader.close() + writer.close() + outputStream.close() + socket.close() + } catch (e: Throwable) { + Logger.xposedLog(e) + } + } + val parse = StringTokenizer(line) + val method = parse.nextToken().uppercase(Locale.getDefault()) + var fileRequested = parse.nextToken().lowercase(Locale.getDefault()) + if (method != "GET") { + writer.println("HTTP/1.1 501 Not Implemented") + writer.println("Content-type: " + "application/octet-stream") + writer.println("Content-length: " + 0) + writer.println() + writer.flush() + close.run() + return + } + if (fileRequested.startsWith("/")) { + fileRequested = fileRequested.substring(1) + } + if (!cachedData.containsKey(fileRequested)) { + writer.println("HTTP/1.1 404 Not Found") + writer.println("Content-type: " + "application/octet-stream") + writer.println("Content-length: " + 0) + writer.println() + writer.flush() + close.run() + return + } + val data = cachedData[fileRequested]!! + writer.println("HTTP/1.1 200 OK") + writer.println("Content-type: " + "application/octet-stream") + writer.println("Content-length: " + data.size) + writer.println() + writer.flush() + outputStream.write(data, 0, data.size) + outputStream.flush() + close.run() + cachedData.remove(fileRequested) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt new file mode 100644 index 00000000..f5f28ece --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt @@ -0,0 +1,122 @@ +package me.rhunk.snapenhance.util.protobuf + +data class Wire(val type: Int, val value: Any) + +class ProtoReader(private val buffer: ByteArray) { + private var offset: Int = 0 + private val values = mutableMapOf>() + + init { + read() + } + + fun getBuffer() = buffer + + private fun readByte() = buffer[offset++] + + private fun readVarInt(): Long { + var result = 0L + var shift = 0 + while (true) { + val b = readByte() + result = result or ((b.toLong() and 0x7F) shl shift) + if (b.toInt() and 0x80 == 0) { + break + } + shift += 7 + } + return result + } + + private fun read() { + while (offset < buffer.size) { + val tag = readVarInt().toInt() + val id = tag ushr 3 + val type = tag and 0x7 + try { + val value = when (type) { + 0 -> readVarInt().toString().toByteArray() + 2 -> { + val length = readVarInt().toInt() + val value = buffer.copyOfRange(offset, offset + length) + offset += length + value + } + else -> break + } + values.getOrPut(id) { mutableListOf() }.add(Wire(type, value)) + } catch (t: Throwable) { + values.clear() + break + } + } + } + + fun readPath(vararg ids: Int, reader: (ProtoReader.() -> Unit)? = null): ProtoReader? { + var thisReader = this + ids.forEach { id -> + if (!thisReader.exists(id)) { + return null + } + thisReader = ProtoReader(thisReader.get(id) as ByteArray) + } + if (reader != null) { + thisReader.reader() + } + return thisReader + } + + fun pathExists(vararg ids: Int): Boolean { + var thisReader = this + ids.forEach { id -> + if (!thisReader.exists(id)) { + return false + } + thisReader = ProtoReader(thisReader.get(id) as ByteArray) + } + return true + } + + fun getByteArray(id: Int) = values[id]?.first()?.value as ByteArray? + fun getByteArray(vararg ids: Int): ByteArray? { + if (ids.isEmpty() || ids.size < 2) { + return null + } + val lastId = ids.last() + var value: ByteArray? = null + readPath(*(ids.copyOfRange(0, ids.size - 1))) { + value = getByteArray(lastId) + } + return value + } + + fun getString(id: Int) = getByteArray(id)?.toString(Charsets.UTF_8) + fun getString(vararg ids: Int) = getByteArray(*ids)?.toString(Charsets.UTF_8) + + fun getInt(id: Int) = getString(id)?.toInt() + fun getInt(vararg ids: Int) = getString(*ids)?.toInt() + + fun getLong(id: Int) = getString(id)?.toLong() + fun getLong(vararg ids: Int) = getString(*ids)?.toLong() + + fun exists(id: Int) = values.containsKey(id) + + fun get(id: Int) = values[id]!!.first().value + + fun isValid() = values.isNotEmpty() + + fun getCount(id: Int) = values[id]!!.size + + fun each(id: Int, reader: ProtoReader.(index: Int) -> Unit) { + values[id]!!.forEachIndexed { index, _ -> + ProtoReader(values[id]!![index].value as ByteArray).reader(index) + } + } + + fun eachExists(id: Int, reader: ProtoReader.(index: Int) -> Unit) { + if (!exists(id)) { + return + } + each(id, reader) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt new file mode 100644 index 00000000..f12eaa1a --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt @@ -0,0 +1,66 @@ +package me.rhunk.snapenhance.util.protobuf + +import java.io.ByteArrayOutputStream + +class ProtoWriter { + private val stream: ByteArrayOutputStream = ByteArrayOutputStream() + + private fun writeVarInt(value: Int) { + var v = value + while (v and -0x80 != 0) { + stream.write(v and 0x7F or 0x80) + v = v ushr 7 + } + stream.write(v) + } + + private fun writeVarLong(value: Long) { + var v = value + while (v and -0x80L != 0L) { + stream.write((v and 0x7FL or 0x80L).toInt()) + v = v ushr 7 + } + stream.write(v.toInt()) + } + + fun writeBuffer(id: Int, value: ByteArray) { + writeVarInt(id shl 3 or 2) + writeVarInt(value.size) + stream.write(value) + } + + fun writeConstant(id: Int, value: Int) { + writeVarInt(id shl 3) + writeVarInt(value) + } + + fun writeConstant(id: Int, value: Long) { + writeVarInt(id shl 3) + writeVarLong(value) + } + + fun writeString(id: Int, value: String) = writeBuffer(id, value.toByteArray()) + + fun write(id: Int, writer: ProtoWriter.() -> Unit) { + val writerStream = ProtoWriter() + writer(writerStream) + writeBuffer(id, writerStream.stream.toByteArray()) + } + + fun write(vararg ids: Int, writer: ProtoWriter.() -> Unit) { + val writerStream = ProtoWriter() + writer(writerStream) + var stream = writerStream.stream.toByteArray() + ids.reversed().forEach { id -> + with(ProtoWriter()) { + writeBuffer(id, stream) + stream = this.stream.toByteArray() + } + } + stream.let(this.stream::write) + } + + fun toByteArray(): ByteArray { + return stream.toByteArray() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapUUID.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapUUID.kt new file mode 100644 index 00000000..a4c9508b --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/SnapUUID.kt @@ -0,0 +1,2 @@ +package me.rhunk.snapenhance.util.snap + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 00000000..0d2c4cc4 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 00000000..3ef365ea --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,6 @@ + + + + com.snapchat.android + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..0d7eee6b --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,33 @@ + + Snap Enhance + Save folder + Prevent read receipts + Hide Bitmoji presence + Show message content + Message logger + Media downloader feature + Download stories + Download public stories + Download spotlight + Overlay merge + Download in chat snaps + Disable metrics + Prevent screenshot + Anonymous story view + Hide typing notification + Menu slot id + Message preview length + Auto save + External media as snap + Conversation export + Snapchat Plus + Remove voice record button + Remove stickers button + Remove cognac button + Remove call buttons + Long snap sending + Block ads + Streak Expiration Info + New map ui + Use download manager + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..4b42505b --- /dev/null +++ b/build.gradle @@ -0,0 +1,10 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.2.2' apply false + id 'com.android.library' version '7.2.2' apply false + id 'org.jetbrains.kotlin.android' version '1.8.21' apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..cd0519bb --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3582dbb3 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri May 12 21:23:16 CEST 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..4f906e0c --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..e06b8971 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "SnapEnhance" +include ':app'