├── .github └── workflows │ ├── android.yml │ └── apk-release.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ ├── api-82.jar │ ├── clientlib.jar │ └── snapmod.jar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── xposed_init │ ├── java │ └── xyz │ │ └── rodit │ │ └── snapmod │ │ ├── CustomResources.kt │ │ ├── DelegateProxy.kt │ │ ├── DummyProxy.kt │ │ ├── FeatureContextUpdater.kt │ │ ├── ForceResumeActivity.kt │ │ ├── ObjectProxy.kt │ │ ├── SettingsActivity.kt │ │ ├── Shared.kt │ │ ├── SnapHooks.kt │ │ ├── UriResolverSubscriber.kt │ │ ├── arroyo │ │ ├── ArroyoMessage.kt │ │ └── ArroyoReader.kt │ │ ├── features │ │ ├── Contextual.kt │ │ ├── Feature.kt │ │ ├── FeatureContext.kt │ │ ├── FeatureManager.kt │ │ ├── InstanceManager.kt │ │ ├── ViewInterceptor.kt │ │ ├── callbacks │ │ │ └── CallbackManager.kt │ │ ├── chatmenu │ │ │ ├── ButtonOption.kt │ │ │ ├── ChatMenuModifier.kt │ │ │ ├── ConversationToggleOption.kt │ │ │ ├── ExportOption.kt │ │ │ ├── MenuPlugin.kt │ │ │ ├── PreviewOption.kt │ │ │ ├── ToggleOption.kt │ │ │ ├── new │ │ │ │ ├── MenuPlugin.kt │ │ │ │ ├── NewChatMenuModifier.kt │ │ │ │ ├── PlainOption.kt │ │ │ │ └── SwitchOption.kt │ │ │ └── shared │ │ │ │ ├── ExportChat.kt │ │ │ │ └── PreviewChat.kt │ │ ├── conversations │ │ │ ├── AutoSave.kt │ │ │ ├── MessageInterceptor.kt │ │ │ ├── NullFilter.kt │ │ │ ├── ObjectFilter.kt │ │ │ ├── PreventBitmojiPresence.kt │ │ │ ├── PreventReadReceipts.kt │ │ │ ├── PreventTypingNotifications.kt │ │ │ ├── SnapInteractionFilter.kt │ │ │ ├── SnapOverrides.kt │ │ │ └── StealthFeature.kt │ │ ├── friendsfeed │ │ │ └── PinChats.kt │ │ ├── info │ │ │ ├── AdditionalFriendInfo.kt │ │ │ └── NetworkLogging.kt │ │ ├── notifications │ │ │ ├── FilterTypes.kt │ │ │ └── ShowMessageContent.kt │ │ ├── opera │ │ │ ├── CustomStoryOptions.kt │ │ │ ├── MenuModifier.kt │ │ │ ├── MenuPlugin.kt │ │ │ ├── OperaModelModifier.kt │ │ │ ├── OperaPlugin.kt │ │ │ ├── SaveMenuOption.kt │ │ │ └── SnapDurationModifier.kt │ │ ├── saving │ │ │ ├── AutoDownloadSnaps.kt │ │ │ ├── AutoDownloadStories.kt │ │ │ ├── ChatSaving.kt │ │ │ ├── PublicProfileSaving.kt │ │ │ ├── StoriesSaving.kt │ │ │ └── StoryHelper.kt │ │ ├── shared │ │ │ └── Filter.kt │ │ └── tweaks │ │ │ ├── BypassVideoLength.kt │ │ │ ├── BypassVideoLengthGlobal.kt │ │ │ ├── CameraResolution.kt │ │ │ ├── ConfigurationTweaks.kt │ │ │ ├── DisableBitmojis.kt │ │ │ ├── HideFriends.kt │ │ │ ├── HideStoryReadReceipts.kt │ │ │ ├── HideStorySections.kt │ │ │ ├── HideStorySectionsLegacy.kt │ │ │ └── PinStories.kt │ │ ├── logging │ │ ├── XLog.kt │ │ └── XLogUtils.kt │ │ └── util │ │ ├── BuildUtils.kt │ │ ├── ConfigExtensions.kt │ │ ├── ConversationManager.kt │ │ ├── DisplayUtils.kt │ │ ├── DownloadExtensions.kt │ │ ├── HookExtensions.kt │ │ ├── LogExtensions.kt │ │ ├── PairExtensions.kt │ │ ├── PathManager.kt │ │ ├── ProtoReader.kt │ │ └── UUIDUtil.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values-fr │ └── strings.xml │ ├── values-night │ └── themes.xml │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── network_security_config.xml │ └── root_preferences.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── snap.ds /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - '**/README.md' 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: set up JDK 11 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: '11' 22 | distribution: 'temurin' 23 | cache: gradle 24 | 25 | - name: Grant execute permission for gradlew 26 | run: chmod +x gradlew 27 | - name: Build with Gradle 28 | run: ./gradlew build 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/apk-release.yml: -------------------------------------------------------------------------------- 1 | name: Release And Update 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | 7 | jobs: 8 | publish: 9 | name: Release Debug APK 10 | runs-on: ubuntu-latest 11 | steps: 12 | 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: 17 20 | distribution: 'adopt' 21 | cache: gradle 22 | 23 | - name: Download Snapchat APK 24 | run: wget -q -O snapchat.apk "$APK_URL" 25 | env: 26 | APK_URL: ${{ secrets.APK_URL }} 27 | 28 | - name: Download dexsearch and clientlib 29 | run: | 30 | wget -q -O dexsearch.jar "https://github.com/rodit/dexsearch/releases/download/$DEXSEARCH_VERSION/dexsearch-latest.jar" 31 | wget -q -O app/libs/clientlib.jar "https://github.com/rodit/dexsearch/releases/download/$DEXSEARCH_VERSION/clientlib-latest.jar" 32 | env: 33 | DEXSEARCH_VERSION: ${{ secrets.DEXSEARCH_VERSION }} 34 | 35 | - name: Download Android Platform 36 | run: | 37 | wget -q -O android.zip "https://dl.google.com/android/repository/platform-32_r01.zip" 38 | unzip -qq android.zip 39 | 40 | - name: Generate Bindings and snapmod jar 41 | run: java -jar dexsearch.jar -i snapchat.apk -s snap.ds -o bindings.json -j app/libs/snapmod.jar -a "android-12/android.jar" -p xyz.rodit.snapmod.mappings 42 | 43 | - name: Grant execute permission for gradlew 44 | run: chmod +x gradlew 45 | 46 | - name: Load keystore 47 | run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > $GITHUB_WORKSPACE/signing.jks 48 | 49 | - name: Generate Signed Release APK 50 | run: ./gradlew clean assembleRelease -Pandroid.injected.signing.store.file="$GITHUB_WORKSPACE/signing.jks" -Pandroid.injected.signing.store.password="${{ secrets.KEYSTORE_PASSWORD }}" -Pandroid.injected.signing.key.alias="${{ secrets.KEYSTORE_ALIAS }}" -Pandroid.injected.signing.key.password="${{ secrets.KEYSTORE_KEY_PASSWORD }}" 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | - name: Generate Version Info 55 | run: ./gradlew versionInfo 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | - name: Publish Release APK and Bindings 60 | uses: marvinpinto/action-automatic-releases@latest 61 | with: 62 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 63 | prerelease: false 64 | files: | 65 | app/build/outputs/apk/release/app-release.apk 66 | app/build/version.json 67 | bindings.json 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | gradle.properties 17 | /keys.txt 18 | /keystore.jks 19 | /.idea/codeStyles/codeStyleConfig.xml 20 | /.idea/codeStyles/Project.xml 21 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | /misc.xml 5 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | SnapMod -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SnapMod 2 | Xposed module for Snapchat. 3 | 4 | ## Setup 5 | To set SnapMod up, download and install the latest apk from [here](https://github.com/rodit/SnapMod/releases). When you open it, it will ask to install some bindings. Press 'Download' and be sure to kill and restart Snapchat afterwards. The latest and only fully supported version of Snapchat is **11.96.0.31** (as of SnapMod 1.8.5). Mappings **will not** be downloaded for previous versions of Snapchat automatically, only for the latest supported version. If you are using an older version, you must manually place the mappings in `/data/data/xyz.rodit.snapmod/files/[build].json` or `/Android/data/xyz.rodit.snapmod/files/[build].json` on your internal storage where `[build]` is the version code of Snapchat the mappings correspond to. Note, there is no guarentee the newest version of Snapmod will work with old mappings (it usually will not for a couple of features). 6 | 7 | ## Features 8 | For a full list of features and their status, please visit the wiki [here](https://github.com/rodit/SnapMod/wiki/Features). 9 | 10 | ## Feature Suggestions 11 | If you would like to suggest a new feature, please do so in **Discussions**. Please do not create an Issue for feature suggetsions (they will be moved to Discussions). 12 | 13 | ## Issues 14 | If you have an issue please post a log from LSPosed (I have no interest in supporting other Xposed implementations, although SnapMod should work fine with them) and a description of the issue. It is **extremely** unhelpful to just say "something doesn't work" or "I can't download stories." 15 | 16 | ## Troubleshooting 17 | If anything doesn't work, the first thing you should try is killing and relaunching Snapchat (through the system settings app, not recent app list) **TWICE** and then trying again. This will ensure any cached mappings are updated. This is especially important after updating SnapMod and its mappings. 18 | 19 | ## Donations 20 | Although this is a personal project I do entirely for fun (and it will most likely stay that way), if you would like to support the development of SnapMod, you can do so by donating: 21 | - PayPal: https://paypal.me/roditmod 22 | - Cashapp: https://cash.app/rodit9 23 | - BTC: bc1qr06chdv85jf9v7ldf7l24lrgp6ad7av8y7jwyc 24 | - ETH: 0x90659C0556b37107359FA32b40AA74c593590E04 25 | - XRP: rDgNCbi4eCeczpzGHGs3XHsR5C3SyUCr5r 26 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 32 8 | 9 | defaultConfig { 10 | applicationId "xyz.rodit.snapmod" 11 | minSdk 24 12 | targetSdk 32 13 | versionCode 31 14 | versionName "1.8.5" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | 31 | namespace 'xyz.rodit.snapmod' 32 | 33 | lint { 34 | disable 'MissingTranslation' 35 | } 36 | } 37 | 38 | task versionInfo { 39 | doLast { 40 | def build = 84641 41 | def infoFile = new File('app/build/version.json') 42 | infoFile.getParentFile().mkdirs() 43 | infoFile.delete() 44 | infoFile.createNewFile() 45 | infoFile.text = "{\"versionCode\":${android.defaultConfig.versionCode},\"build\":${build}}" 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation 'androidx.appcompat:appcompat:1.4.1' 52 | implementation 'com.google.android.material:material:1.5.0' 53 | 54 | implementation files('libs/snapmod.jar') 55 | implementation 'androidx.preference:preference:1.2.0' 56 | implementation 'xyz.rodit:xposed:1.4.4' 57 | 58 | implementation 'com.squareup.okhttp3:okhttp:4.9.3' 59 | implementation 'io.noties.markwon:core:4.6.2' 60 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 61 | 62 | testImplementation 'junit:junit:4.+' 63 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 65 | 66 | compileOnly files('libs/api-82.jar') 67 | } -------------------------------------------------------------------------------- /app/libs/api-82.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/libs/api-82.jar -------------------------------------------------------------------------------- /app/libs/clientlib.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/libs/clientlib.jar -------------------------------------------------------------------------------- /app/libs/snapmod.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/libs/snapmod.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | 20 | 23 | 26 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | xyz.rodit.snapmod.SnapHooks -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/CustomResources.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import de.robv.android.xposed.XC_MethodHook 6 | import de.robv.android.xposed.XposedBridge 7 | 8 | object CustomResources { 9 | 10 | private val strings: MutableMap = mutableMapOf( 11 | string.menu_option_stealth_mode to "Stealth Mode", 12 | string.menu_option_preview to "More Information", 13 | string.menu_option_auto_save to "Auto-Save Messages", 14 | string.menu_option_auto_download to "Auto-Download Snaps", 15 | string.menu_option_export to "Export...", 16 | 17 | string.chat_action_playback_speed to "Set Playback Speed", 18 | 19 | string.menu_story_custom_options to "SnapMod:CUSTOM_STORY_OPTIONS" 20 | ) 21 | 22 | fun init() { 23 | XposedBridge.hookAllMethods(Resources::class.java, "getString", object : XC_MethodHook() { 24 | override fun beforeHookedMethod(param: MethodHookParam) { 25 | val id = param.args[0] as Int 26 | strings[id]?.let { 27 | param.result = it 28 | } 29 | } 30 | }) 31 | 32 | XposedBridge.hookAllMethods(Context::class.java, "getString", object : XC_MethodHook() { 33 | override fun beforeHookedMethod(param: MethodHookParam) { 34 | val id = param.args[0] as Int 35 | strings[id]?.let { 36 | param.result = it 37 | } 38 | } 39 | }) 40 | } 41 | 42 | fun get(id: Int): String? { 43 | return strings[id] 44 | } 45 | 46 | object string { 47 | const val menu_option_stealth_mode = -100000 48 | const val menu_option_preview = -100001 49 | const val menu_option_auto_save = -100002 50 | const val menu_option_auto_download = -100003 51 | const val menu_option_export = -100004 52 | 53 | const val chat_action_playback_speed = -200000 54 | 55 | const val menu_story_custom_options = -300000 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/DelegateProxy.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | import java.lang.reflect.InvocationHandler 4 | import java.lang.reflect.Method 5 | import java.lang.reflect.Proxy 6 | 7 | typealias DelegateFunction = (Any, Array) -> Any? 8 | 9 | class DelegateProxy(private val delegate: DelegateFunction) : InvocationHandler { 10 | 11 | override fun invoke(target: Any, method: Method, args: Array?): Any? { 12 | return delegate(target, args ?: emptyArray()) 13 | } 14 | } 15 | 16 | fun Class<*>.createDelegate(classLoader: ClassLoader, delegate: DelegateFunction): Any { 17 | return Proxy.newProxyInstance( 18 | classLoader, 19 | arrayOf(this), 20 | DelegateProxy(delegate) 21 | ) 22 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/DummyProxy.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | import xyz.rodit.dexsearch.client.xposed.MappedObject 4 | import java.lang.reflect.InvocationHandler 5 | import java.lang.reflect.Method 6 | import java.lang.reflect.Proxy 7 | 8 | const val DummyProxyString = "DummyProxy" 9 | 10 | class DummyProxy : InvocationHandler { 11 | 12 | override fun invoke(target: Any?, method: Method, args: Array?): Any? { 13 | if (method.name == "toString") return DummyProxyString 14 | return null 15 | } 16 | } 17 | 18 | fun Class<*>.createDummyProxy(classLoader: ClassLoader): Any { 19 | return Proxy.newProxyInstance( 20 | classLoader, 21 | arrayOf(this), 22 | DummyProxy() 23 | ) 24 | } 25 | 26 | val Any.isDummyProxy: Boolean 27 | get() { 28 | return if (this is MappedObject) this.instance.isDummyProxy else this.toString() == DummyProxyString 29 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/FeatureContextUpdater.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.os.Bundle 6 | import xyz.rodit.snapmod.features.FeatureContext 7 | 8 | class FeatureContextUpdater(val context: FeatureContext) : Application.ActivityLifecycleCallbacks { 9 | 10 | override fun onActivityCreated(activity: Activity, bundle: Bundle?) { 11 | context.activity = activity 12 | } 13 | 14 | override fun onActivityStarted(activity: Activity) { 15 | 16 | } 17 | 18 | override fun onActivityResumed(activity: Activity) { 19 | 20 | } 21 | 22 | override fun onActivityPaused(activity: Activity) { 23 | 24 | } 25 | 26 | override fun onActivityStopped(activity: Activity) { 27 | 28 | } 29 | 30 | override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) { 31 | 32 | } 33 | 34 | override fun onActivityDestroyed(activity: Activity) { 35 | if (context.activity == activity) context.activity = null 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/ForceResumeActivity.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | 6 | class ForceResumeActivity : Activity() { 7 | 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | finish() 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/ObjectProxy.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | import java.lang.reflect.InvocationHandler 4 | import java.lang.reflect.Method 5 | 6 | class ObjectProxy(private val target: Any) : InvocationHandler { 7 | 8 | @Throws(Throwable::class) 9 | override fun invoke(o: Any, method: Method, args: Array?): Any? { 10 | if (args == null) return method.invoke(target) 11 | return method.invoke(target, *args) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | import android.content.Intent 4 | import android.content.pm.PackageManager 5 | import android.graphics.Color 6 | import android.graphics.ImageFormat 7 | import android.hardware.camera2.CameraCharacteristics 8 | import android.hardware.camera2.CameraManager 9 | import android.net.Uri 10 | import android.os.Bundle 11 | import android.text.InputType 12 | import android.text.Spannable 13 | import android.text.SpannableStringBuilder 14 | import android.text.Spanned 15 | import android.text.style.ForegroundColorSpan 16 | import android.view.inputmethod.EditorInfo 17 | import android.widget.EditText 18 | import androidx.appcompat.app.AlertDialog 19 | import androidx.preference.EditTextPreference 20 | import androidx.preference.ListPreference 21 | import androidx.preference.Preference 22 | import androidx.preference.PreferenceManager 23 | import xyz.rodit.xposed.SettingsActivity 24 | import xyz.rodit.xposed.utils.PathUtils 25 | import java.io.File 26 | import java.util.function.Consumer 27 | 28 | class SettingsActivity : SettingsActivity(R.xml.root_preferences) { 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | 33 | val minUpdateDelta = PreferenceManager.getDefaultSharedPreferences(this) 34 | .getString("update_frequency", "0")!!.toLong() 35 | if (minUpdateDelta >= 0) { 36 | val updateCheckFile = File(filesDir, ".update") 37 | val lastUpdateCheck = 38 | if (updateCheckFile.exists()) updateCheckFile.lastModified() else 0 39 | if (System.currentTimeMillis() - lastUpdateCheck >= minUpdateDelta) { 40 | performUpdateCheck() 41 | updateCheckFile.writeBytes(byteArrayOf()) 42 | } 43 | } 44 | } 45 | 46 | private fun performUpdateCheck() { 47 | updates.checkForUpdates("rodit", "SnapMod") 48 | } 49 | 50 | override fun onCreatePreferences(fragment: SettingsFragment) { 51 | fragment.findPreference("installation_status")?.apply { 52 | summary = getInstallationSummary(false) 53 | onPreferenceClickListener = Preference.OnPreferenceClickListener { 54 | showInfoDialog() 55 | true 56 | } 57 | } 58 | 59 | fragment.findPreference("donations")?.apply { 60 | onPreferenceClickListener = 61 | Preference.OnPreferenceClickListener { 62 | startActivity( 63 | Intent( 64 | Intent.ACTION_VIEW, 65 | Uri.parse(getString(R.string.donations_link)) 66 | ) 67 | ) 68 | true 69 | } 70 | } 71 | 72 | fragment.findPreference("check_for_updates")?.apply { 73 | onPreferenceClickListener = Preference.OnPreferenceClickListener { 74 | performUpdateCheck() 75 | true 76 | } 77 | } 78 | 79 | setNumericInput(fragment, "public_dp_resolution") 80 | setNumericInput(fragment, "location_share_lat") 81 | setNumericInput(fragment, "location_share_long") 82 | setNumericInput(fragment, "audio_playback_speed") 83 | setNumericInput(fragment, "custom_video_fps") 84 | setNumericInput(fragment, "custom_video_bitrate") 85 | 86 | (fragment.findPreference("hidden_friends") as EditTextPreference?)?.apply { 87 | setOnBindEditTextListener { 88 | it.imeOptions = EditorInfo.IME_ACTION_NONE 89 | it.inputType = InputType.TYPE_TEXT_FLAG_MULTI_LINE 90 | it.isSingleLine = false 91 | it.setSelection(it.text.length) 92 | } 93 | } 94 | 95 | val camera = getSystemService(CAMERA_SERVICE) as CameraManager 96 | val characteristics = camera.getCameraCharacteristics(camera.cameraIdList[0]) 97 | val config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) 98 | config?.let { c -> 99 | val sizes = c.getOutputSizes(ImageFormat.JPEG) 100 | val strings = sizes.map { s -> "${s.width}x${s.height}" }.toTypedArray() 101 | sequenceOf( 102 | "custom_image_resolution", 103 | "custom_video_resolution" 104 | ).map { fragment.findPreference(it) } 105 | .filterNotNull() 106 | .forEach { 107 | it.entries = strings + "Default" 108 | it.entryValues = strings + "0" 109 | } 110 | } 111 | 112 | fragment.findPreference("camera_readme")?.apply { 113 | onPreferenceClickListener = Preference.OnPreferenceClickListener { 114 | showCameraReadme() 115 | true 116 | } 117 | } 118 | } 119 | 120 | private fun setNumericInput(fragment: SettingsFragment, name: String) { 121 | (fragment.findPreference(name) as EditTextPreference?)?.apply { 122 | setOnBindEditTextListener { t: EditText -> 123 | t.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL 124 | } 125 | } 126 | } 127 | 128 | private fun showInfoDialog() { 129 | AlertDialog.Builder(this) 130 | .setTitle(R.string.installation_status_title) 131 | .setMessage(getInstallationSummary(true)) 132 | .show() 133 | } 134 | 135 | private fun showCameraReadme() { 136 | AlertDialog.Builder(this) 137 | .setTitle(R.string.camera_title) 138 | .setMessage(R.string.camera_dialog_description) 139 | .show() 140 | } 141 | 142 | private fun getInstallationSummary(detailed: Boolean): Spannable { 143 | val builder = SpannableStringBuilder() 144 | try { 145 | val info = packageManager.getPackageInfo(Shared.SNAPCHAT_PACKAGE, 0) 146 | val possible = 147 | PathUtils.getPossibleMappingFiles(this, info.versionCode.toString() + ".json") 148 | val mappings = possible.firstOrNull { obj: File -> obj.exists() } 149 | val supported = mappings != null 150 | builder.append("Snapchat Version: ") 151 | .append( 152 | info.versionName + " (" + info.versionCode + ")", 153 | ForegroundColorSpan(if (supported) Color.GREEN else Color.RED), 154 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 155 | ) 156 | .append('\n') 157 | if (supported) { 158 | builder.append( 159 | "Supported", 160 | ForegroundColorSpan(Color.GREEN), 161 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 162 | ) 163 | if (detailed) { 164 | builder.append("\n\nMappings found at ").append(mappings.toString()) 165 | } 166 | } else { 167 | builder.append( 168 | "Unsupported", 169 | ForegroundColorSpan(Color.RED), 170 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 171 | ) 172 | if (detailed) { 173 | builder.append("\n\nNo mappings found at ") 174 | possible.forEach(Consumer { f: File -> builder.append(f.path).append('\n') }) 175 | } 176 | } 177 | } catch (e: PackageManager.NameNotFoundException) { 178 | builder.append( 179 | "Error getting Snapchat package info.", 180 | ForegroundColorSpan(Color.RED), 181 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 182 | ) 183 | } 184 | 185 | if (!detailed) { 186 | builder.append("\nTap for more info.") 187 | } 188 | 189 | return builder 190 | } 191 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/Shared.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | object Shared { 4 | 5 | const val SNAPCHAT_PACKAGE = "com.snapchat.android" 6 | 7 | const val SNAPMOD_PACKAGE_NAME = "xyz.rodit.snapmod" 8 | const val SNAPMOD_CONFIG_ACTION = SNAPMOD_PACKAGE_NAME + ".ACTION_CONFIG" 9 | const val SNAPMOD_FILES_ACTION = SNAPMOD_PACKAGE_NAME + ".ACTION_FILES" 10 | const val SNAPMOD_FORCE_RESUME_ACTIVITY = SNAPMOD_PACKAGE_NAME + ".ForceResumeActivity" 11 | 12 | const val CONTEXT_HOOK_CLASS = "android.app.Application" 13 | const val CONTEXT_HOOK_METHOD = "attach" 14 | 15 | const val PINNED_FRIENDMOJI_NAME = "pinned" 16 | const val PINNED_FRIENDMOJI_EMOJI = "\uD83D\uDCCC" 17 | 18 | val MONTHS = arrayOf( 19 | "January", 20 | "February", 21 | "March", 22 | "April", 23 | "May", 24 | "June", 25 | "July", 26 | "August", 27 | "September", 28 | "October", 29 | "November", 30 | "December", 31 | "Unknown" 32 | ) 33 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/SnapHooks.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.app.admin.DevicePolicyManager 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.os.Handler 9 | import de.robv.android.xposed.XC_MethodHook 10 | import de.robv.android.xposed.XposedBridge 11 | import xyz.rodit.snapmod.features.FeatureContext 12 | import xyz.rodit.snapmod.features.InstanceManager 13 | import xyz.rodit.snapmod.logging.XLog 14 | import xyz.rodit.snapmod.logging.log 15 | import xyz.rodit.snapmod.mappings.MainActivity 16 | import xyz.rodit.snapmod.util.getList 17 | import xyz.rodit.snapmod.util.versionCode 18 | import xyz.rodit.xposed.HooksBase 19 | import xyz.rodit.xposed.mappings.LoadScheme 20 | import java.util.* 21 | 22 | class SnapHooks : HooksBase( 23 | listOf(Shared.SNAPCHAT_PACKAGE), 24 | EnumSet.of(LoadScheme.CACHED_ON_CONTEXT, LoadScheme.SERVICE), 25 | Shared.SNAPMOD_PACKAGE_NAME, 26 | Shared.SNAPMOD_CONFIG_ACTION, 27 | Shared.CONTEXT_HOOK_CLASS, 28 | Shared.CONTEXT_HOOK_METHOD 29 | ) { 30 | 31 | private var mainActivity: Activity? = null 32 | private var featureContext: FeatureContext? = null 33 | private var queueFeatureConfig = false 34 | 35 | override fun onPackageLoad() { 36 | XposedBridge.hookAllMethods( 37 | DevicePolicyManager::class.java, 38 | "getCameraDisabled", 39 | object : XC_MethodHook() { 40 | override fun beforeHookedMethod(param: MethodHookParam) { 41 | if (config == null || !config.isLoaded || config.getBoolean("disable_camera")) { 42 | param.result = true 43 | } 44 | } 45 | }) 46 | 47 | CustomResources.init() 48 | } 49 | 50 | override fun onContextHook(context: Context) { 51 | Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> 52 | log.error("Uncaught exception on thread $thread.", throwable) 53 | } 54 | } 55 | 56 | override fun onConfigLoaded(first: Boolean) { 57 | mainActivity?.let { 58 | Handler(it.mainLooper).postDelayed({ 59 | val intent = Intent().apply { 60 | setClassName( 61 | Shared.SNAPMOD_PACKAGE_NAME, 62 | Shared.SNAPMOD_FORCE_RESUME_ACTIVITY 63 | ) 64 | } 65 | it.startActivity(intent) 66 | }, 500) 67 | } 68 | 69 | XLog.globalLevel = config.getList("global_log_level") 70 | .map(String::toInt) 71 | .fold(0) { a, b -> a or b } 72 | 73 | if (featureContext != null) { 74 | featureContext!!.features.onConfigLoaded(first) 75 | } else { 76 | queueFeatureConfig = true 77 | } 78 | } 79 | 80 | override fun performHooks() { 81 | requireFileService(Shared.SNAPMOD_FILES_ACTION) 82 | requireStreamServer(0) 83 | 84 | MainActivity.attachBaseContext.hook(object : XC_MethodHook() { 85 | override fun beforeHookedMethod(param: MethodHookParam) { 86 | featureContext?.let { 87 | it.activity = param.thisObject as Activity 88 | mainActivity = it.activity 89 | } 90 | } 91 | }) 92 | 93 | featureContext = 94 | FeatureContext( 95 | appContext, 96 | lpparam.classLoader, 97 | config, 98 | files, 99 | server, 100 | InstanceManager(), 101 | appContext.versionCode 102 | ) 103 | 104 | (appContext as Application).registerActivityLifecycleCallbacks( 105 | FeatureContextUpdater(featureContext!!) 106 | ) 107 | 108 | featureContext!!.features.apply { 109 | load() 110 | init() 111 | performHooks() 112 | 113 | if (queueFeatureConfig) onConfigLoaded(true) 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/UriResolverSubscriber.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod 2 | 3 | import android.util.Log 4 | import xyz.rodit.snapmod.logging.log 5 | import xyz.rodit.snapmod.mappings.RxObserver 6 | import xyz.rodit.snapmod.util.TAG 7 | import java.lang.reflect.InvocationHandler 8 | import java.lang.reflect.Method 9 | 10 | typealias ResolutionListener = (result: Any?) -> Unit 11 | 12 | open class UriResolverSubscriber(private val listener: ResolutionListener) : InvocationHandler { 13 | 14 | override fun invoke(proxy: Any, method: Method, args: Array): Any? { 15 | if (method.name == RxObserver.accept.dexName) { 16 | Log.d(TAG, "Resolved content uri - notifying listener.") 17 | listener.invoke(args[0]) 18 | } else if (method.name == RxObserver.error.dexName) { 19 | log.error("Error resolving media uri.", args[0] as Throwable) 20 | } 21 | 22 | return null 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoMessage.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.arroyo 2 | 3 | data class ArroyoMessage(val content: String, val timestamp: Long, val senderId: String) -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoReader.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.arroyo 2 | 3 | import android.content.Context 4 | import android.database.sqlite.SQLiteDatabase 5 | import android.util.Base64 6 | import xyz.rodit.snapmod.logging.log 7 | import xyz.rodit.snapmod.util.ProtoReader 8 | import java.io.File 9 | 10 | class ArroyoReader(private val context: Context) { 11 | 12 | fun getMessageContent(conversationId: String, messageId: String): String? { 13 | val blob = getMessageBlob(conversationId, messageId) ?: return null 14 | return readChatMessageContent(blob) 15 | } 16 | 17 | fun getAllMessages(conversationId: String, after: Long = 0): Pair, Set> { 18 | val messages = mutableListOf() 19 | val senderIds = hashSetOf() 20 | SQLiteDatabase.openDatabase( 21 | File(context.filesDir, "../databases/arroyo.db").path, 22 | null, 23 | 0 24 | ).use { 25 | it.rawQuery( 26 | "SELECT message_content,creation_timestamp,sender_id FROM conversation_message WHERE client_conversation_id='$conversationId' AND creation_timestamp>$after AND content_type=1 ORDER BY creation_timestamp ASC", 27 | null 28 | ).use { cursor -> 29 | while (cursor.moveToNext()) { 30 | val content = cursor.getBlob(0) 31 | val timestamp = cursor.getLong(1) 32 | val senderId = cursor.getString(2) 33 | val contentString = readChatMessageContent(content) ?: continue 34 | messages.add(ArroyoMessage(contentString, timestamp, senderId)) 35 | senderIds.add(senderId) 36 | } 37 | } 38 | } 39 | 40 | return messages to senderIds 41 | } 42 | 43 | fun getKeyAndIv(conversationId: String, messageId: String): Pair? { 44 | val blob = getMessageBlob(conversationId, messageId) ?: return null 45 | val key = followProtoString(blob, 4, 4, 3, 3, 5, 1, 1, 4, 1) ?: return null 46 | val iv = followProtoString(blob, 4, 4, 3, 3, 5, 1, 1, 4, 2) ?: return null 47 | return Base64.decode(key, Base64.DEFAULT) to Base64.decode(iv, Base64.DEFAULT) 48 | } 49 | 50 | fun getSnapData(conversationId: String, messageId: String): Triple? { 51 | val blob = getMessageBlob(conversationId, messageId) ?: return null 52 | val key = followProto(blob, 4, 4, 11, 5, 1, 1, 19, 1) ?: return null 53 | val iv = followProto(blob, 4, 4, 11, 5, 1, 1, 19, 2) ?: return null 54 | val urlKey = followProtoString(blob, 4, 5, 1, 3, 2, 2) ?: return null 55 | return Triple(key, iv, urlKey) 56 | } 57 | 58 | private fun readChatMessageContent(blob: ByteArray): String? { 59 | return followProtoString(blob, 4, 4, 2, 1) 60 | } 61 | 62 | private fun getMessageBlob(conversationId: String, messageId: String, retries: Int = 10): ByteArray? { 63 | SQLiteDatabase.openDatabase( 64 | File(context.filesDir, "../databases/arroyo.db").path, 65 | null, 66 | 0 67 | ).use { 68 | fun getBlob(): ByteArray? { 69 | it.rawQuery( 70 | "SELECT message_content FROM conversation_message WHERE client_conversation_id='$conversationId' AND server_message_id=$messageId", 71 | null 72 | ).use { cursor -> 73 | if (cursor.moveToFirst()) return cursor.getBlob(0) 74 | } 75 | return null 76 | } 77 | for (i in 1..retries) { 78 | val blob = getBlob() 79 | if (blob != null) { 80 | return blob 81 | } 82 | if (retries > 0) { 83 | Thread.sleep(500) 84 | } 85 | } 86 | log.debug("No message found in db after $retries retries, conversationId: $conversationId, messageId: $messageId") 87 | } 88 | 89 | return null 90 | } 91 | } 92 | 93 | fun followProtoString(data: ByteArray, vararg indices: Int): String? { 94 | val proto = followProto(data, *indices) 95 | return if (proto != null) String(proto) else null 96 | } 97 | 98 | fun followProto(data: ByteArray, vararg indices: Int): ByteArray? { 99 | var current = data 100 | indices.forEach { i -> 101 | val parts = ProtoReader(current).read() 102 | current = parts.firstOrNull { it.index == i }?.value ?: return null 103 | } 104 | 105 | return current 106 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/Contextual.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features 2 | 3 | abstract class Contextual(protected val context: FeatureContext) -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/Feature.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features 2 | 3 | abstract class Feature( 4 | context: FeatureContext, val support: LongRange = LongRange(0, Long.MAX_VALUE) 5 | ) : Contextual(context) { 6 | 7 | open fun init() {} 8 | open fun onConfigLoaded(first: Boolean) {} 9 | 10 | abstract fun performHooks() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/FeatureContext.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import xyz.rodit.snapmod.arroyo.ArroyoReader 6 | import xyz.rodit.snapmod.features.callbacks.CallbackManager 7 | import xyz.rodit.snapmod.util.ConversationManager 8 | import xyz.rodit.xposed.client.ConfigurationClient 9 | import xyz.rodit.xposed.client.FileClient 10 | import xyz.rodit.xposed.client.http.StreamServer 11 | 12 | private const val PINNED_CONVERSATIONS_FILE = "pinned.list" 13 | private const val STEALTH_CONVERSATIONS_FILE = "stealth.list" 14 | private const val AUTO_SAVE_CONVERSATIONS_FILE = "auto_save.list" 15 | private const val AUTO_DOWNLOAD_CONVERSATIONS_FILE = "auto_download.list" 16 | private const val AUTO_DOWNLOAD_STORIES_FILE = "auto_download_stories.list" 17 | private const val PINNED_STORIES_FILE = "pinned_stories.list" 18 | 19 | class FeatureContext( 20 | val appContext: Context, 21 | val classLoader: ClassLoader, 22 | val config: ConfigurationClient, 23 | val files: FileClient, 24 | val server: StreamServer, 25 | val instances: InstanceManager, 26 | val appVersion: Long 27 | ) { 28 | val features: FeatureManager = FeatureManager(this) 29 | val callbacks: CallbackManager = CallbackManager() 30 | val pinned: ConversationManager = ConversationManager(appContext.filesDir, PINNED_CONVERSATIONS_FILE) 31 | val stealth: ConversationManager = ConversationManager(appContext.filesDir, STEALTH_CONVERSATIONS_FILE) 32 | val autoSave: ConversationManager = ConversationManager(appContext.filesDir, AUTO_SAVE_CONVERSATIONS_FILE) 33 | val autoDownload: ConversationManager = ConversationManager(appContext.filesDir, AUTO_DOWNLOAD_CONVERSATIONS_FILE) 34 | val autoDownloadStories: ConversationManager = ConversationManager(appContext.filesDir, AUTO_DOWNLOAD_STORIES_FILE) 35 | val pinnedStories: ConversationManager = ConversationManager(appContext.filesDir, PINNED_STORIES_FILE) 36 | val arroyo = ArroyoReader(appContext) 37 | val views = ViewInterceptor(this) 38 | 39 | var activity: Activity? = null 40 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/FeatureManager.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features 2 | 3 | import okhttp3.internal.toImmutableList 4 | import xyz.rodit.snapmod.features.chatmenu.ChatMenuModifier 5 | import xyz.rodit.snapmod.features.chatmenu.new.NewChatMenuModifier 6 | import xyz.rodit.snapmod.features.conversations.* 7 | import xyz.rodit.snapmod.features.friendsfeed.PinChats 8 | import xyz.rodit.snapmod.features.info.AdditionalFriendInfo 9 | import xyz.rodit.snapmod.features.info.NetworkLogging 10 | import xyz.rodit.snapmod.features.notifications.FilterTypes 11 | import xyz.rodit.snapmod.features.notifications.ShowMessageContent 12 | import xyz.rodit.snapmod.features.opera.CustomStoryOptions 13 | import xyz.rodit.snapmod.features.opera.OperaModelModifier 14 | import xyz.rodit.snapmod.features.saving.* 15 | import xyz.rodit.snapmod.features.tweaks.* 16 | 17 | class FeatureManager(context: FeatureContext) : Contextual(context) { 18 | 19 | private val features: MutableList = ArrayList() 20 | val pubFeatures get() = features.toImmutableList() 21 | 22 | fun load() { 23 | // Chat context menu 24 | add(::ChatMenuModifier) 25 | add(::NewChatMenuModifier) 26 | 27 | // Friends feed 28 | add(::PinChats) 29 | 30 | // Conversations/chats 31 | add(::AutoSave) 32 | add(::MessageInterceptor) 33 | add(::PreventBitmojiPresence) 34 | add(::PreventReadReceipts) 35 | add(::PreventTypingNotifications) 36 | add(::SnapInteractionFilter) 37 | add(::SnapOverrides) 38 | 39 | // Notifications 40 | add(::FilterTypes) 41 | add(::ShowMessageContent) 42 | 43 | // Opera (story/snap view) 44 | add(::CustomStoryOptions) 45 | add(::OperaModelModifier) 46 | 47 | // Saving 48 | add(::AutoDownloadSnaps) 49 | add(::AutoDownloadStories) 50 | add(::ChatSaving) 51 | add(::PublicProfileSaving) 52 | add(::StoriesSaving) 53 | 54 | // Information 55 | add(::AdditionalFriendInfo) 56 | add(::NetworkLogging) 57 | 58 | // Tweaks 59 | add(::BypassVideoLength) 60 | add(::BypassVideoLengthGlobal) 61 | add(::CameraResolution) 62 | add(::ConfigurationTweaks) 63 | add(::ConfigurationTweaks) 64 | add(::DisableBitmojis) 65 | // add(::HideFriends) 66 | add(::HideStoryReadReceipts) 67 | add(::HideStorySections) 68 | add(::HideStorySectionsLegacy) 69 | add(::PinStories) 70 | } 71 | 72 | fun init() { 73 | features.forEach { it.init() } 74 | } 75 | 76 | fun onConfigLoaded(first: Boolean) { 77 | features.forEach { it.onConfigLoaded(first) } 78 | } 79 | 80 | fun performHooks() { 81 | features.forEach { it.performHooks() } 82 | } 83 | 84 | fun add(supplier: (FeatureContext) -> Feature) { 85 | val feature = supplier(context) 86 | if (feature.support.contains(context.appVersion)) { 87 | features.add(feature) 88 | } 89 | } 90 | 91 | inline fun get(): T where T : Feature { 92 | return pubFeatures.filterIsInstance().first() 93 | } 94 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/InstanceManager.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features 2 | 3 | import xyz.rodit.snapmod.mappings.ArroyoChatCommandsClient 4 | import xyz.rodit.snapmod.mappings.ConversationManager 5 | import xyz.rodit.snapmod.mappings.FriendsRepository 6 | import xyz.rodit.snapmod.util.after 7 | 8 | class InstanceManager { 9 | 10 | var conversationManager: ConversationManager = ConversationManager.wrap(null) 11 | var friendsRepository: FriendsRepository = FriendsRepository.wrap(null) 12 | var chatCommandsClient: ArroyoChatCommandsClient = ArroyoChatCommandsClient.wrap(null) 13 | 14 | init { 15 | ConversationManager.constructors.after { 16 | conversationManager = ConversationManager.wrap(it.thisObject) 17 | } 18 | 19 | FriendsRepository.constructors.after { 20 | friendsRepository = FriendsRepository.wrap(it.thisObject) 21 | } 22 | 23 | ArroyoChatCommandsClient.constructors.after { 24 | chatCommandsClient = ArroyoChatCommandsClient.wrap(it.thisObject) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/ViewInterceptor.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import de.robv.android.xposed.XC_MethodHook 7 | import de.robv.android.xposed.XposedHelpers 8 | 9 | typealias InflateListener = (view: View) -> Unit 10 | typealias AddListener = (parent: ViewGroup, view: View) -> Unit 11 | 12 | class ViewInterceptor(context: FeatureContext) : Contextual(context) { 13 | 14 | private val inflateListeners = mutableMapOf>() 15 | private val addListeners = mutableMapOf>() 16 | 17 | private val tagWithLayout = mutableSetOf() 18 | 19 | fun onInflate(resource: String, listener: InflateListener) { 20 | inflateListeners.computeIfAbsent(getResourceId(resource)) { mutableListOf() }.add(listener) 21 | } 22 | 23 | fun onAdd(resource: String, listener: AddListener) { 24 | val resourceId = getResourceId(resource) 25 | addListeners.computeIfAbsent(resourceId) { mutableListOf() }.add(listener) 26 | tagWithLayout.add(resourceId) 27 | } 28 | 29 | private fun getResourceId(resource: String): Int { 30 | val resourceId = context.appContext.resources.getIdentifier( 31 | resource, 32 | "layout", 33 | context.appContext.packageName 34 | ) 35 | return if (resourceId == 0) throw RuntimeException("Resource for interception listener not found $resource.") else resourceId 36 | } 37 | 38 | init { 39 | XposedHelpers.findAndHookMethod( 40 | LayoutInflater::class.java, 41 | "inflate", 42 | Int::class.java, 43 | ViewGroup::class.java, 44 | Boolean::class.java, 45 | object : XC_MethodHook() { 46 | override fun afterHookedMethod(param: MethodHookParam) { 47 | val layoutId = param.args[0] 48 | if (layoutId !is Int) return 49 | 50 | val view = param.result 51 | if (view !is View) return 52 | 53 | if (tagWithLayout.contains(layoutId)) view.tag = layoutId 54 | 55 | inflateListeners[layoutId]?.let { 56 | it.forEach { listener -> listener(view) } 57 | } 58 | } 59 | }) 60 | 61 | XposedHelpers.findAndHookMethod( 62 | ViewGroup::class.java, 63 | "addView", 64 | View::class.java, 65 | Int::class.java, 66 | ViewGroup.LayoutParams::class.java, 67 | object : XC_MethodHook() { 68 | override fun afterHookedMethod(param: MethodHookParam) { 69 | val parent = param.thisObject as ViewGroup 70 | val view = param.args[0] 71 | if (view !is View) return 72 | 73 | val layoutId = view.tag 74 | if (layoutId !is Int) return 75 | 76 | addListeners[layoutId]?.let { 77 | it.forEach { listener -> listener(parent, view) } 78 | } 79 | } 80 | } 81 | ) 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/callbacks/CallbackManager.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.callbacks 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | import xyz.rodit.dexsearch.client.xposed.MethodRef 5 | import xyz.rodit.snapmod.isDummyProxy 6 | import xyz.rodit.snapmod.mappings.DefaultFetchConversationCallback 7 | import xyz.rodit.snapmod.mappings.DefaultFetchMessageCallback 8 | import xyz.rodit.snapmod.util.before 9 | import kotlin.reflect.KClass 10 | 11 | typealias HookedCallback = (XC_MethodHook.MethodHookParam) -> Boolean 12 | 13 | class CallbackManager { 14 | 15 | private val callbacks = mutableMapOf>() 16 | 17 | fun hook(type: KClass<*>, method: MethodRef, obtainInterface: (Any) -> Any) { 18 | method.before { 19 | if (!obtainInterface(it.thisObject).isDummyProxy) return@before 20 | 21 | callbacks["${type.simpleName}:${method.name}"]?.let { list -> 22 | val remove = list.filter { c -> c(it) } 23 | list.removeAll(remove) 24 | } 25 | 26 | it.result = null 27 | } 28 | } 29 | 30 | fun on(type: KClass<*>, method: MethodRef, callback: HookedCallback) { 31 | callbacks.computeIfAbsent("${type.simpleName}:${method.name}") { mutableListOf() }.add(callback) 32 | } 33 | 34 | init { 35 | hook( 36 | DefaultFetchConversationCallback::class, 37 | DefaultFetchConversationCallback.onFetchConversationWithMessagesComplete 38 | ) { DefaultFetchConversationCallback.wrap(it).dummy } 39 | 40 | hook( 41 | DefaultFetchMessageCallback::class, 42 | DefaultFetchMessageCallback.onFetchMessageComplete 43 | ) { DefaultFetchMessageCallback.wrap(it).dummy } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ButtonOption.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu 2 | 3 | import xyz.rodit.snapmod.features.FeatureContext 4 | import xyz.rodit.snapmod.mappings.* 5 | 6 | abstract class ButtonOption(context: FeatureContext, name: String, private val textResource: Int) : 7 | MenuPlugin(context, name) { 8 | 9 | override fun createModel(key: String?): Any { 10 | val actionDataModel = SendChatActionDataModel(createEventData(key!!), false) 11 | val action = SendChatAction(actionDataModel) 12 | val textViewModel = ActionMenuOptionTextViewModel(textResource, null, null, null, null, 62) 13 | return ActionMenuOptionItemViewModel( 14 | textViewModel, 15 | ActionMenuActionModel(arrayOf(action.instance)), 16 | 0, 17 | null, 18 | null, 19 | null, 20 | null, 21 | false, 22 | null, 23 | ActionMenuOptionItemType.OPTION_ITEM() 24 | ).instance 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ChatMenuModifier.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu 2 | 3 | import xyz.rodit.snapmod.CustomResources.string.menu_option_auto_download 4 | import xyz.rodit.snapmod.CustomResources.string.menu_option_auto_save 5 | import xyz.rodit.snapmod.CustomResources.string.menu_option_stealth_mode 6 | import xyz.rodit.snapmod.Shared 7 | import xyz.rodit.snapmod.features.Feature 8 | import xyz.rodit.snapmod.features.FeatureContext 9 | import xyz.rodit.snapmod.mappings.FriendChatActionHandler 10 | import xyz.rodit.snapmod.mappings.FriendChatActionMenuBuilder 11 | import xyz.rodit.snapmod.mappings.RxSingleton 12 | import xyz.rodit.snapmod.mappings.SendChatAction 13 | import xyz.rodit.snapmod.util.after 14 | import xyz.rodit.snapmod.util.before 15 | 16 | const val EVENT_PREFIX = "CUSTOM_ACTION" 17 | const val EVENT_DELIMITER = "\u0000:\u0000" 18 | const val PIN_STRING_NAME = "action_menu_pin_conversation" 19 | 20 | class ChatMenuModifier(context: FeatureContext) : Feature(context) { 21 | 22 | private val plugins = mutableMapOf() 23 | 24 | override fun init() { 25 | registerPlugin(PreviewOption(context)) 26 | registerPlugin(ExportOption(context)) 27 | 28 | val pinTextResource = context.appContext.resources.getIdentifier( 29 | PIN_STRING_NAME, 30 | "string", 31 | Shared.SNAPCHAT_PACKAGE 32 | ) 33 | registerConversationToggle("pinning", pinTextResource) { it.pinned } 34 | registerConversationToggle("stealth", menu_option_stealth_mode) { it.stealth } 35 | registerConversationToggle("auto_save", menu_option_auto_save) { it.autoSave } 36 | registerConversationToggle("auto_download", menu_option_auto_download) { it.autoDownload } 37 | } 38 | 39 | private fun registerConversationToggle(name: String, textResource: Int, manager: Manager) { 40 | registerPlugin(ConversationToggleOption(context, name, textResource, manager)) 41 | } 42 | 43 | private fun registerPlugin(plugin: MenuPlugin) { 44 | plugins[plugin.name] = plugin 45 | } 46 | 47 | override fun performHooks() { 48 | // Add plugin actions to chat action menu. 49 | FriendChatActionMenuBuilder.build.after { 50 | val self = FriendChatActionMenuBuilder.wrap(it.thisObject) 51 | val key = self.feedInfoHolder.info.key 52 | val options = RxSingleton.wrap(it.result).value as MutableList 53 | 54 | options.addAll( 55 | plugins.values.filter(MenuPlugin::shouldCreate).map { p -> p.createModel(key) }) 56 | } 57 | 58 | // Override plugin action events. 59 | FriendChatActionHandler.handle.before { 60 | if (!SendChatAction.isInstance(it.args[0])) return@before 61 | 62 | val action = SendChatAction.wrap(it.args[0]) 63 | val data = action.dataModel.key 64 | val parts = data.split(EVENT_DELIMITER) 65 | if (parts[0] == EVENT_PREFIX && parts.size == 3) { 66 | val pluginName = parts[1] 67 | plugins[pluginName]?.let { p -> 68 | p.handleEvent(parts[2]) 69 | it.result = null 70 | } 71 | } 72 | } 73 | 74 | plugins.values.forEach(MenuPlugin::performHooks) 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ConversationToggleOption.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu 2 | 3 | import xyz.rodit.snapmod.features.FeatureContext 4 | import xyz.rodit.snapmod.util.ConversationManager 5 | import xyz.rodit.snapmod.util.getList 6 | 7 | typealias Manager = (FeatureContext) -> ConversationManager 8 | 9 | class ConversationToggleOption( 10 | context: FeatureContext, 11 | name: String, 12 | textResource: Int, 13 | private val manager: Manager 14 | ) : ToggleOption(context, name, textResource) { 15 | 16 | override fun shouldCreate() = !context.config.getList("hidden_chat_options").contains(name) 17 | 18 | override fun isToggled(key: String?) = manager(context).isEnabled(key) 19 | 20 | override fun handleEvent(data: String?) = manager(context).toggle(data) 21 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ExportOption.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu 2 | 3 | import xyz.rodit.snapmod.CustomResources.string.menu_option_export 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.features.chatmenu.shared.export 6 | 7 | class ExportOption(context: FeatureContext): 8 | ButtonOption(context, "export_chat", menu_option_export) { 9 | 10 | override fun shouldCreate() = true 11 | 12 | override fun handleEvent(data: String?) { 13 | if (data == null) return 14 | 15 | export(context, data) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/MenuPlugin.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu 2 | 3 | import xyz.rodit.snapmod.features.Contextual 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | 6 | abstract class MenuPlugin(context: FeatureContext, val name: String) : Contextual(context) { 7 | 8 | abstract fun shouldCreate(): Boolean 9 | 10 | abstract fun createModel(key: String?): Any 11 | 12 | abstract fun handleEvent(data: String?) 13 | 14 | open fun performHooks() { 15 | 16 | } 17 | } 18 | 19 | fun MenuPlugin.createEventData(key: String): String { 20 | return EVENT_PREFIX + EVENT_DELIMITER + name + EVENT_DELIMITER + key 21 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/PreviewOption.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu 2 | 3 | import xyz.rodit.snapmod.CustomResources.string.menu_option_preview 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.features.chatmenu.shared.previewChat 6 | 7 | class PreviewOption(context: FeatureContext) : 8 | ButtonOption(context, "preview", menu_option_preview) { 9 | 10 | override fun shouldCreate() = true 11 | 12 | override fun handleEvent(data: String?) { 13 | if (data == null) return 14 | 15 | previewChat(context, data) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ToggleOption.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu 2 | 3 | import xyz.rodit.snapmod.features.FeatureContext 4 | import xyz.rodit.snapmod.mappings.* 5 | 6 | abstract class ToggleOption(context: FeatureContext, name: String, private val textResource: Int) : 7 | MenuPlugin(context, name) { 8 | 9 | protected abstract fun isToggled(key: String?): Boolean 10 | 11 | override fun createModel(key: String?): Any { 12 | val actionDataModel = SendChatActionDataModel(createEventData(key!!), false) 13 | val action = SendChatAction(actionDataModel) 14 | val textViewModel = ActionMenuOptionTextViewModel(textResource, null, null, null, null, 62) 15 | return ActionMenuOptionToggleItemViewModel( 16 | textViewModel, 17 | ActionMenuActionModel(arrayOf(action.instance)), 18 | isToggled(key) 19 | ).instance 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/new/MenuPlugin.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu.new 2 | 3 | abstract class MenuPlugin { 4 | 5 | abstract fun shouldCreate(): Boolean 6 | 7 | abstract fun createModel(key: String): Any 8 | 9 | open fun performHooks() { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/new/NewChatMenuModifier.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu.new 2 | 3 | import xyz.rodit.snapmod.createDelegate 4 | import xyz.rodit.snapmod.features.Feature 5 | import xyz.rodit.snapmod.features.FeatureContext 6 | import xyz.rodit.snapmod.features.chatmenu.shared.export 7 | import xyz.rodit.snapmod.features.chatmenu.shared.previewChat 8 | import xyz.rodit.snapmod.mappings.* 9 | import xyz.rodit.snapmod.util.before 10 | 11 | class NewChatMenuModifier(context: FeatureContext) : Feature(context) { 12 | 13 | private val plugins = mutableListOf() 14 | 15 | override fun init() { 16 | registerPlain("Export") { export(context, it) } 17 | registerPlain("Preview") { previewChat(context, it) } 18 | 19 | registerSwitch("pinning", "Pin Conversation") { it.pinned } 20 | registerSwitch("stealth", "Stealth Mode") { it.stealth } 21 | registerSwitch("auto_save", "Auto-Save Messages") { it.autoSave } 22 | registerSwitch("auto_download", "Auto-Download Snaps") { it.autoDownload } 23 | } 24 | 25 | private fun registerPlugin(plugin: MenuPlugin) { 26 | plugins.add(plugin) 27 | } 28 | 29 | private fun registerPlain(text: String, click: ClickHandler) { 30 | registerPlugin(PlainOption(context, text, click)) 31 | } 32 | 33 | private fun registerSwitch(name: String, text: String, manager: Manager) { 34 | registerPlugin(SwitchOption(context, name, text, manager)) 35 | } 36 | 37 | override fun performHooks() { 38 | // Force new chat action menu 39 | ProfileActionSheetChooser.choose.before { 40 | it.args[0] = true 41 | } 42 | 43 | // Add subsection 44 | ProfileActionSheetCreator.apply.before { 45 | if (it.args[0] !is List<*>) return@before 46 | 47 | val newItems = (it.args[0] as List<*>).toMutableList() 48 | val creator = ProfileActionSheetCreator.wrap(it.thisObject) 49 | if (!NestedActionMenuContext.isInstance(creator.nestedContext) 50 | || !ActionMenuContext.isInstance(creator.actionMenuContext)) return@before 51 | 52 | val nestedContext = NestedActionMenuContext.wrap(creator.nestedContext) 53 | val actionContext = ActionMenuContext.wrap(creator.actionMenuContext) 54 | val key = actionContext.feedInfo.key 55 | 56 | val subOptions = plugins.filter(MenuPlugin::shouldCreate).map { p -> 57 | p.createModel(key) 58 | } 59 | val clickProxy = 60 | Func0.getMappedClass().createDelegate(context.classLoader) { _, _ -> 61 | NestedActionMenuContext.display( 62 | nestedContext, 63 | "SnapMod", 64 | subOptions 65 | ) 66 | null 67 | } 68 | val snapModSettings = 69 | ActionClickableCaret("SnapMod Settings", null, Func0.wrap(clickProxy)).instance 70 | newItems.add(snapModSettings) 71 | 72 | it.args[0] = newItems 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/new/PlainOption.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu.new 2 | 3 | import xyz.rodit.snapmod.createDelegate 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.ActionPlain 6 | import xyz.rodit.snapmod.mappings.Func0 7 | 8 | typealias ClickHandler = (key: String) -> Unit 9 | 10 | class PlainOption( 11 | private val context: FeatureContext, 12 | private val text: String, 13 | private val click: ClickHandler 14 | ) : MenuPlugin() { 15 | 16 | override fun shouldCreate() = true 17 | 18 | override fun createModel(key: String): Any = ActionPlain( 19 | text, 20 | Func0.wrap(Func0.getMappedClass().createDelegate(context.classLoader) { _, _ -> 21 | click(key) 22 | }) 23 | ).instance 24 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/new/SwitchOption.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu.new 2 | 3 | import xyz.rodit.snapmod.createDelegate 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.ActionSwitch 6 | import xyz.rodit.snapmod.mappings.Func1 7 | import xyz.rodit.snapmod.util.ConversationManager 8 | import xyz.rodit.snapmod.util.getList 9 | 10 | typealias Manager = (FeatureContext) -> ConversationManager 11 | 12 | class SwitchOption( 13 | private val context: FeatureContext, 14 | private val name: String, 15 | private val text: String, 16 | private val manager: Manager 17 | ) : MenuPlugin() { 18 | 19 | override fun shouldCreate() = !context.config.getList("hidden_chat_options").contains(name) 20 | 21 | override fun createModel(key: String): Any = ActionSwitch( 22 | text, 23 | manager(context).isEnabled(key), 24 | Func1.wrap(Func1.getMappedClass().createDelegate(context.classLoader) { _, _ -> true }), 25 | Func1.wrap(Func1.getMappedClass().createDelegate(context.classLoader) { _, _ -> 26 | manager(context).toggle(key) 27 | true 28 | }), 29 | null, 30 | 0 31 | ).instance 32 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/shared/ExportChat.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu.shared 2 | 3 | import android.content.Intent 4 | import androidx.core.content.FileProvider 5 | import xyz.rodit.snapmod.features.FeatureContext 6 | import xyz.rodit.snapmod.mappings.SelectFriendsByUserIds 7 | import java.io.File 8 | import java.text.SimpleDateFormat 9 | import java.util.* 10 | 11 | fun export(context: FeatureContext, key: String) { 12 | val (messages, senders) = context.arroyo.getAllMessages(key) 13 | val friendData = 14 | context.instances.friendsRepository.selectFriendsByUserIds(senders.toList()) 15 | val senderMap = friendData.map(SelectFriendsByUserIds::wrap).associateBy { u -> u.userId } 16 | 17 | val dateFormat = SimpleDateFormat("dd/MM/yyyy, HH:mm:ss", Locale.getDefault()) 18 | 19 | val temp = File.createTempFile( 20 | "Snapchat Export ", 21 | ".txt", 22 | File(context.appContext.filesDir, "file_manager/media") 23 | ) 24 | temp.deleteOnExit() 25 | temp.bufferedWriter().use { 26 | messages.forEach { m -> 27 | val username = senderMap[m.senderId]?.displayName ?: "Unknown" 28 | val dateTime = dateFormat.format(m.timestamp) 29 | it.append(dateTime) 30 | .append(" - ") 31 | .append(username) 32 | .append(": ") 33 | .appendLine(m.content) 34 | } 35 | } 36 | 37 | val intent = Intent(Intent.ACTION_SEND) 38 | .setType("text/plain") 39 | .putExtra( 40 | Intent.EXTRA_STREAM, 41 | FileProvider.getUriForFile( 42 | context.appContext, 43 | "com.snapchat.android.media.fileprovider", 44 | temp 45 | ) 46 | ) 47 | context.activity?.startActivity(Intent.createChooser(intent, "Export Chat")) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/chatmenu/shared/PreviewChat.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.chatmenu.shared 2 | 3 | import android.app.AlertDialog 4 | import android.util.Base64 5 | import de.robv.android.xposed.XC_MethodHook 6 | import xyz.rodit.snapmod.arroyo.followProtoString 7 | import xyz.rodit.snapmod.createDummyProxy 8 | import xyz.rodit.snapmod.features.FeatureContext 9 | import xyz.rodit.snapmod.mappings.* 10 | import xyz.rodit.snapmod.util.toSnapUUID 11 | import xyz.rodit.snapmod.util.toUUIDString 12 | import xyz.rodit.xposed.utils.StreamUtils 13 | import java.io.File 14 | 15 | fun previewChat(context: FeatureContext, key: String) { 16 | val uuid = key.toSnapUUID() 17 | val proxy = 18 | ConversationDummyInterface.wrap( 19 | ConversationDummyInterface.getMappedClass().createDummyProxy(context.classLoader) 20 | ) 21 | 22 | context.callbacks.on( 23 | DefaultFetchConversationCallback::class, 24 | DefaultFetchConversationCallback.onFetchConversationWithMessagesComplete 25 | ) { displayPreview(context, it) } 26 | 27 | context.instances.conversationManager.fetchConversationWithMessages( 28 | uuid, 29 | DefaultFetchConversationCallback(proxy, uuid, false) 30 | ) 31 | } 32 | 33 | private fun displayPreview(context: FeatureContext, param: XC_MethodHook.MethodHookParam): Boolean { 34 | val conversation = Conversation.wrap(param.args[0]) 35 | 36 | val userIds = conversation.participants.map(Participant::wrap) 37 | .map { p -> (p.participantId.id as ByteArray).toUUIDString() } 38 | val friendData = context.instances.friendsRepository.selectFriendsByUserIds(userIds) 39 | val userMap = friendData.map(SelectFriendsByUserIds::wrap).associateBy { u -> u.userId } 40 | 41 | val messageList = param.args[1] as List<*> 42 | val previewText = StringBuilder() 43 | if (messageList.isEmpty()) previewText.append("No messages available.") 44 | else { 45 | val numMessages = 46 | Integer.min(context.config.getInt("preview_messages_count", 5), messageList.size) 47 | previewText.append("Last ").append(numMessages).append(" messages:") 48 | messageList.takeLast(numMessages) 49 | .map(Message::wrap).forEach { m -> 50 | run { 51 | val uuidString = m.senderId.toUUIDString() 52 | val displayName = userMap[uuidString]?.displayName ?: "Unknown" 53 | previewText.append('\n').append(displayName).append(": ") 54 | if (m.messageContent.contentType.instance == ContentType.CHAT().instance) { 55 | previewText.append(followProtoString(m.messageContent.content as ByteArray, 2, 1)) 56 | } else { 57 | previewText.append(m.messageContent.contentType.instance) 58 | } 59 | } 60 | } 61 | } 62 | 63 | userMap.values.find { f -> (f.streakExpiration ?: 0L) > 0L }?.let { f -> 64 | val hourDiff = 65 | (f.streakExpiration - System.currentTimeMillis()).toDouble() / 3600000.0 66 | previewText.append("\n\nStreak Expires in ") 67 | .append(String.format("%.1f", hourDiff)) 68 | .append(" hours") 69 | } 70 | 71 | context.activity?.runOnUiThread { 72 | AlertDialog.Builder(context.activity) 73 | .setTitle(if (conversation.title.isNullOrBlank()) "Chat Preview" else conversation.title) 74 | .setMessage(previewText) 75 | .setPositiveButton("Ok") { _, _ -> } 76 | .show() 77 | } 78 | 79 | return true 80 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/AutoSave.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.logging.log 6 | import xyz.rodit.snapmod.mappings.ArroyoConvertMessagesAction 7 | import xyz.rodit.snapmod.mappings.ChatCommandSource 8 | import xyz.rodit.snapmod.mappings.Message 9 | import xyz.rodit.snapmod.util.* 10 | 11 | class AutoSave(context: FeatureContext) : Feature(context, 84608.toMax()) { 12 | 13 | private val messageTypes = hashSetOf() 14 | 15 | override fun onConfigLoaded(first: Boolean) { 16 | messageTypes.clear() 17 | messageTypes.addAll(context.config.getList("auto_save_types")) 18 | } 19 | 20 | override fun performHooks() { 21 | ArroyoConvertMessagesAction.apply.before { 22 | if (it.args[0] !is List<*>) return@before 23 | if (context.instances.chatCommandsClient.isNull) { 24 | log.debug("Cannot auto-save messages. Chat commands client was null.") 25 | return@before 26 | } 27 | 28 | val messages = 29 | (it.args[0] as List<*>).map { p -> p?.pairFirst } 30 | .filter(Message::isInstance) 31 | .map { m -> Message.wrap(m) } 32 | messages.filter(Message::isNotNull).forEach { m -> 33 | val descriptor = m.descriptor 34 | val conversationId = descriptor.conversationId.toUUIDString() 35 | 36 | if (!context.config.getBoolean("auto_save_all_chats") 37 | && !context.autoSave.isEnabled(conversationId) 38 | ) return@forEach 39 | 40 | val contentType = m.messageContent.contentType.instance.toString() 41 | if (!messageTypes.contains(contentType)) return@forEach 42 | 43 | val savedBy = m.metadata.savedBy 44 | if (!savedBy.isNullOrEmpty()) return@forEach 45 | 46 | val arroyoId = createArroyoId(conversationId, descriptor.messageId) 47 | context.instances.chatCommandsClient.saveMessage( 48 | null, 49 | arroyoId, 50 | true, 51 | false, 52 | ChatCommandSource.CHAT(), 53 | false 54 | ) 55 | } 56 | } 57 | } 58 | 59 | private fun createArroyoId(conversationId: String, messageId: Long): String { 60 | return "$conversationId:arroyo-m-id:$messageId" 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/MessageInterceptor.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import xyz.rodit.snapmod.features.FeatureContext 4 | import xyz.rodit.snapmod.mappings.* 5 | import xyz.rodit.snapmod.util.toUUIDString 6 | 7 | class MessageInterceptor(context: FeatureContext) : StealthFeature(context) { 8 | 9 | override fun init() { 10 | setClass(ConversationManager.getMapping()) 11 | 12 | putFilters( 13 | ConversationManager.sendMessageWithContent, 14 | { LocalMessageContent.wrap(it.args[1]).contentType }, 15 | { 16 | val conversations = MessageDestinations.wrap(it.args[0]).conversations 17 | if (conversations.isNotEmpty()) conversations[0].toUUIDString() else null 18 | }, 19 | ObjectFilter( 20 | context, 21 | "hide_screenshot", 22 | ContentType.STATUS_CONVERSATION_CAPTURE_SCREENSHOT(), 23 | ContentType.STATUS_CONVERSATION_CAPTURE_RECORD() 24 | ), 25 | ObjectFilter(context, "hide_save_gallery", ContentType.STATUS_SAVE_TO_CAMERA_ROLL()) 26 | ) 27 | 28 | putFilters( 29 | ConversationManager.updateMessage, 30 | { MessageUpdate.wrap(it.args[2]) }, 31 | { it.args[0].toUUIDString() }, 32 | ObjectFilter(context, "hide_read", MessageUpdate.READ()), 33 | ObjectFilter( 34 | context, 35 | "hide_screenshot", 36 | MessageUpdate.SCREENSHOT(), 37 | MessageUpdate.SCREEN_RECORD() 38 | ) 39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/NullFilter.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import xyz.rodit.snapmod.features.FeatureContext 4 | 5 | class NullFilter(context: FeatureContext, configKey: String) : ObjectFilter(context, configKey, null) { 6 | 7 | override fun shouldFilter(item: Any?): Boolean { 8 | return true 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/ObjectFilter.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import xyz.rodit.dexsearch.client.xposed.MappedObject 4 | import xyz.rodit.snapmod.features.Contextual 5 | import xyz.rodit.snapmod.features.FeatureContext 6 | import xyz.rodit.snapmod.features.shared.Filter 7 | 8 | open class ObjectFilter(context: FeatureContext, private val configKey: String, vararg filtered: T) : Contextual(context), Filter { 9 | 10 | private val filtered: MutableSet = HashSet() 11 | 12 | override val isEnabled: Boolean 13 | get() = context.config.getBoolean(configKey) 14 | 15 | override fun shouldFilter(item: Any?): Boolean { 16 | val compare = if (item is MappedObject) item.instance else item 17 | return filtered.contains(compare) 18 | } 19 | 20 | init { 21 | this.filtered.addAll(filtered.mapNotNull { if (it is MappedObject) it.instance else it }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/PreventBitmojiPresence.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import xyz.rodit.snapmod.features.FeatureContext 4 | import xyz.rodit.snapmod.mappings.ArroyoMessageListDataProvider 5 | import xyz.rodit.snapmod.mappings.ChatContext 6 | import xyz.rodit.snapmod.mappings.PresenceSession 7 | import xyz.rodit.snapmod.util.before 8 | 9 | class PreventBitmojiPresence(context: FeatureContext) : StealthFeature(context) { 10 | 11 | private var currentConversationId: String = "" 12 | 13 | override fun init() { 14 | setClass(PresenceSession.getMapping()) 15 | 16 | putFilters( 17 | PresenceSession.activate, 18 | { null }, 19 | { currentConversationId }, 20 | NullFilter(context, "hide_bitmoji_presence") 21 | ) 22 | 23 | putFilters( 24 | PresenceSession.deactivate, 25 | { null }, 26 | { currentConversationId }, 27 | NullFilter(context, "hide_bitmoji_presence") 28 | ) 29 | 30 | putFilters( 31 | PresenceSession.processTypingActivity, 32 | { null }, 33 | { currentConversationId }, 34 | NullFilter(context, "hide_bitmoji_presence") 35 | ) 36 | } 37 | 38 | override fun performHooks() { 39 | super.performHooks() 40 | 41 | ArroyoMessageListDataProvider.enterConversation.before { 42 | currentConversationId = ChatContext.wrap(it.args[0]).conversationId.orEmpty() 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/PreventReadReceipts.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import xyz.rodit.snapmod.features.FeatureContext 4 | import xyz.rodit.snapmod.mappings.ConversationManager 5 | import xyz.rodit.snapmod.util.toUUIDString 6 | 7 | class PreventReadReceipts(context: FeatureContext) : StealthFeature(context) { 8 | 9 | override fun init() { 10 | setClass(ConversationManager.getMapping()) 11 | 12 | putFilters( 13 | ConversationManager.displayedMessages, 14 | { null }, 15 | { it.args[0].toUUIDString() }, 16 | NullFilter(context, "hide_read") 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/PreventTypingNotifications.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import de.robv.android.xposed.XC_MethodHook.MethodHookParam 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.Callback 6 | import xyz.rodit.snapmod.mappings.ConversationManager 7 | import xyz.rodit.snapmod.util.toUUIDString 8 | 9 | class PreventTypingNotifications(context: FeatureContext?) : StealthFeature(context!!) { 10 | 11 | override fun init() { 12 | setClass(ConversationManager.getMapping()) 13 | 14 | putFilters( 15 | ConversationManager.sendTypingNotification, 16 | { null }, 17 | { it.args[0].toUUIDString() }, 18 | NullFilter(context, "hide_typing") 19 | ) 20 | } 21 | 22 | override fun onPostHook(param: MethodHookParam) { 23 | param.args.filter(Callback::isInstance).map(Callback::wrap).first()?.onSuccess() 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/SnapInteractionFilter.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import xyz.rodit.snapmod.features.FeatureContext 4 | import xyz.rodit.snapmod.mappings.SnapInteractionType 5 | import xyz.rodit.snapmod.mappings.SnapManager 6 | import xyz.rodit.snapmod.util.toUUIDString 7 | 8 | class SnapInteractionFilter(context: FeatureContext) : StealthFeature(context) { 9 | 10 | override fun init() { 11 | setClass(SnapManager.getMapping()) 12 | 13 | putFilters( 14 | SnapManager.onSnapInteraction, 15 | { SnapInteractionType.wrap(it.args[0]) }, 16 | { it.args[1].toUUIDString() }, 17 | ObjectFilter( 18 | context, 19 | "hide_snap_views", 20 | SnapInteractionType.VIEWING_INITIATED(), 21 | SnapInteractionType.VIEWING_FINISHED(), 22 | SnapInteractionType.MARK_AS_INVALID() 23 | ) 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/SnapOverrides.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.* 6 | import xyz.rodit.snapmod.util.before 7 | 8 | class SnapOverrides(context: FeatureContext) : Feature(context) { 9 | 10 | override fun performHooks() { 11 | // Hook message sending to convert gallery to live snap. 12 | MessageSenderCrossroad.apply.before(context, "override_snap") { 13 | val self = MessageSenderCrossroad.wrap(it.thisObject) 14 | val container = self.payload.media 15 | 16 | if (!SerializableContent.isInstance(container.instance)) return@before 17 | 18 | val content = SerializableContent.wrap(container.instance) 19 | val message = content.message 20 | if (!GallerySnapMedia.isInstance(message.instance)) return@before 21 | 22 | val id = GallerySnapMedia.wrap(message.instance).media.id 23 | val snap = LiveSnapMedia().apply { mediaId = id } 24 | 25 | val paramPackage = ParameterPackage( 26 | true, 27 | 0.0, 28 | null, 29 | null, 30 | null, 31 | null, 32 | null, 33 | null, 34 | null, 35 | null, 36 | null, 37 | null, 38 | false 39 | ) 40 | snap.parameterPackage = paramPackage 41 | content.message = MediaBaseBase.wrap(snap.instance) 42 | } 43 | 44 | MessageTypeChecker.isMediaOverLimit.before(context, "override_snap") { 45 | it.result = false 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/conversations/StealthFeature.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.conversations 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | import de.robv.android.xposed.XC_MethodHook.MethodHookParam 5 | import xyz.rodit.dexsearch.client.ClassMapping 6 | import xyz.rodit.dexsearch.client.xposed.MappedObject 7 | import xyz.rodit.dexsearch.client.xposed.MethodRef 8 | import xyz.rodit.snapmod.features.Feature 9 | import xyz.rodit.snapmod.features.FeatureContext 10 | import xyz.rodit.snapmod.features.shared.Filter 11 | 12 | typealias FilterObjectSupplier = (MethodHookParam) -> Any? 13 | typealias ConversationIdSupplier = (MethodHookParam) -> String? 14 | 15 | abstract class StealthFeature(context: FeatureContext) : Feature(context) { 16 | 17 | private val filters: MutableMap> = HashMap() 18 | private val suppliers: MutableMap = HashMap() 19 | private val conversationIdSuppliers: MutableMap = HashMap() 20 | 21 | private var className: String? = null 22 | 23 | fun setClass(mapping: ClassMapping) { 24 | className = mapping.niceClassName 25 | } 26 | 27 | fun putFilters( 28 | method: MethodRef, 29 | supplier: FilterObjectSupplier, 30 | conversationIdSupplier: ConversationIdSupplier, 31 | vararg filters: Filter 32 | ) { 33 | putFilters(method.name, supplier, conversationIdSupplier, *filters) 34 | } 35 | 36 | fun putFilters( 37 | methodName: String, 38 | supplier: FilterObjectSupplier, 39 | conversationIdSupplier: ConversationIdSupplier, 40 | vararg filters: Filter 41 | ) { 42 | this.filters.computeIfAbsent(methodName) { ArrayList() }.addAll(filters) 43 | suppliers[methodName] = supplier 44 | conversationIdSuppliers[methodName] = conversationIdSupplier 45 | } 46 | 47 | protected open fun onPostHook(param: MethodHookParam) {} 48 | 49 | override fun performHooks() { 50 | filters.forEach { 51 | val methodName = it.key 52 | val supplier: FilterObjectSupplier? = suppliers[methodName] 53 | val conversationIdSupplier = conversationIdSuppliers[methodName] 54 | 55 | MappedObject.hook(className, methodName, object : XC_MethodHook() { 56 | override fun beforeHookedMethod(param: MethodHookParam) { 57 | val obj = supplier?.invoke(param) 58 | val id = conversationIdSupplier?.invoke(param) ?: return 59 | val stealth = context.stealth.isEnabled(id) 60 | 61 | if (filters[methodName]!!.any { f -> 62 | (f.isEnabled || stealth) && f.shouldFilter(obj) 63 | }) { 64 | param.result = null 65 | onPostHook(param) 66 | } 67 | } 68 | }) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/friendsfeed/PinChats.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.friendsfeed 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.FriendsFeedConfig 6 | import xyz.rodit.snapmod.mappings.FriendsFeedView 7 | import xyz.rodit.snapmod.util.after 8 | 9 | class PinChats(context: FeatureContext) : Feature(context) { 10 | 11 | override fun performHooks() { 12 | FriendsFeedConfig.constructors.after(context, "allow_pin_chats") { 13 | FriendsFeedConfig.wrap(it.thisObject).isPinConversationsEnabled = true 14 | } 15 | 16 | FriendsFeedView.constructors.after(context, "allow_pin_chats") { 17 | val view = FriendsFeedView.wrap(it.thisObject) 18 | if (context.pinned.isEnabled(view.key)) { 19 | view.pinnedTimestamp = view.lastInteractionTimestamp 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/info/AdditionalFriendInfo.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.info 2 | 3 | import xyz.rodit.snapmod.Shared 4 | import xyz.rodit.snapmod.features.Feature 5 | import xyz.rodit.snapmod.features.FeatureContext 6 | import xyz.rodit.snapmod.mappings.CalendarDate 7 | import xyz.rodit.snapmod.mappings.FooterInfoItem 8 | import xyz.rodit.snapmod.mappings.FriendProfilePageData 9 | import xyz.rodit.snapmod.mappings.FriendProfileTransformer 10 | import xyz.rodit.snapmod.util.after 11 | import java.text.SimpleDateFormat 12 | import java.util.* 13 | import kotlin.math.max 14 | 15 | class AdditionalFriendInfo(context: FeatureContext) : Feature(context) { 16 | 17 | override fun performHooks() { 18 | // Show more info in friend profile footer. 19 | FriendProfileTransformer.apply.after(context, "more_profile_info") { 20 | val transformer = FriendProfileTransformer.wrap(it.thisObject) 21 | if (!FriendProfilePageData.isInstance(transformer.data) || it.result !is List<*>) return@after 22 | 23 | val viewModelList = it.result as List<*> 24 | if (viewModelList.isEmpty()) return@after 25 | 26 | val viewModel = viewModelList[0]!! 27 | if (!FooterInfoItem.isInstance(viewModel)) return@after 28 | 29 | val data = FriendProfilePageData.wrap(transformer.data) 30 | val friendDate = Date(max(data.addedTimestamp, data.reverseAddedTimestamp)) 31 | val birthday = if (data.birthday.isNull) CalendarDate(13, -1) else data.birthday 32 | 33 | val info = """Friends with ${data.displayName} since ${ 34 | SimpleDateFormat.getDateInstance().format(friendDate) 35 | }. 36 | ${data.displayName}'s birthday is ${birthday.day} ${Shared.MONTHS[birthday.month - 1]} 37 | Friendship: ${data.friendLinkType.instance} 38 | Added by: ${data.addSourceTypeForNonFriend.instance}""" 39 | FooterInfoItem.wrap(viewModel).text = info 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/info/NetworkLogging.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.info 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | import de.robv.android.xposed.XposedBridge 5 | import xyz.rodit.snapmod.features.Feature 6 | import xyz.rodit.snapmod.features.FeatureContext 7 | import xyz.rodit.snapmod.mappings.NetworkApi 8 | import xyz.rodit.snapmod.util.before 9 | 10 | class NetworkLogging(context: FeatureContext) : Feature(context) { 11 | 12 | override fun performHooks() { 13 | // Hook network manager to log requests. 14 | val hook = { it: XC_MethodHook.MethodHookParam -> XposedBridge.log(it.args[0].toString()) } 15 | 16 | NetworkApi.submit.before(context, "log_network_requests", hook) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/notifications/FilterTypes.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.notifications 2 | 3 | import android.app.Notification 4 | import xyz.rodit.snapmod.features.Feature 5 | import xyz.rodit.snapmod.features.FeatureContext 6 | import xyz.rodit.snapmod.mappings.SnapNotificationBuilder 7 | import xyz.rodit.snapmod.util.after 8 | import xyz.rodit.snapmod.util.getList 9 | import xyz.rodit.snapmod.util.toMax 10 | 11 | class FilterTypes(context: FeatureContext) : Feature(context, 84608.toMax()) { 12 | 13 | private val hiddenTypes = hashSetOf() 14 | 15 | override fun onConfigLoaded(first: Boolean) { 16 | hiddenTypes.clear() 17 | hiddenTypes.addAll(context.config.getList("filtered_notification_types")) 18 | } 19 | 20 | override fun performHooks() { 21 | SnapNotificationBuilder.build.after { 22 | if (hiddenTypes.isEmpty()) return@after 23 | 24 | val notification = it.result as Notification 25 | val snapBundle = notification.extras.getBundle("system_notification_extras") ?: return@after 26 | val type = snapBundle.getString("notification_type") ?: return@after 27 | if (hiddenTypes.contains(type)) { 28 | it.result = null 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/notifications/ShowMessageContent.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.notifications 2 | 3 | import android.app.Notification 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.media.ThumbnailUtils 7 | import android.os.Build 8 | import android.provider.MediaStore 9 | import android.util.Size 10 | import androidx.core.app.NotificationCompat 11 | import xyz.rodit.snapmod.features.Feature 12 | import xyz.rodit.snapmod.features.FeatureContext 13 | import xyz.rodit.snapmod.logging.* 14 | import xyz.rodit.snapmod.mappings.* 15 | import xyz.rodit.snapmod.util.after 16 | import xyz.rodit.snapmod.util.toMax 17 | import java.io.File 18 | import java.io.InputStream 19 | import java.net.URL 20 | import java.util.concurrent.ConcurrentHashMap 21 | 22 | private val IMAGE_SIG = listOf( 23 | "ffd8ff", // jpeg 24 | "1a45dfa3", // webm 25 | "89504e47", // png 26 | ) 27 | 28 | private const val VIDEO_SNAP_DEBOUNCE = 2500L 29 | 30 | class ShowMessageContent(context: FeatureContext) : Feature(context, 84608.toMax()) { 31 | 32 | private var lastVideoSnapMessageTimes: MutableMap = ConcurrentHashMap() 33 | 34 | override fun performHooks() { 35 | SnapNotificationBuilder.build.after { 36 | val notification = it.result as Notification 37 | val extras = notification.extras 38 | val snapBundle = extras.getBundle("system_notification_extras") ?: return@after 39 | val type = snapBundle.getString("notification_type") ?: return@after 40 | val conversationId = snapBundle.getString("conversation_id") ?: return@after 41 | val messageId = snapBundle.getString("message_id") ?: return@after 42 | if (type == "CHAT") { 43 | val content = 44 | context.arroyo.getMessageContent(conversationId, messageId) ?: return@after 45 | extras.putString("android.text", content) 46 | extras.putString("android.bigText", content) 47 | } else if (type == "SNAP") { 48 | val time = System.currentTimeMillis() 49 | if (time - (lastVideoSnapMessageTimes[conversationId] ?: 0L) < VIDEO_SNAP_DEBOUNCE) { 50 | it.result = null 51 | return@after 52 | } 53 | val (key, iv, urlKey) = context.arroyo.getSnapData(conversationId, messageId) 54 | ?: return@after 55 | val mediaUrl = "https://cf-st.sc-cdn.net/h/$urlKey" 56 | val crypt = AesCrypto(key, iv) 57 | crypt.decrypt(URL(mediaUrl).openStream()).use { stream -> 58 | val (bitmap, isImage) = generatePreview(stream) 59 | if (bitmap == null) return@use 60 | if (!isImage) lastVideoSnapMessageTimes[conversationId] = time 61 | 62 | val title = 63 | extras.getString("android.text") + " (${if (isImage) "Image" else "Video"})" 64 | 65 | it.result = NotificationCompat.Builder(context.appContext, notification) 66 | .setContentText(title) 67 | .setLargeIcon(bitmap) 68 | .setStyle( 69 | NotificationCompat.BigPictureStyle() 70 | .bigPicture(bitmap) 71 | .bigLargeIcon(null) 72 | ) 73 | .build() 74 | } 75 | } 76 | } 77 | } 78 | 79 | private fun generatePreview(stream: InputStream): Pair { 80 | val temp = File.createTempFile("snapmod", "tmp", context.appContext.cacheDir) 81 | temp.outputStream().use(stream::copyTo) 82 | 83 | val sig = ByteArray(4) 84 | temp.inputStream().use { it.read(sig) } 85 | val sigString = sig.joinToString("") { "%02x".format(it) } 86 | 87 | val isImage = IMAGE_SIG.any { sigString.startsWith(it) } 88 | val preview = 89 | if (isImage) BitmapFactory.decodeFile(temp.path) else generateVideoPreview(temp) 90 | 91 | temp.delete() 92 | return preview to isImage 93 | } 94 | 95 | private fun generateVideoPreview(file: File): Bitmap? { 96 | try { 97 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 98 | ThumbnailUtils.createVideoThumbnail( 99 | file, 100 | Size(1024, 1024), 101 | null 102 | ) 103 | } else { 104 | ThumbnailUtils.createVideoThumbnail( 105 | file.path, 106 | MediaStore.Images.Thumbnails.MINI_KIND 107 | ) 108 | } 109 | } catch (e: Exception) { 110 | log.error("Error creating video thumbnail.", e) 111 | } 112 | 113 | return null 114 | } 115 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/opera/CustomStoryOptions.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.opera 2 | 3 | import android.view.ViewGroup 4 | import android.widget.Switch 5 | import android.widget.TextView 6 | import androidx.core.view.children 7 | import androidx.core.view.setPadding 8 | import xyz.rodit.snapmod.CustomResources 9 | import xyz.rodit.snapmod.CustomResources.string.menu_story_custom_options 10 | import xyz.rodit.snapmod.features.Feature 11 | import xyz.rodit.snapmod.features.FeatureContext 12 | import xyz.rodit.snapmod.features.saving.isChat 13 | import xyz.rodit.snapmod.features.saving.storyId 14 | import xyz.rodit.snapmod.mappings.* 15 | import xyz.rodit.snapmod.util.ConversationManager 16 | import xyz.rodit.snapmod.util.after 17 | import xyz.rodit.snapmod.util.dp 18 | import java.lang.ref.WeakReference 19 | 20 | class CustomStoryOptions(context: FeatureContext) : Feature(context), MenuPlugin { 21 | 22 | private var currentParams: WeakReference? = null 23 | 24 | override val isEnabled = true 25 | 26 | override fun createActions(params: ParamsMap): Collection { 27 | return if (params.storyId == null) emptySet() else setOf( 28 | OperaActionMenuOptionViewModel( 29 | 0, 30 | menu_story_custom_options, 31 | true, 32 | OperaContextAction.HIDE_AD() 33 | ) 34 | ) 35 | } 36 | 37 | override fun performHooks() { 38 | OperaPageViewController.onDisplayStateChanged.after { 39 | val viewController = OperaPageViewController.wrap(it.thisObject) 40 | if (viewController.state.instance != OperaDisplayState.FULLY_DISPLAYED().instance) return@after 41 | 42 | val params = ParamsMap.wrap(viewController.metadata.instance) 43 | if (params.isChat) return@after 44 | 45 | currentParams = WeakReference(params.instance) 46 | } 47 | 48 | context.views.onAdd("context_action_view") { parent, view -> 49 | if (view !is ViewGroup) return@onAdd 50 | if (view.children.filterIsInstance() 51 | .first().text != CustomResources.get(menu_story_custom_options) 52 | ) return@onAdd 53 | 54 | parent.removeView(view) 55 | parent.addView(createToggle("Auto-Download", context.autoDownloadStories)) 56 | parent.addView(createToggle("Pin", context.pinnedStories)) 57 | } 58 | } 59 | 60 | private fun createToggle(title: String, manager: ConversationManager): Switch { 61 | val storyId = ParamsMap.wrap(currentParams!!.get()).storyId 62 | return Switch(context.appContext).apply { 63 | text = title 64 | isChecked = manager.isEnabled(storyId) 65 | setPadding(16.dp) 66 | setOnCheckedChangeListener { _, toggled -> 67 | if (toggled) manager.enable(storyId) 68 | else manager.disable(storyId) 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/opera/MenuModifier.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.opera 2 | 3 | import xyz.rodit.snapmod.features.Contextual 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.OperaActionMenuOptionViewModel 6 | import xyz.rodit.snapmod.mappings.ParamsMap 7 | 8 | private const val KEY_NAME = "action_menu_options" 9 | 10 | class MenuModifier(context: FeatureContext) : Contextual(context), OperaPlugin { 11 | 12 | private val plugins: MutableList = ArrayList() 13 | 14 | override val isEnabled: Boolean 15 | get() { 16 | return true 17 | } 18 | 19 | override fun shouldOverride(params: ParamsMap, key: String): Boolean { 20 | return KEY_NAME == key 21 | } 22 | 23 | override fun override(params: ParamsMap, key: String, value: Any): Any { 24 | if (value !is List<*>) return value 25 | val newList: MutableList = ArrayList() 26 | newList.addAll(value.filterNotNull()) 27 | newList.addAll( 28 | plugins.filter(MenuPlugin::isEnabled) 29 | .flatMap { it.createActions(params) } 30 | .map(OperaActionMenuOptionViewModel::instance) 31 | ) 32 | return newList 33 | } 34 | 35 | init { 36 | plugins.add(SaveMenuOption(context)) 37 | plugins.add(context.features.get()) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/opera/MenuPlugin.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.opera 2 | 3 | import xyz.rodit.snapmod.mappings.OperaActionMenuOptionViewModel 4 | import xyz.rodit.snapmod.mappings.ParamsMap 5 | 6 | interface MenuPlugin { 7 | 8 | val isEnabled: Boolean 9 | fun createActions(params: ParamsMap): Collection 10 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/opera/OperaModelModifier.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.opera 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.MapKey 6 | import xyz.rodit.snapmod.mappings.ParamsMap 7 | import xyz.rodit.snapmod.util.before 8 | 9 | class OperaModelModifier(context: FeatureContext) : Feature(context) { 10 | 11 | private val plugins: MutableList = ArrayList() 12 | 13 | override fun init() { 14 | plugins.add(MenuModifier(context)) 15 | plugins.add(SnapDurationModifier(context)) 16 | } 17 | 18 | override fun performHooks() { 19 | // Modify opera model map on insert. 20 | ParamsMap.put.before { 21 | val params = ParamsMap.wrap(it.thisObject) 22 | val key = MapKey.wrap(it.args[0]).name 23 | plugins.filter { p -> p.isEnabled && p.shouldOverride(params, key) } 24 | .forEach { p -> it.args[1] = p.override(params, key, it.args[1]) } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/opera/OperaPlugin.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.opera 2 | 3 | import xyz.rodit.snapmod.mappings.ParamsMap 4 | 5 | interface OperaPlugin { 6 | 7 | val isEnabled: Boolean 8 | fun shouldOverride(params: ParamsMap, key: String): Boolean 9 | fun override(params: ParamsMap, key: String, value: Any): Any 10 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/opera/SaveMenuOption.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.opera 2 | 3 | import xyz.rodit.snapmod.features.Contextual 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.OperaActionMenuOptionViewModel 6 | import xyz.rodit.snapmod.mappings.OperaContextActions 7 | import xyz.rodit.snapmod.mappings.ParamsMap 8 | 9 | class SaveMenuOption(context: FeatureContext) : Contextual(context), MenuPlugin { 10 | 11 | override val isEnabled: Boolean 12 | get() = context.config.getBoolean("allow_download_stories") 13 | 14 | override fun createActions(params: ParamsMap): Collection { 15 | return setOf(OperaContextActions.getSaveAction()) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/opera/SnapDurationModifier.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.opera 2 | 3 | import xyz.rodit.snapmod.features.Contextual 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.ParamsMap 6 | import xyz.rodit.snapmod.mappings.StoryAutoAdvanceMode 7 | import xyz.rodit.snapmod.mappings.StoryMediaPlaybackMode 8 | 9 | private const val KEY_AUTO_ADVANCE_MODE = "auto_advance_mode" 10 | private const val KEY_MEDIA_PLAYBACK_MODE = "media_playback_mode" 11 | 12 | class SnapDurationModifier(context: FeatureContext) : Contextual(context), OperaPlugin { 13 | 14 | override val isEnabled: Boolean 15 | get() = context.config.getBoolean("unlimited_snap_duration") 16 | 17 | override fun shouldOverride(params: ParamsMap, key: String): Boolean { 18 | return KEY_AUTO_ADVANCE_MODE == key || KEY_MEDIA_PLAYBACK_MODE == key 19 | } 20 | 21 | override fun override(params: ParamsMap, key: String, value: Any): Any { 22 | if (KEY_AUTO_ADVANCE_MODE == key) { 23 | return StoryAutoAdvanceMode.NO_AUTO_ADVANCE().instance 24 | } else if (KEY_MEDIA_PLAYBACK_MODE == key) { 25 | return StoryMediaPlaybackMode.LOOPING().instance 26 | } 27 | 28 | return value 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/saving/AutoDownloadSnaps.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.saving 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.* 6 | import xyz.rodit.snapmod.util.PathManager 7 | import xyz.rodit.snapmod.util.after 8 | import xyz.rodit.snapmod.util.toMax 9 | import xyz.rodit.snapmod.util.toUUIDString 10 | 11 | class AutoDownloadSnaps(context: FeatureContext) : Feature(context, 84608.toMax()) { 12 | 13 | private val ignore = hashSetOf() 14 | 15 | override fun performHooks() { 16 | OperaPageViewController.onDisplayStateChanged.after { 17 | val viewController = OperaPageViewController.wrap(it.thisObject) 18 | if (viewController.state.instance != OperaDisplayState.FULLY_DISPLAYED().instance) return@after 19 | 20 | val params = ParamsMap.wrap(viewController.metadata.instance) 21 | val map = params.map 22 | if (!params.isChat) return@after 23 | 24 | val messageId = map[MessageStoryKeys.getMessageId().instance] as String? 25 | val conversationId = UUID.wrap(map[ConversationStoryKeys.getConversationId().instance]) 26 | if (messageId == null || conversationId.isNull) return@after 27 | 28 | if (!context.config.getBoolean("auto_download_snaps") 29 | && !context.autoDownload.isEnabled(conversationId.toUUIDString()) 30 | ) return@after 31 | 32 | if (ignore.contains(messageId)) return@after 33 | 34 | ignore.add(messageId) 35 | getMediaInfo(context, params) { info -> 36 | downloadOperaMedia( 37 | context, 38 | PathManager.DOWNLOAD_SNAP, 39 | info 40 | ) 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/saving/AutoDownloadStories.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.saving 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.OperaDisplayState 6 | import xyz.rodit.snapmod.mappings.OperaPageViewController 7 | import xyz.rodit.snapmod.mappings.ParamsMap 8 | import xyz.rodit.snapmod.util.PathManager 9 | import xyz.rodit.snapmod.util.after 10 | import xyz.rodit.snapmod.util.toMax 11 | 12 | class AutoDownloadStories(context: FeatureContext) : Feature(context, 84608.toMax()) { 13 | 14 | override fun performHooks() { 15 | OperaPageViewController.onDisplayStateChanged.after { 16 | val viewController = OperaPageViewController.wrap(it.thisObject) 17 | if (viewController.state.instance != OperaDisplayState.FULLY_DISPLAYED().instance) return@after 18 | 19 | val params = ParamsMap.wrap(viewController.metadata.instance) 20 | if (params.isChat) return@after 21 | 22 | val storyId = params.storyId 23 | if (!context.config.getBoolean("auto_download_stories") 24 | && !context.autoDownloadStories.isEnabled(storyId) 25 | ) return@after 26 | 27 | getMediaInfo(context, params) { info -> 28 | downloadOperaMedia( 29 | context, 30 | PathManager.DOWNLOAD_STORY, 31 | info 32 | ) 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/saving/ChatSaving.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.saving 2 | 3 | import android.net.Uri 4 | import xyz.rodit.snapmod.ResolutionListener 5 | import xyz.rodit.snapmod.Shared 6 | import xyz.rodit.snapmod.UriResolverSubscriber 7 | import xyz.rodit.snapmod.features.Feature 8 | import xyz.rodit.snapmod.features.FeatureContext 9 | import xyz.rodit.snapmod.logging.log 10 | import xyz.rodit.snapmod.mappings.* 11 | import xyz.rodit.snapmod.util.PathManager 12 | import xyz.rodit.snapmod.util.after 13 | import xyz.rodit.snapmod.util.before 14 | import xyz.rodit.snapmod.util.download 15 | import xyz.rodit.xposed.client.http.StreamProvider 16 | import xyz.rodit.xposed.client.http.streams.CachedStreamProvider 17 | import xyz.rodit.xposed.client.http.streams.FileProxyStreamProvider 18 | import java.io.File 19 | import java.io.FileInputStream 20 | import java.io.IOException 21 | import java.lang.reflect.Proxy 22 | 23 | class ChatSaving(context: FeatureContext) : Feature(context) { 24 | 25 | private val chatMediaMap: MutableMap = HashMap() 26 | 27 | private var chatMediaHandler: ChatMediaHandler? = null 28 | private var lastMessageData: MessageDataModel? = null 29 | 30 | override fun performHooks() { 31 | // Obtain chat media handler instance 32 | ChatMediaHandler.constructors.before { 33 | chatMediaHandler = ChatMediaHandler.wrap(it.thisObject) 34 | } 35 | 36 | // Allow save action in chat long-press menu. 37 | ChatActionHelper.canSave.before(context, "allow_save_snaps") { 38 | it.result = true 39 | } 40 | 41 | // Allow save action in chat model. 42 | ChatModelBase.canSave.before(context, "allow_save_snaps") { 43 | it.result = true 44 | } 45 | 46 | // Override save type to gallery to allow saving any snaps. 47 | ChatModelBase.getSaveType.before(context, "allow_save_snaps") { 48 | if (ChatModelLiveSnap.isInstance(it.thisObject) 49 | || ChatModelPlugin.isInstance(it.thisObject) 50 | ) { 51 | it.result = SaveType.SNAPCHAT_ALBUM().instance 52 | } 53 | } 54 | 55 | // Map live snap model hashCode to media object for download later. 56 | ChatModelWithMedia.constructors.after(context, "allow_save_snaps") { 57 | val hashCode = it.thisObject.hashCode() 58 | chatMediaMap[hashCode] = it.args[6] 59 | } 60 | 61 | // Export non-savable media (live snaps and voice notes). 62 | ActionMenuPresenter.handleAction.before { 63 | if (it.args[3] != ChatMenuItemType.SAVE_TO_CAMERA_ROLL().instance) return@before 64 | 65 | val base = ChatModelBase.wrap(it.args[2]) 66 | lastMessageData = base.messageData 67 | 68 | when { 69 | ChatModelWithMedia.isInstance(it.args[2]) -> { 70 | // Convert live snap to saved snap. 71 | val hashCode = it.args[2].hashCode() 72 | val media = LiveSnapMedia.wrap(chatMediaMap[hashCode]) 73 | it.args[2] = ChatModelSavedSnap( 74 | base.context, 75 | base.messageData, 76 | base.senderId, 77 | emptyMap(), 78 | true, 79 | null, 80 | true, 81 | 0, 82 | 0, 83 | media, 84 | null, 85 | base.status, 86 | true 87 | ).instance 88 | } 89 | ChatModelPlugin.isInstance(it.args[2]) -> { 90 | val messageData = base.messageData 91 | 92 | if (messageData.type != "audio_note") return@before 93 | val media = GallerySnapMedia.wrap(messageData.media.instance).media 94 | val uri = createMediaUri(messageData.arroyoMessageId, media.id) 95 | 96 | resolveAndDownload(uri, messageData) 97 | it.result = null 98 | } 99 | } 100 | } 101 | 102 | // Override snap save location by custom-downloading media instead. 103 | MediaExportControllerImpl.exportMedia.before(context, "enable_custom_snap_download_path") { 104 | val messageData = lastMessageData 105 | if (messageData?.isNotNull != true) return@before 106 | val destination = DestinationInfo.wrap(it.args[2]) 107 | 108 | val provider = FileProxyStreamProvider(context.appContext) { 109 | FileInputStream( 110 | destination.actualFileName?.let(::File) 111 | ) 112 | } 113 | 114 | context.download( 115 | PathManager.DOWNLOAD_SNAP, 116 | createParams(messageData), 117 | '.' + destination.actualFileName.orEmpty().split('.').last(), 118 | provider, 119 | "${lastMessageData!!.senderDisplayName}'s snap" 120 | ) 121 | 122 | it.result = null 123 | } 124 | } 125 | 126 | private fun createParams(messageData: MessageDataModel): Map { 127 | return mapOf( 128 | "d" to messageData.senderDisplayName, 129 | "id" to messageData.senderId, 130 | "u" to messageData.senderUsername 131 | ) 132 | } 133 | 134 | private fun resolveAndDownload(uri: Uri, messageData: MessageDataModel) { 135 | // Resolve audio uri and resolve through proxy of RxObserver. 136 | // Note: the content resolver provided by appContext cannot open a stream from the uri. 137 | val observerProxy = Proxy.newProxyInstance( 138 | context.classLoader, 139 | arrayOf(RxObserver.getMappedClass()), 140 | MediaUriDownloader( 141 | context, 142 | PathManager.DOWNLOAD_AUDIO_NOTE, 143 | createParams(messageData), 144 | ".aac" 145 | ) 146 | ) 147 | 148 | chatMediaHandler!!.resolve( 149 | uri, 150 | emptySet(), 151 | true, 152 | emptySet() 153 | ).subscribe(RxObserver.wrap(observerProxy)) 154 | } 155 | 156 | private fun createMediaUri(messageId: String, mediaId: String): Uri { 157 | return Uri.Builder().scheme("content").authority("${Shared.SNAPCHAT_PACKAGE}.provider") 158 | .appendPath("chat_media") 159 | .appendPath(messageId) 160 | .appendPath(mediaId) 161 | .appendQueryParameter("target", "DEFAULT") 162 | .build() 163 | } 164 | 165 | private class MediaUriDownloader( 166 | context: FeatureContext, type: String, paramsMap: Map, extension: String 167 | ) : 168 | UriResolverSubscriber(UriListener(context, type, paramsMap, extension)) { 169 | 170 | private class UriListener( 171 | private val context: FeatureContext, private val type: String, 172 | private val paramsMap: Map, private val extension: String 173 | ) : 174 | ResolutionListener { 175 | 176 | override fun invoke(result: Any?) { 177 | log.debug("Accepted media stream provider: $result") 178 | val streamProvider = MediaStreamProvider.wrap(result) 179 | val provider: StreamProvider = 180 | CachedStreamProvider(FileProxyStreamProvider(context.appContext) { streamProvider.mediaStream }) 181 | try { 182 | provider.provide() 183 | } catch (e: IOException) { 184 | log.error("Error pre-providing cached stream.", e) 185 | } 186 | 187 | context.download(type, paramsMap, extension, provider, "Media") 188 | } 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/saving/PublicProfileSaving.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.saving 2 | 3 | import android.app.AlertDialog 4 | import android.view.View 5 | import xyz.rodit.snapmod.features.Feature 6 | import xyz.rodit.snapmod.features.FeatureContext 7 | import xyz.rodit.snapmod.mappings.PublicProfileTile 8 | import xyz.rodit.snapmod.mappings.PublicProfileTileTransformer 9 | import xyz.rodit.snapmod.util.PathManager 10 | import xyz.rodit.snapmod.util.after 11 | 12 | private const val PROFILE_PICTURE_RESOLUTION_PATTERN = "0,\\d+_" 13 | 14 | class PublicProfileSaving(context: FeatureContext) : Feature(context) { 15 | 16 | override fun performHooks() { 17 | // Hook friend public profile tile to allow download profile picture. 18 | PublicProfileTileTransformer.transform.after { 19 | val view = 20 | PublicProfileTileTransformer.wrap(it.thisObject).profileImageView as View 21 | val tile = PublicProfileTile.wrap(it.args[0]) 22 | view.setOnLongClickListener { 23 | if (!context.config.getBoolean( 24 | "allow_download_public_dp" 25 | ) 26 | ) return@setOnLongClickListener false 27 | 28 | val url = tile.profilePictureUrl 29 | context.activity!!.runOnUiThread { 30 | AlertDialog.Builder(context.activity) 31 | .setTitle("Download Profile Picture?") 32 | .setPositiveButton("Yes") { _, _ -> 33 | var resolution = context.config.getString("public_dp_resolution", "500") 34 | 35 | var resDouble = resolution.toDouble() 36 | if (resDouble < 1 || resDouble > 5000) resDouble = 500.0 37 | 38 | resolution = resDouble.toInt().toString() 39 | val resizedUrl = url.replace( 40 | PROFILE_PICTURE_RESOLUTION_PATTERN.toRegex(), 41 | "0," + resolution + "_" 42 | ) 43 | val username = tile.info.metadata.username 44 | val dest = PathManager.getUri( 45 | context.config, 46 | PathManager.DOWNLOAD_PROFILE, 47 | mapOf("u" to username), 48 | ".jpg" 49 | ) 50 | context.files.download( 51 | context.config.getBoolean("use_android_download_manager"), 52 | resizedUrl, 53 | dest, 54 | "$username's profile picture", 55 | null 56 | ) 57 | } 58 | .setNegativeButton("No") { _, _ -> } 59 | .show() 60 | } 61 | 62 | return@setOnLongClickListener true 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/saving/StoriesSaving.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.saving 2 | 3 | import xyz.rodit.snapmod.createDelegate 4 | import xyz.rodit.snapmod.features.Feature 5 | import xyz.rodit.snapmod.features.FeatureContext 6 | import xyz.rodit.snapmod.mappings.ContextActionMenuModel 7 | import xyz.rodit.snapmod.mappings.Func1 8 | import xyz.rodit.snapmod.mappings.OperaContextAction 9 | import xyz.rodit.snapmod.mappings.ParamsMap 10 | import xyz.rodit.snapmod.util.after 11 | 12 | class StoriesSaving(context: FeatureContext) : Feature(context) { 13 | 14 | override fun performHooks() { 15 | // Override save story click. 16 | ContextActionMenuModel.constructors.after(context, "allow_download_stories") { 17 | val model = ContextActionMenuModel.wrap(it.thisObject) 18 | if (model.action.instance == OperaContextAction.SAVE().instance) { 19 | val clickProxy = 20 | Func1.getMappedClass().createDelegate(context.classLoader) { _, args -> 21 | val params = ParamsMap.wrap(args[0]) 22 | getMediaInfo(context, params) { info -> 23 | downloadOperaMedia(context, null, info) 24 | } 25 | null 26 | } 27 | model.onClick = Func1.wrap(clickProxy) 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/saving/StoryHelper.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.saving 2 | 3 | import xyz.rodit.snapmod.createDummyProxy 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.logging.XLog 6 | import xyz.rodit.snapmod.mappings.* 7 | import xyz.rodit.snapmod.util.PathManager 8 | import xyz.rodit.snapmod.util.download 9 | import xyz.rodit.snapmod.util.toSnapUUID 10 | import xyz.rodit.snapmod.util.toUUIDString 11 | import xyz.rodit.xposed.client.http.StreamProvider 12 | import xyz.rodit.xposed.client.http.streams.FileProxyStreamProvider 13 | import java.net.URL 14 | 15 | private val mLog = XLog("StoryHelper") 16 | 17 | typealias UsernameFetcher = (Map<*, *>) -> String? 18 | 19 | private val usernameFetchers = listOf( 20 | { 21 | val session = ContextSession.wrap(it[ContextStoryKeys.getContextSession().instance]) 22 | if (session.isNull) null else { 23 | val snapUsername = session.info.username 24 | if (snapUsername.isNotNull) snapUsername.displayString else null 25 | } 26 | }, 27 | { 28 | val storySnap = 29 | PlayableStorySnap.wrap(it[FriendStoryKeys.getPlayableSnapStoryRecord().instance]) 30 | if (storySnap.isNotNull) storySnap.displayName else null 31 | } 32 | ) 33 | 34 | fun downloadOperaMedia(context: FeatureContext, type: String?, media: StoryMedia?) { 35 | if (media == null || media.info.isNull) return 36 | 37 | val provider: StreamProvider = FileProxyStreamProvider(context.appContext) { 38 | try { 39 | var stream = URL(media.info.uri).openStream() 40 | val enc = media.info.encryption 41 | if (enc.isNotNull) { 42 | stream = enc.decryptStream(stream) 43 | } 44 | 45 | return@FileProxyStreamProvider stream 46 | } catch (e: Exception) { 47 | mLog.error("Error opening story media stream.", e) 48 | } 49 | return@FileProxyStreamProvider null 50 | } 51 | 52 | val finalType = type 53 | ?: if (media.map.containsKey(MessageStoryKeys.getMessageId().instance)) PathManager.DOWNLOAD_SNAP else PathManager.DOWNLOAD_STORY 54 | val typeString = if (finalType == PathManager.DOWNLOAD_SNAP) "Snap" else "Story" 55 | 56 | context.download( 57 | finalType, 58 | mapOf("u" to media.username), 59 | media.extension, 60 | provider, 61 | "${media.username}'s $typeString" 62 | ) 63 | } 64 | 65 | fun getMediaInfo( 66 | context: FeatureContext, metadata: ParamsMap, callback: (StoryMedia?) -> Unit 67 | ) { 68 | val map: Map<*, *> = metadata.map 69 | 70 | getUsername(context, metadata) { username -> 71 | map[StoryMetadata.getImageMediaInfo().instance]?.let { 72 | mLog.debug("Found image media info for story.") 73 | return@getUsername callback(StoryMedia(OperaMediaInfo.wrap(it), username, ".jpg", map)) 74 | } 75 | 76 | map[StoryMetadata.getVideoMediaInfo().instance]?.let { 77 | mLog.debug("Found video media info for story.") 78 | return@getUsername callback(StoryMedia(OperaMediaInfo.wrap(it), username, ".mp4", map)) 79 | } 80 | 81 | map[StoryMetadata.getOverlayImageMediaInfo().instance]?.let { 82 | mLog.debug("Found image overlay media info for story.") 83 | return@getUsername callback(StoryMedia(OperaMediaInfo.wrap(it), username, ".jpg", map)) 84 | } 85 | 86 | mLog.error("Error getting media info for $metadata.") 87 | callback(null) 88 | } 89 | } 90 | 91 | private fun getUsername( 92 | context: FeatureContext, metadata: ParamsMap, callback: (String) -> Unit 93 | ) { 94 | val map: Map<*, *> = metadata.map 95 | 96 | val username = usernameFetchers.firstNotNullOfOrNull { it(map) } 97 | if (username != null) { 98 | mLog.debug("Found username using fetchers.") 99 | return callback(username) 100 | } 101 | 102 | val messageId = map[MessageStoryKeys.getMessageId().instance] 103 | mLog.debug("Trying to get username from message id: $messageId") 104 | if (messageId !is String) return callback("unknown") 105 | 106 | val idParts = messageId.split(':') 107 | if (idParts.size != 3) return callback("unknown") 108 | 109 | val uuid = idParts[0] 110 | val arroyoId = idParts[2].toLong() 111 | val proxy = 112 | MessageDummyInterface.wrap( 113 | MessageDummyInterface.getMappedClass().createDummyProxy(context.classLoader) 114 | ) 115 | val messageCallback = DefaultFetchMessageCallback(proxy, null, 0, 0) 116 | context.callbacks.on( 117 | DefaultFetchMessageCallback::class, 118 | DefaultFetchMessageCallback.onFetchMessageComplete 119 | ) { 120 | val message = Message.wrap(it.args[0]) 121 | val senderId = message.senderId.toUUIDString() 122 | val friends = 123 | context.instances.friendsRepository.selectFriendsByUserIds(listOf(senderId)) 124 | val friendUsername = 125 | if (friends.isNotEmpty()) SelectFriendsByUserIds.wrap(friends[0]).username.displayString else "unknown" 126 | mLog.debug("Fetched message with username $friendUsername.") 127 | callback(friendUsername) 128 | true 129 | } 130 | mLog.debug("Fetching message with conversation manager.") 131 | context.instances.conversationManager.fetchMessage( 132 | uuid.toSnapUUID(), 133 | arroyoId, 134 | messageCallback 135 | ) 136 | } 137 | 138 | val ParamsMap.isChat: Boolean 139 | get() = map.containsKey(MessageStoryKeys.getSnapInSavedState().instance) 140 | 141 | val ParamsMap.storyId: String? 142 | get() { 143 | return try { 144 | val reportingInfo = map[FriendStoryKeys.getStorySnapViewReportingInfo().instance] 145 | if (reportingInfo is Collection<*>) StorySnapViewReportingInfo.wrap(reportingInfo.first()).storySnapKey.storyKey.storyId else null 146 | } catch (ex: NullPointerException) { 147 | null 148 | } 149 | } 150 | 151 | class StoryMedia( 152 | val info: OperaMediaInfo, val username: String, val extension: String, val map: Map<*, *> 153 | ) -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/shared/Filter.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.shared 2 | 3 | interface Filter { 4 | 5 | val isEnabled: Boolean 6 | fun shouldFilter(item: Any?): Boolean 7 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/BypassVideoLength.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.MemoriesPickerVideoDurationConfig 6 | import xyz.rodit.snapmod.util.before 7 | 8 | class BypassVideoLength(context: FeatureContext) : Feature(context) { 9 | 10 | override fun performHooks() { 11 | // Bypass video duration limit from gallery in chat. 12 | MemoriesPickerVideoDurationConfig.constructors.before( 13 | context, "bypass_video_length_restrictions" 14 | ) { it.args[0] = Long.MAX_VALUE } 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/BypassVideoLengthGlobal.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.CameraRollVideoLengthChecker 6 | import xyz.rodit.snapmod.mappings.MediaPackage 7 | import xyz.rodit.snapmod.mappings.VideoLengthChecker 8 | import xyz.rodit.snapmod.util.after 9 | import xyz.rodit.snapmod.util.before 10 | import xyz.rodit.snapmod.util.toMax 11 | 12 | class BypassVideoLengthGlobal(context: FeatureContext) : Feature(context, 84606.toMax()) { 13 | 14 | private var lastVideoDuration: Long = 0L 15 | 16 | override fun performHooks() { 17 | VideoLengthChecker.apply.before(context, "bypass_video_length_restrictions") { 18 | if (!MediaPackage.isInstance(it.args[0])) return@before 19 | 20 | val media = MediaPackage.wrap(it.args[0]).media 21 | lastVideoDuration = media.videoDurationMs 22 | media.videoDurationMs = 0L 23 | } 24 | 25 | VideoLengthChecker.apply.after(context, "bypass_video_length_restrictions") { 26 | if (!MediaPackage.isInstance(it.args[0])) return@after 27 | 28 | MediaPackage.wrap(it.args[0]).media.videoDurationMs = lastVideoDuration 29 | } 30 | 31 | CameraRollVideoLengthChecker.isOver60Seconds.before( 32 | context, 33 | "bypass_video_length_restrictions" 34 | ) { 35 | it.result = false 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/CameraResolution.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.MediaQualityLevel 6 | import xyz.rodit.snapmod.mappings.RecordingCodecConfiguration 7 | import xyz.rodit.snapmod.mappings.ScCameraSettings 8 | import xyz.rodit.snapmod.mappings.TranscodingRequest 9 | import xyz.rodit.snapmod.util.after 10 | import xyz.rodit.snapmod.util.getNonDefault 11 | import xyz.rodit.snapmod.util.getResolution 12 | 13 | class CameraResolution(context: FeatureContext) : Feature(context) { 14 | 15 | override fun performHooks() { 16 | // Override preview and picture resolution 17 | ScCameraSettings.constructors.after { 18 | val settings = ScCameraSettings.wrap(it.thisObject) 19 | context.config.getResolution("custom_video_resolution")?.let { r -> 20 | val previewResolution = settings.previewResolution 21 | if (previewResolution.isNotNull) { 22 | previewResolution.width = r.width 23 | previewResolution.height = r.height 24 | } 25 | } 26 | 27 | context.config.getResolution("custom_image_resolution")?.let { r -> 28 | val pictureResolution = settings.pictureResolution 29 | if (pictureResolution.isNotNull) { 30 | pictureResolution.width = r.width 31 | pictureResolution.height = r.height 32 | } 33 | } 34 | } 35 | 36 | // Override actual recording resolution 37 | RecordingCodecConfiguration.constructors.after { 38 | val config = RecordingCodecConfiguration.wrap(it.thisObject) 39 | context.config.getResolution("custom_video_resolution")?.let { r -> 40 | val res = config.resolution 41 | res.width = r.width 42 | res.height = r.height 43 | } 44 | 45 | context.config.getNonDefault("custom_video_bitrate")?.let { bitrate -> 46 | config.bitrate = bitrate 47 | } 48 | } 49 | 50 | // Override save/send quality level 51 | TranscodingRequest.constructors.after(context, "force_source_encoding") { 52 | TranscodingRequest.wrap(it.thisObject).qualityLevel = MediaQualityLevel.LEVEL_MAX() 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/ConfigurationTweaks.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.CompositeConfigurationProvider 6 | import xyz.rodit.snapmod.mappings.ConfigKeyBase 7 | import xyz.rodit.snapmod.util.after 8 | import xyz.rodit.xposed.client.ConfigurationClient 9 | import xyz.rodit.xposed.utils.Predicate 10 | 11 | private const val DUMMY_URL = "https://127.0.0.1:1" 12 | 13 | class ConfigurationTweaks(context: FeatureContext) : Feature(context) { 14 | 15 | private val tweaks: MutableMap = HashMap() 16 | 17 | private fun override(name: String, optionName: String, overrideValue: Any) { 18 | override(name, { it.getBoolean(optionName) }, overrideValue) 19 | } 20 | 21 | private fun override( 22 | name: String, 23 | optionRequirement: Predicate, 24 | overrideValue: Any 25 | ) { 26 | tweaks[name] = Tweak(optionRequirement, overrideValue) 27 | } 28 | 29 | override fun init() { 30 | override("ENABLE_DISCOVER_AD", "block_ads", false) 31 | override("ENABLE_USER_STORY_AD", "block_ads", false) 32 | override("ENABLE_ADS_IN_SHOWS", "block_ads", false) 33 | override("ENABLE_CONTENT_INTERSTITIAL_ADS", "block_ads", false) 34 | override("ENABLE_COGNAC_AD", "block_ads", false) 35 | 36 | override("SPOTLIGHT_5TH_TAB_ENABLED", "disable_spotlight", false) 37 | 38 | override("PREVENT_STORIES_FROM_BEING_MARKED_AS_VIEWED", "hide_story_views_local", true) 39 | 40 | override("GRAPHENE_HOST", "disable_metrics", DUMMY_URL) 41 | override("MAX_RETRY_QUEUE_SIZE", "disable_metrics", 0) 42 | override("LOG_EVENTS", "disable_metrics", false) 43 | override("FLUSH_EVENTS_TO_DISK_ON_PAUSE", "disable_metrics", false) 44 | override("V2_BLIZZARD_DISK_QUOTA", "disable_metrics", 0) 45 | override("ARE_BENCHMARKS_ENABLED", "disable_metrics", false) 46 | override("CUSTOM_SPECTRUM_COLLECTOR_URL", "disable_metrics", DUMMY_URL) 47 | override("CUSTOM_COLLECTOR_URL", "disable_metrics", DUMMY_URL) 48 | 49 | override( 50 | "DF_ADAPTER_REFACTOR_ENABLED", 51 | { it.getString("disable_story_sections", "[]").length > 2 }, 52 | true 53 | ) 54 | 55 | override("ENABLE_DF_FRIENDS_SECTION_LIST_VIEW", "enable_story_list", true) 56 | 57 | override("ENABLE_VOICE_NOTE_REVAMP", "enable_new_voice_notes", true) 58 | override("ENABLE_VOICE_NOTES_MESSAGING_PLUGIN", "enable_new_voice_notes", true) 59 | 60 | override("ENABLE_LONG_SNAPS", "disable_snap_splitting", true) 61 | } 62 | 63 | override fun performHooks() { 64 | // Apply tweak overrides based on configuration. 65 | CompositeConfigurationProvider.get.after { 66 | val key = ConfigKeyBase.wrap(it.args[0]) 67 | val tweak = tweaks[key.name] 68 | if (tweak != null && tweak.optionRequirement.test(context.config)) { 69 | it.result = tweak.overrideValue 70 | } 71 | } 72 | } 73 | 74 | private class Tweak( 75 | val optionRequirement: Predicate, 76 | val overrideValue: Any 77 | ) 78 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/DisableBitmojis.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.BitmojiUriHandler 6 | import xyz.rodit.snapmod.util.before 7 | 8 | class DisableBitmojis(context: FeatureContext) : Feature(context) { 9 | 10 | override fun performHooks() { 11 | // Disable Bitmoji avatars. 12 | BitmojiUriHandler.handle.before(context, "disable_bitmojis") { it.result = null } 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/HideFriends.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | /* 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.ComposerFriend 6 | import xyz.rodit.snapmod.mappings.DisplayInfoContainer 7 | import xyz.rodit.snapmod.mappings.FriendListener 8 | import xyz.rodit.snapmod.mappings.ProfileMyFriendsSection 9 | import xyz.rodit.snapmod.util.before 10 | 11 | class HideFriends(context: FeatureContext) : Feature(context) { 12 | 13 | private val hiddenFriends: MutableSet = HashSet() 14 | 15 | override fun onConfigLoaded(first: Boolean) { 16 | hiddenFriends.clear() 17 | for (username in context.config.getString("hidden_friends", "").split("\n") 18 | .filter { it.isNotBlank() }) { 19 | hiddenFriends.add(username.trim()) 20 | } 21 | } 22 | 23 | override fun performHooks() { 24 | // Hide friends from 'My Friends' in profile. 25 | ProfileMyFriendsSection.filter.before(context, "hide_friends") { 26 | val list = it.args[0] as List<*> 27 | it.args[0] = list.filter { friend -> 28 | !hiddenFriends.contains( 29 | DisplayInfoContainer.wrap(friend).term 30 | ) 31 | } 32 | } 33 | 34 | // Hide friends from best friends list. 35 | FriendListener.handle.before(context, "hide_friends") { 36 | if (it.args[0] is List<*>) { 37 | val list = it.args[0] as List<*> 38 | if (list.isEmpty() || !ComposerFriend.isInstance(list[0])) { 39 | return@before 40 | } 41 | 42 | it.args[0] = 43 | list.filter { friend -> 44 | !hiddenFriends.contains( 45 | ComposerFriend.wrap(friend).user.username 46 | ) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | */ -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/HideStoryReadReceipts.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.UploadSnapReadReceiptDurableJobProcessor 6 | import xyz.rodit.snapmod.util.before 7 | 8 | class HideStoryReadReceipts(context: FeatureContext) : Feature(context) { 9 | 10 | override fun performHooks() { 11 | // Prevent story read receipt uploads. 12 | UploadSnapReadReceiptDurableJobProcessor.uploadReadReceipts.before( 13 | context, "hide_story_views" 14 | ) { it.result = null } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/HideStorySections.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.DiscoverFeedObservableSection 6 | import xyz.rodit.snapmod.mappings.DiscoverViewBinder 7 | import xyz.rodit.snapmod.util.before 8 | import xyz.rodit.snapmod.util.getList 9 | 10 | class HideStorySections(context: FeatureContext) : Feature(context, 0L..84609L) { 11 | 12 | private val hiddenStorySections: MutableSet = HashSet() 13 | 14 | override fun onConfigLoaded(first: Boolean) { 15 | hiddenStorySections.clear() 16 | hiddenStorySections.addAll(context.config.getList("disable_story_sections")) 17 | } 18 | 19 | override fun performHooks() { 20 | // Hide story sections. 21 | DiscoverViewBinder.setSections.before { 22 | if (hiddenStorySections.size > 0) { 23 | val sections = it.args[0] as List<*> 24 | it.args[0] = sections.filter { section -> 25 | !hiddenStorySections.contains( 26 | DiscoverFeedObservableSection.wrap(section).model.name 27 | ) 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/HideStorySectionsLegacy.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | 3 | import xyz.rodit.snapmod.features.Feature 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.snapmod.mappings.AdapterBase 6 | import xyz.rodit.snapmod.mappings.DfSectionController 7 | import xyz.rodit.snapmod.util.before 8 | import xyz.rodit.snapmod.util.getList 9 | import xyz.rodit.snapmod.util.toMax 10 | 11 | private val legacyMap = mapOf( 12 | "friends" to "FRIENDS_SECTION", 13 | "subs" to "SUBSCRIBED_SECTION", 14 | "for_you" to "FOR_YOU_SECTION" 15 | ) 16 | 17 | class HideStorySectionsLegacy(context: FeatureContext) : Feature(context, 84612L.toMax()) { 18 | 19 | private val hiddenStorySections: MutableSet = HashSet() 20 | 21 | override fun onConfigLoaded(first: Boolean) { 22 | hiddenStorySections.clear() 23 | hiddenStorySections.addAll( 24 | context.config.getList("disable_story_sections").mapNotNull { legacyMap[it] }) 25 | } 26 | 27 | override fun performHooks() { 28 | AdapterBase.addSection.before { 29 | if (!DfSectionController.isInstance(it.args[0])) return@before 30 | 31 | val sectionType = DfSectionController.wrap(it.args[0]).sectionType.instance.toString() 32 | if (hiddenStorySections.contains(sectionType)) { 33 | it.result = null 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/features/tweaks/PinStories.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.features.tweaks 2 | 3 | import xyz.rodit.snapmod.ObjectProxy 4 | import xyz.rodit.snapmod.features.Feature 5 | import xyz.rodit.snapmod.features.FeatureContext 6 | import xyz.rodit.snapmod.mappings.* 7 | import xyz.rodit.snapmod.util.before 8 | import xyz.rodit.snapmod.util.pairSecond 9 | import java.lang.reflect.Proxy 10 | 11 | class PinStories(context: FeatureContext) : Feature(context) { 12 | 13 | override fun performHooks() { 14 | DfSectionControllerActions.apply.before { 15 | if (!DfSectionController.isInstance(DfSectionControllerActions.wrap(it.thisObject).sectionController)) return@before 16 | 17 | val pairSecond = it.args[0].pairSecond 18 | if (!DataWithPagination.isInstance(pairSecond)) return@before 19 | 20 | val data = DataWithPagination.wrap(pairSecond) 21 | val dataModels = (data.dataModels.instance as Iterable<*>).toList() 22 | if (dataModels.isEmpty()) return@before 23 | 24 | val reordered = 25 | dataModels.sortedByDescending { model -> 26 | if (!StoryViewModel.isInstance(model)) return@sortedByDescending 0 27 | 28 | val storyData = StoryViewModel.wrap(model).storyData.instance 29 | if (!FriendStoryData.isInstance(storyData)) return@sortedByDescending 0 30 | 31 | val friendStoryData = FriendStoryData.wrap(storyData) 32 | if (context.pinnedStories.isEnabled(friendStoryData.storyRecordStoryId)) { 33 | val displayName = friendStoryData.displayName 34 | if (!displayName.startsWith("\uD83D\uDCCC")) { 35 | friendStoryData.displayName = "\uD83D\uDCCC $displayName" 36 | } 37 | 1 38 | } else 0 39 | } 40 | val iterableProxy = Proxy.newProxyInstance( 41 | context.classLoader, 42 | arrayOf(SnapIterable.getMappedClass()), 43 | ObjectProxy(reordered) 44 | ) 45 | 46 | data.dataModels = SnapIterable.wrap(iterableProxy) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/logging/XLog.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.logging 2 | 3 | import de.robv.android.xposed.XposedBridge 4 | 5 | const val LOG_NONE = 0 6 | const val LOG_ERROR = 1 7 | const val LOG_WARN = 2 8 | const val LOG_DEBUG = 4 9 | 10 | private val levelNames = mapOf( 11 | LOG_ERROR to "error", 12 | LOG_WARN to "warn", 13 | LOG_DEBUG to "debug" 14 | ) 15 | 16 | class XLog(private val tag: String) { 17 | 18 | var level: Int = LOG_ERROR or LOG_WARN or LOG_DEBUG 19 | 20 | fun put(level: Int, message: String) { 21 | if ((globalLevel or this.level) and level == 0 || !levelNames.containsKey(level)) return 22 | 23 | XposedBridge.log("$tag/${levelNames[level]}: $message") 24 | } 25 | 26 | fun error(message: String, error: Throwable? = null) { 27 | put(LOG_ERROR, message) 28 | error?.let { put(LOG_ERROR, error.toString()) } 29 | } 30 | 31 | fun warn(message: String) { 32 | put(LOG_WARN, message) 33 | } 34 | 35 | fun debug(message: String) { 36 | put(LOG_DEBUG, message) 37 | } 38 | 39 | companion object { 40 | var globalLevel = LOG_NONE 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/logging/XLogUtils.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.logging 2 | 3 | import android.util.Log 4 | import de.robv.android.xposed.XC_MethodHook 5 | import de.robv.android.xposed.XposedBridge 6 | import de.robv.android.xposed.XposedHelpers 7 | import java.util.* 8 | 9 | private val logMap = WeakHashMap() 10 | 11 | val Any.log: XLog 12 | get() { 13 | return logMap.computeIfAbsent(this) { XLog(this.javaClass.simpleName) }!! 14 | } 15 | 16 | fun XLog.dumpMap(map: Map<*, *>) { 17 | this.debug("${map.javaClass.simpleName}:\n" + map.entries.joinToString("\n") { "${it.key}: ${it.value}" }) 18 | } 19 | 20 | fun XLog.dump(o: Any?) { 21 | if (o == null) { 22 | this.debug("Null object dumped.") 23 | return 24 | } 25 | 26 | this.debug("Object dump type=${o.javaClass}\n" + 27 | "${o}\n" + 28 | o.javaClass.fields.joinToString("\n") { 29 | try { 30 | "${it.name}: ${it.get(o)}" 31 | } catch (e: Exception) { 32 | "Error getting field value for ${it.name}: ${e.message}" 33 | } 34 | } 35 | ) 36 | } 37 | 38 | fun XLog.dumpStackTrace() { 39 | this.debug(Log.getStackTraceString(Exception())) 40 | } 41 | 42 | fun XLog.dumpMethodCall(param: XC_MethodHook.MethodHookParam) { 43 | this.debug( 44 | "Called ${param.method.declaringClass.name}#${param.method.name}\n" + 45 | "this: ${param.thisObject}\n" + 46 | "Arguments:\n" + 47 | param.args.mapIndexed { i, o -> "$i: $o" }.joinToString("\n") 48 | ) 49 | } 50 | 51 | fun XLog.dumpConstruction(className: String, classLoader: ClassLoader) { 52 | val cls = XposedHelpers.findClass(className, classLoader) 53 | XposedBridge.hookAllConstructors(cls, object : XC_MethodHook() { 54 | override fun afterHookedMethod(param: MethodHookParam) { 55 | debug("${param.thisObject}") 56 | } 57 | }) 58 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/BuildUtils.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | 6 | val Context.versionCode: Long 7 | get() { 8 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) 9 | this.packageManager.getPackageInfo(this.packageName, 0).longVersionCode else 10 | this.packageManager.getPackageInfo(this.packageName, 0).versionCode.toLong() 11 | } 12 | 13 | fun Long.to(max: Long): LongRange { 14 | return LongRange(this, max) 15 | } 16 | 17 | fun Int.to(max: Long): LongRange { 18 | return LongRange(this.toLong(), max) 19 | } 20 | 21 | fun Long.toMax(): LongRange { 22 | return LongRange(this, Long.MAX_VALUE) 23 | } 24 | 25 | fun Int.toMax(): LongRange { 26 | return LongRange(this.toLong(), Long.MAX_VALUE) 27 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/ConfigExtensions.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import xyz.rodit.xposed.client.ConfigurationClient 4 | 5 | fun ConfigurationClient.getList(key: String): List { 6 | return this.getString(key, "[]") 7 | .drop(1) 8 | .dropLast(1) 9 | .split(',') 10 | .filter(String::isNotBlank) 11 | .map(String::trim) 12 | } 13 | 14 | fun ConfigurationClient.getNonDefault(key: String, default: Int = 0): Int? { 15 | val value = this.getInt(key, default) 16 | return if (value == default) null else value 17 | } 18 | 19 | fun ConfigurationClient.getResolution(key: String): Resolution? { 20 | val parts = this.getString(key, "0").split('x') 21 | if (parts.size != 2) return null 22 | return try { 23 | val dimens = parts.map { it.toInt() } 24 | Resolution(dimens[0], dimens[1]) 25 | } catch (ex: Exception) { 26 | null 27 | } 28 | } 29 | 30 | data class Resolution(val width: Int, val height: Int) -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/ConversationManager.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import xyz.rodit.snapmod.logging.log 4 | import java.io.File 5 | import java.io.IOException 6 | 7 | class ConversationManager(rootDir: File, fileName: String) { 8 | 9 | private val conversations: MutableSet = HashSet() 10 | private val file: File = File(rootDir, fileName) 11 | private var lastLoaded: Long = 0 12 | 13 | val enabled: Set 14 | get() { 15 | if (file.lastModified() > lastLoaded) { 16 | load() 17 | } 18 | 19 | return conversations 20 | } 21 | 22 | fun enable(key: String?) { 23 | key?.let { 24 | if (!conversations.contains(it)) { 25 | conversations.add(it) 26 | save() 27 | } 28 | } 29 | } 30 | 31 | fun disable(key: String?) { 32 | if (conversations.remove(key)) { 33 | save() 34 | } 35 | } 36 | 37 | fun toggle(key: String?) { 38 | if (isEnabled(key)) { 39 | disable(key) 40 | } else { 41 | enable(key) 42 | } 43 | } 44 | 45 | fun isEnabled(key: String?): Boolean { 46 | return enabled.contains(key) 47 | } 48 | 49 | private fun load() { 50 | try { 51 | conversations.clear() 52 | if (file.exists()) { 53 | file.readText().split("\n").filter { it.isNotEmpty() }.forEach(conversations::add) 54 | } 55 | 56 | lastLoaded = System.currentTimeMillis() 57 | } catch (e: IOException) { 58 | log.error("Error loading conversation data.", e) 59 | } 60 | } 61 | 62 | private fun save() { 63 | try { 64 | file.writeText(conversations.joinToString("\n")) 65 | lastLoaded = System.currentTimeMillis() 66 | } catch (e: IOException) { 67 | log.error("Error saving conversation data.", e) 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/DisplayUtils.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import android.content.res.Resources.getSystem 4 | 5 | val Int.dp: Int get() = (this * getSystem().displayMetrics.density).toInt() 6 | val Int.px: Int get() = (this / getSystem().displayMetrics.density).toInt() -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/DownloadExtensions.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import android.widget.Toast 4 | import xyz.rodit.snapmod.features.FeatureContext 5 | import xyz.rodit.xposed.client.http.StreamProvider 6 | import java.util.* 7 | 8 | fun FeatureContext.download( 9 | type: String, 10 | pathParams: Map, 11 | extension: String, 12 | provider: StreamProvider, 13 | title: String? = null, 14 | description: String? = null 15 | ) { 16 | val dest = PathManager.getUri(config, type, pathParams, extension) 17 | download(dest, provider, title, description) 18 | } 19 | 20 | fun FeatureContext.download( 21 | dest: String, provider: StreamProvider, 22 | title: String? = null, 23 | description: String? = null 24 | ) { 25 | if (config.getBoolean("show_toast_on_download")) { 26 | activity?.runOnUiThread { 27 | Toast.makeText(appContext, "Download started.", Toast.LENGTH_SHORT).show() 28 | } 29 | } 30 | 31 | val uuid = UUID.randomUUID().toString() 32 | server.mapStream(uuid, provider) 33 | files.download( 34 | config.getBoolean("use_android_download_manager", true), 35 | "${server.root}/$uuid", 36 | dest, 37 | title, 38 | description 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/HookExtensions.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import de.robv.android.xposed.XC_MethodHook 4 | import xyz.rodit.dexsearch.client.xposed.HookableBase 5 | import xyz.rodit.snapmod.features.FeatureContext 6 | 7 | typealias FunctionHook = (param: XC_MethodHook.MethodHookParam) -> Unit 8 | 9 | fun HookableBase.before(hook: FunctionHook) { 10 | hook(object : XC_MethodHook() { 11 | override fun beforeHookedMethod(param: MethodHookParam) { 12 | hook(param) 13 | } 14 | }) 15 | } 16 | 17 | fun HookableBase.before(context: FeatureContext, configKey: String, hook: FunctionHook) { 18 | hook(object : XC_MethodHook() { 19 | override fun beforeHookedMethod(param: MethodHookParam) { 20 | if (context.config.getBoolean(configKey)) { 21 | hook(param) 22 | } 23 | } 24 | }) 25 | } 26 | 27 | fun HookableBase.after(hook: FunctionHook) { 28 | hook(object : XC_MethodHook() { 29 | override fun afterHookedMethod(param: MethodHookParam) { 30 | hook(param) 31 | } 32 | }) 33 | } 34 | 35 | fun HookableBase.after(context: FeatureContext, configKey: String, hook: FunctionHook) { 36 | hook(object : XC_MethodHook() { 37 | override fun afterHookedMethod(param: MethodHookParam) { 38 | if (context.config.getBoolean(configKey)) { 39 | hook(param) 40 | } 41 | } 42 | }) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/LogExtensions.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | val Any.TAG: String 4 | get() { 5 | val tag = javaClass.simpleName 6 | return if (tag.length <= 23) tag else tag.substring(0, 23) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/PairExtensions.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import de.robv.android.xposed.XposedHelpers 4 | 5 | val Any.pairFirst: Any? 6 | get() { 7 | return try { 8 | XposedHelpers.getObjectField(this, "a") 9 | } catch (ex: Throwable) { 10 | null 11 | } 12 | } 13 | 14 | val Any.pairSecond: Any? 15 | get() { 16 | return try { 17 | XposedHelpers.getObjectField(this, "b") 18 | } catch (ex: Throwable) { 19 | null 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/PathManager.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import android.net.Uri 4 | import android.os.Build 5 | import android.os.Environment 6 | import xyz.rodit.xposed.client.ConfigurationClient 7 | import java.io.File 8 | import java.text.SimpleDateFormat 9 | import java.util.* 10 | import java.util.regex.Pattern 11 | 12 | object PathManager { 13 | const val DOWNLOAD_STORY = "story" 14 | const val DOWNLOAD_AUDIO_NOTE = "audio_note" 15 | const val DOWNLOAD_PROFILE = "profile" 16 | const val DOWNLOAD_SNAP = "snap" 17 | 18 | private const val DEFAULT_DATE_FORMAT = "dd-MM-yyyy_HH-mm-ss" 19 | 20 | private val PATTERN_PUBLIC_DIR = Pattern.compile("""\$(\w+)""") 21 | private val PATTERN_PARAMETER = Pattern.compile("%([A-Za-z]+)") 22 | private val publicDirs: MutableMap = mutableMapOf( 23 | "Downloads" to Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path, 24 | "Movies" to Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).path, 25 | "Pictures" to Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).path, 26 | "Alarms" to Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_ALARMS).path, 27 | "DCIM" to Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).path, 28 | "Music" to Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).path, 29 | "Ringtones" to Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES).path 30 | ) 31 | private val defaultPaths: MutableMap = mutableMapOf( 32 | DOWNLOAD_STORY to "\$Downloads/SnapMod/%u_story_%d", 33 | DOWNLOAD_AUDIO_NOTE to "\$Downloads/SnapMod/%u_audio_%d", 34 | DOWNLOAD_PROFILE to "\$Downloads/SnapMod/%u_profile_%d", 35 | DOWNLOAD_SNAP to "\$Downloads/SnapMod/%u_snap_%d" 36 | ) 37 | 38 | private fun appendDefaultParamsMap(config: ConfigurationClient, paramsMap: Map): Map { 39 | val appended: MutableMap = HashMap(paramsMap) 40 | appended["t"] = System.currentTimeMillis().toString() 41 | val dateFormat = 42 | SimpleDateFormat( 43 | config.getString("download_date_format", DEFAULT_DATE_FORMAT) 44 | .replace(':', '-'), 45 | Locale.getDefault() 46 | ) 47 | appended["d"] = dateFormat.format(Date()) 48 | return appended 49 | } 50 | 51 | fun getPath( 52 | config: ConfigurationClient, 53 | downloadType: String, 54 | paramsMap: Map, 55 | extension: String 56 | ): File { 57 | val params = appendDefaultParamsMap(config, paramsMap) 58 | var path = config.getString("download_path_$downloadType", defaultPaths[downloadType]) 59 | 60 | var matcher = PATTERN_PUBLIC_DIR.matcher(path) 61 | if (matcher.find()) { 62 | publicDirs[matcher.group(1).orEmpty()]?.let { 63 | path = matcher.replaceFirst(it) 64 | } 65 | } 66 | 67 | matcher = PATTERN_PARAMETER.matcher(path) 68 | val buffer = StringBuffer() 69 | while (matcher.find()) { 70 | params[matcher.group(1).orEmpty()]?.let { 71 | matcher.appendReplacement(buffer, it) 72 | } 73 | } 74 | matcher.appendTail(buffer) 75 | 76 | return File(buffer.toString() + extension) 77 | } 78 | 79 | fun getUri( 80 | config: ConfigurationClient, 81 | downloadType: String, 82 | paramsMap: Map, 83 | extension: String 84 | ): String { 85 | return Uri.fromFile(getPath(config, downloadType, paramsMap, extension)).toString() 86 | } 87 | 88 | init { 89 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 90 | publicDirs["Recordings"] = 91 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RECORDINGS).path 92 | publicDirs["Screenshots"] = 93 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_SCREENSHOTS).path 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/ProtoReader.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import xyz.rodit.snapmod.logging.log 4 | import java.math.BigInteger 5 | 6 | private val BIGINT_2 = BigInteger.valueOf(2) 7 | private const val TYPE_VAR_INT = 0 8 | private const val TYPE_STRING = 2 9 | 10 | internal inline infix fun Byte.and(other: Int): Int = toInt() and other 11 | 12 | internal inline infix fun Byte.shl(other: Int): Int = toInt() shl other 13 | 14 | class ProtoReader(private val data: ByteArray) { 15 | 16 | private var position = 0 17 | private var checkpoint = 0 18 | 19 | fun read(): List { 20 | val parts = mutableListOf() 21 | 22 | while (position < data.size) { 23 | checkpoint = position 24 | 25 | val varInt = internalReadVarint32() 26 | val type = varInt and 0b111 27 | val index = varInt shr 3 28 | 29 | var value = ByteArray(0) 30 | 31 | if (type == TYPE_VAR_INT) { 32 | value = internalReadVarint32().toString().toByteArray() 33 | } else if (type == TYPE_STRING) { 34 | val length = internalReadVarint32() 35 | value = ByteArray(length) 36 | data.copyInto(value, 0, position, position + length) 37 | position += length 38 | } else { 39 | log.error("Unknown protobuf type $type") 40 | } 41 | 42 | parts.add(ProtoPart(index, type, value)) 43 | } 44 | 45 | return parts 46 | } 47 | 48 | private fun readByte(): Byte { 49 | return data[position++] 50 | } 51 | 52 | private fun internalReadVarint32(): Int { 53 | var tmp = readByte() 54 | if (tmp >= 0) { 55 | return tmp.toInt() 56 | } 57 | var result = tmp and 0x7f 58 | tmp = readByte() 59 | if (tmp >= 0) { 60 | result = result or (tmp shl 7) 61 | } else { 62 | result = result or (tmp and 0x7f shl 7) 63 | tmp = readByte() 64 | if (tmp >= 0) { 65 | result = result or (tmp shl 14) 66 | } else { 67 | result = result or (tmp and 0x7f shl 14) 68 | tmp = readByte() 69 | if (tmp >= 0) { 70 | result = result or (tmp shl 21) 71 | } else { 72 | result = result or (tmp and 0x7f shl 21) 73 | tmp = readByte() 74 | result = result or (tmp shl 28) 75 | if (tmp < 0) { 76 | for (i in 0..4) { 77 | if (readByte() >= 0) { 78 | return result 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | return result 86 | } 87 | } 88 | 89 | data class ProtoPart(val index: Int, val type: Int, val value: ByteArray) -------------------------------------------------------------------------------- /app/src/main/java/xyz/rodit/snapmod/util/UUIDUtil.kt: -------------------------------------------------------------------------------- 1 | package xyz.rodit.snapmod.util 2 | 3 | import xyz.rodit.snapmod.mappings.UUID 4 | import java.nio.ByteBuffer 5 | 6 | fun Any.toUUIDString(): String { 7 | return UUID.wrap(this).toUUIDString() 8 | } 9 | 10 | fun UUID.toUUIDString(): String { 11 | return (this.id as ByteArray).toUUIDString() 12 | } 13 | 14 | fun ByteArray.toUUIDString(): String { 15 | val bb = ByteBuffer.wrap(this) 16 | val high = bb.long 17 | val low = bb.long 18 | return java.util.UUID(high, low).toString() 19 | } 20 | 21 | fun String.toSnapUUID(): UUID { 22 | return UUID(arrayOf(this.toUUIDBytes())) 23 | } 24 | 25 | fun String.toUUIDBytes(): ByteArray { 26 | val uuid = java.util.UUID.fromString(this) 27 | val bb = ByteBuffer.wrap(ByteArray(16)) 28 | bb.putLong(uuid.mostSignificantBits) 29 | bb.putLong(uuid.leastSignificantBits) 30 | return bb.array() 31 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Image 4 | Video 5 | Video (No Sound) 6 | 7 | 8 | 9 | IMAGE 10 | VIDEO 11 | VIDEO_NO_SOUND 12 | 13 | 14 | 15 | Username 16 | Added Me Back 17 | Suggested 18 | Shared Username 19 | Shared Story 20 | Group Chat 21 | 22 | 23 | 24 | ADDED_BY_USERNAME 25 | ADDED_BY_ADDED_ME_BACK 26 | ADDED_BY_SUGGESTED 27 | ADDED_BY_SHARED_USERNAME 28 | ADDED_BY_SHARED_STORY 29 | ADDED_BY_GROUP_CHAT 30 | 31 | 32 | 33 | Friends 34 | Subscriptions 35 | For You 36 | 37 | 38 | 39 | friends 40 | subs 41 | for_you 42 | 43 | 44 | 45 | Chats 46 | Snaps 47 | Typing 48 | 49 | 50 | 51 | chat 52 | snap 53 | typing 54 | 55 | 56 | 57 | Warnings 58 | Errors 59 | Debug 60 | 61 | 62 | 63 | 1 64 | 2 65 | 4 66 | 67 | 68 | 69 | Every Launch 70 | Every 6 Hours 71 | Every 12 Hours 72 | Every Day 73 | Never 74 | 75 | 76 | 77 | 0 78 | 21600000 79 | 43200000 80 | 86400000 81 | -1 82 | 83 | 84 | 85 | Chats 86 | Stickers 87 | Images/Videos 88 | Snaps 89 | Notes 90 | 91 | 92 | 93 | CHAT 94 | STICKER 95 | EXTERNAL_MEDIA 96 | SNAP 97 | NOTE 98 | 99 | 100 | 101 | Pin 102 | Stealth 103 | Auto Save 104 | Auto Download Snaps 105 | 106 | 107 | 108 | pinning 109 | stealth 110 | auto_save 111 | auto_download 112 | 113 | 114 | 115 | CHAT 116 | STICKER 117 | EXTERNAL_MEDIA 118 | NOTE 119 | 120 | 121 | 122 | Default 123 | 124 | 125 | 126 | 0 127 | 128 | 129 | 130 | com.snapchat.android 131 | 132 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SnapMod 3 | 4 | Settings 5 | Privacy 6 | Tweaks 7 | 🧪 Camera 8 | Snaps 9 | Downloads 10 | Notifications 11 | Location Sharing 12 | Information 13 | 14 | Disable Camera 15 | Hide Read Receipts 16 | 🧪 Hide Chat Saves/Unsaves 17 | Hide Screenshot/Record Notifications 18 | Hide Saved to Camera Roll 19 | Hide Replay Notifications 20 | Hide Bitmoji Presence 21 | Hide Typing Notifications 22 | Save Any Message 23 | 🧪 Don\'t Release Messages 24 | Disable Bitmojis 25 | Message Preview Limit 26 | 🧪 Override Add Friend Method 27 | 🧪 Add Friend Override Value 28 | Hide Friends 29 | Edit Hidden Friends 30 | Enter the username of each friend you would like to hide on a new line. 31 | Allow Pinning Chats 32 | Block Ads 33 | Disable Spotlight Tab 34 | Hide Story Sections 35 | Hide Snap Views 36 | Anonymous Story Viewing 37 | Ignore Story Viewing (Local) 38 | Disable Some Metrics and Logging 39 | Audio Note Playback Speed 40 | Auto-Save Messages 41 | Auto-Save Message Types 42 | 43 | Send Gallery as Snaps 44 | Gallery Snaps Time Limit 45 | Allow Saving Snaps 46 | Allow Download Stories 47 | Allow Download Public DPs 48 | Public DP Download Resolution 49 | 🧪 Override Gallery Snap Type 50 | 🧪 Snap Type Override 51 | Bypass Video Length Restrictions 52 | 53 | Story Download Path 54 | Snap Download Path 55 | Audio Note Download Path 56 | Public Profile Picture Download Path 57 | Always Use Snap Download Path 58 | Forces the custom snap download path above to be used for saving snaps through the Save to Camera Roll action. 59 | Use Android Download Manager 60 | 61 | 🧪 Location Sharing Override 62 | 🧪 Latitude 63 | 🧪 Longitude 64 | 65 | Show More Info on Profile 66 | Log Network Requests 67 | Log Request Body 68 | Log Level 69 | 70 | Unlimited Snap/Story Duration 71 | 72 | Disables the camera in all parts of the app. This vastly improves performance and startup time. 73 | Prevents read receipts from being sent. 74 | Prevents chat save/unsave messages from being sent. 75 | Prevents screenshot notifications from being sent. 76 | Prevents saved to camera roll messages from being sent. 77 | Prevents snap replay messages from being sent. 78 | Prevents other users from seeing your bitmoji on the chat screen. 79 | Prevents typing push notifications from being sent to other users. 80 | Allows any message type to be saved. This is sometimes required for saving snaps and audio notes. 81 | Prevents messages from being \'released\'. This should prevent them from being deleted, although I am unsure. 82 | Disables all Bitmojis throughout the app. 83 | The maximum number of messages to show in the conversation preview. 84 | Hides the given list of friends from your friends and best friends lists. 85 | Allows chats to be pinned like on iOS. 86 | Removes all ads between stories/discover videos. 87 | Disables the Spotlight/Discover (5th) tab. This can improve performance. 88 | Hides the selected sections from the stories tab. You can use this to hide the \'For You\' section. Requires Snapchat restart. 89 | Prevents other users from seeing you have opened their snaps. Snaps cannot be saved in chat while this option is enabled. 90 | Prevents users from knowing you have viewed their stories. 91 | Prevents stories from being marked as viewed locally. 92 | Disables some metrics (Graphene) and logging (Blizzard). 93 | Sets the default audio note playback speed. 94 | Auto-saves all messages when they appear (visible to all chat participants). 95 | Sets the types of message to auto-save. 96 | Forces all images/videos sent in chats to be sent as live snaps. 97 | Sets the gallery snaps\' timer (in seconds). 98 | Allows snaps to be saved to gallery as if they were saved in chat. 99 | Allows stories (and any full screen snap) to be saved. A \'Save\' option is added to the context menu of stories. 100 | Allows profile pictures of friends with public profiles to be saved. Hold down on their profile picture on their friends page. 101 | The resolution (width and height) to download public profile pictures at. 102 | Sets the type of snap being sent when sending gallery media as a snap. 103 | Overrides the add method other users see when you add them as a friend. 104 | Removes the video time limit restrictions when sending snaps from the gallery drawer in chat. 105 | Stories and snaps will loop and never auto-close or auto-advance. 106 | The path on the device to download stories to. 107 | The path on the device to download snaps to. 108 | The path on the device to download audio notes to. 109 | The path on the device to download profile pictures to. 110 | Whether or not to use the Android system download manager. This is required when downloading to public directories on Android 11+. Try turning this off if you have issues downloading media. 111 | Overrides your shared location with the configured latitude and logitude below. I have no idea if this actually works. 112 | Shows more information on friends\' profile pages such as their birthday (if available). 113 | Logs some HTTP requests to LSPosed for debugging. 114 | Enable all log levels if you are experiencing problems. Include LSPosed logs in any issues on Github. 115 | 116 | Installation Status 117 | Checking… 118 | 119 | Donations 120 | If you would like to support SnapMod, you can donate by tapping here. 121 | https://github.com/rodit/SnapMod#donations 122 | 123 | Updates 124 | Update Check Frequency 125 | Sets the minimum amount of time between SnapMod update checks. Note: SnapMod still only checks for updates when you launch it (never in the background). 126 | 127 | Check For Updates 128 | Checks for updates now. 129 | 130 | Date Format 131 | Sets the date format for substituted dates (%d) in download paths. 132 | 133 | Show Toast 134 | If enabled, a toast message will be shown when downloads start. 135 | 136 | Disable Snap Splitting 137 | Stops long snaps from being split up into parts. 138 | 139 | Enable New Voice Notes 140 | Force enables the new voice note UI. 141 | 142 | Show Message In Notifications 143 | Displays message content in notifications. 144 | 145 | Show Media Previews 146 | Shows previews in the notifications for non-text media (images and videos). Note, this requires the entire media to be downloaded so this will consume data when notifications are received. 147 | 148 | Filtered Message Types 149 | Notifications will not be shown for filtered message types. 150 | 151 | Hidden Chat Options 152 | Hides extra chat options (if you feel the chat menu is too cluttered, for example). 153 | 154 | Auto-Download Snaps 155 | Automatically downloads snaps when viewed. 156 | 157 | Auto-Download Stories 158 | Automatically downloads stories when viewed. 159 | 160 | Enable New Chat Menu 161 | Enables the new chat context menu (recommended). 162 | 163 | Enable Story List 164 | Replaces the default story carousel with a list. 165 | 166 | Notice 167 | Tap to read. 168 | Note, increasing video resolution, FPS and bitrate all have an effect on the media size. This means high quality videos will take up a lot of storage space and will take a long time to upload and download for the recipient. 169 | 170 | Image Resolution 171 | Video Resolution 172 | 173 | Video FPS 174 | Set to 0 for default FPS. 175 | 176 | Video Bitrate (bps) 177 | Set to 0 for default bitrate. Higher bitrate leads to higher quality but also larger file sizes. For example, 4k videos typically have a bitrate of 60000000 to 100000000 bps. 178 | 179 | Force Source Encoding 180 | Forces Snapchat to use the source resolution and bitrate when saving/sending videos and images. This must be enabled for custom video/image resolution to be noticeable. 181 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:7.3.0' 9 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21' 10 | 11 | // NOTE: Do not place your application dependencies here; they belong 12 | // in the individual module build.gradle files 13 | } 14 | } 15 | 16 | task clean(type: Delete) { 17 | delete rootProject.buildDir 18 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodit/SnapMod/d96082ab5931a0767772bd16dfb31709e9ac5869/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 02 16:44:55 GMT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | maven { 7 | url = uri("https://maven.pkg.github.com/rodit/XposedLib") 8 | credentials { 9 | username = System.getenv("GITHUB_ACTOR") ?: gh_user 10 | password = System.getenv("GITHUB_TOKEN") ?: gh_key 11 | } 12 | } 13 | } 14 | } 15 | rootProject.name = "SnapMod" 16 | include ':app' 17 | --------------------------------------------------------------------------------