├── .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 |
--------------------------------------------------------------------------------