From ce9833f2c38831852b3a196922c237520d0485f4 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:32:50 +0200 Subject: [PATCH] refactor: bridge and mapper (#130) * feat: fingerprint spoof * fetch upstream - fix errors * fix(ci): wrong env * fix all items being shown and translations * use filter instead of check * fix(snap_enums/ContentType): status plus gift * refactor: download bridge * fix: check if bridge svc is correctly installed * refactor bridge - async handlers - optimized reply callback calls - add timeout debug when sending a bridge msg * fix: check for bridge correctly initialized instead of connected * fix: android compat + suppress warnings * fix(feature/manager): set a fixed thread pool * feat: dex mapper * build: libs catalog * fix: infinite loading * refactor: config ui items * feat: disable google play services dialogs * fix(mapper): getClassName * fix(bridge/force_start): use broadcast receiver instead of activity * fix: notification filter * refactor: aidl * fix(bridge): bindServiceAsUser handler * refactor: download manager * fix(download/callback): add outputPath * fix(mapper): lspatch support * fix(notifications): blacklist & reply - config value container translation key * feat: download from notifications * fix(notification/reply): use FLAG_ONLY_ALERT_ONCE * fix: alert dialog builder theme * hooker method inline * fix: immersive camera preview * fix: chat action menu design * fix(download/processor): fallback toasts * fix(message_logger): limit thread pool to 10 * feat(download): logging * fix: screenshot detection in snaps * feat: hide friend suggestions * refactor: download content URIs * feat: preview group chats * fix(config): ask for save folder * fix(download_manager): async item preview * fix(media_downloader): story replies * fix(notifications): concurrent exception * build: kotlin dsl * fix(ci): upload only on main * fix(build): getVersion task * fix(build): abi filters * fix(build): default product flavor * test(mapper): mapping tests * fix(mappings): group chat preview * fix(mapper): dexlib interfaces instead of impl --------- Co-authored-by: auth <64337177+authorisation@users.noreply.github.com> --- .github/workflows/android.yml | 2 + app/build.gradle | 110 ------ app/build.gradle.kts | 119 +++++++ app/proguard-rules.pro | 21 -- app/src/main/AndroidManifest.xml | 18 +- .../snapenhance/bridge/BridgeInterface.aidl | 108 ++++++ .../snapenhance/bridge/DownloadCallback.aidl | 7 + app/src/main/assets/lang/en_US.json | 65 +++- .../kotlin/me/rhunk/snapenhance/ModContext.kt | 8 +- .../me/rhunk/snapenhance/SharedContext.kt | 34 +- .../me/rhunk/snapenhance/SnapEnhance.kt | 37 ++- .../action/impl/ExportChatMessages.kt | 9 +- .../action/impl/RefreshMappings.kt | 2 +- .../bridge/AbstractBridgeClient.kt | 122 ------- .../rhunk/snapenhance/bridge/BridgeClient.kt | 126 +++++++ .../rhunk/snapenhance/bridge/BridgeService.kt | 105 ++++++ .../snapenhance/bridge/ForceStartActivity.kt | 11 + .../bridge/client/RootBridgeClient.kt | 155 --------- .../bridge/client/ServiceBridgeClient.kt | 270 --------------- .../bridge/common/BridgeMessage.kt | 16 - .../bridge/common/BridgeMessageType.kt | 23 -- .../impl/download/DownloadContentRequest.kt | 20 -- .../impl/download/DownloadContentResult.kt | 17 - .../common/impl/file/FileAccessRequest.kt | 33 -- .../common/impl/file/FileAccessResult.kt | 20 -- .../common/impl/locale/LocaleRequest.kt | 12 - .../bridge/common/impl/locale/LocaleResult.kt | 31 -- .../messagelogger/MessageLoggerListResult.kt | 18 - .../messagelogger/MessageLoggerRequest.kt | 34 -- .../impl/messagelogger/MessageLoggerResult.kt | 20 -- .../bridge/service/BridgeService.kt | 189 ----------- .../impl/file => types}/BridgeFileType.kt | 4 +- .../bridge/{ => wrapper}/ConfigWrapper.kt | 7 +- .../{ => wrapper}/MessageLoggerWrapper.kt | 2 +- .../{ => wrapper}/TranslationWrapper.kt | 7 +- .../snapenhance/config/ConfigCategory.kt | 6 +- .../snapenhance/config/ConfigProperty.kt | 81 +++-- .../config/impl/ConfigStringValue.kt | 1 + .../me/rhunk/snapenhance/data/FileType.kt | 7 + .../me/rhunk/snapenhance/data/SnapEnums.kt | 65 ++-- .../data/wrapper/impl/MessageDestinations.kt | 1 + .../download/DownloadManagerClient.kt | 92 ++--- ...anagerReceiver.kt => DownloadProcessor.kt} | 161 +++++---- .../download/DownloadTaskManager.kt | 57 ++-- .../download/data/DownloadMetadata.kt | 9 + .../download/data/DownloadRequest.kt | 86 +---- .../download/data/PendingDownload.kt | 21 +- .../download/data/SplitMediaAssetType.kt | 5 + .../snapenhance/features/BridgeFileFeature.kt | 2 +- .../me/rhunk/snapenhance/features/Feature.kt | 4 + .../snapenhance/features/impl/AutoUpdater.kt | 3 +- .../snapenhance/features/impl/Messaging.kt | 14 +- .../impl/downloader/AntiAutoDownload.kt | 2 +- .../impl/downloader/MediaDownloader.kt | 184 ++++++---- .../features/impl/experiments/AppPasscode.kt | 3 +- .../impl/experiments/DeviceSpooferHook.kt | 47 +++ .../impl/experiments/MeoPasscodeBypass.kt | 6 +- .../impl/privacy/PreventMessageSending.kt | 47 +-- .../features/impl/spying/MessageLogger.kt | 18 +- .../features/impl/spying/StealthMode.kt | 2 +- .../features/impl/tweaks/AntiAutoSave.kt | 2 +- .../impl/tweaks/GalleryMediaSendOverride.kt | 3 +- .../impl/tweaks/GooglePlayServicesDialogs.kt | 24 ++ .../impl/tweaks/MediaQualityLevelOverride.kt | 9 +- .../features/impl/tweaks/Notifications.kt | 314 ++++++++++-------- .../features/impl/ui/PinConversations.kt | 2 +- .../snapenhance/features/impl/ui/UITweaks.kt | 35 +- .../me/rhunk/snapenhance/hook/Hooker.kt | 86 +++-- .../snapenhance/manager/impl/ConfigManager.kt | 2 +- .../manager/impl/FeatureManager.kt | 26 +- .../manager/impl/MappingManager.kt | 136 +++----- .../me/rhunk/snapenhance/mapping/Mapper.kt | 13 - .../mapping/impl/BCryptClassMapper.kt | 26 -- .../mapping/impl/CallbackMapper.kt | 29 -- .../mapping/impl/DefaultMediaItemMapper.kt | 24 -- .../snapenhance/mapping/impl/EnumMapper.kt | 66 ---- .../impl/OperaPageViewControllerMapper.kt | 78 ----- .../impl/PlatformAnalyticsCreatorMapper.kt | 26 -- .../mapping/impl/PlusSubscriptionMapper.kt | 27 -- .../mapping/impl/ScCameraSettingsMapper.kt | 21 -- .../mapping/impl/StoryBoostStateMapper.kt | 21 -- .../me/rhunk/snapenhance/ui/ItemHelper.kt | 85 +++++ .../ui/{menu => }/ViewAppearanceHelper.kt | 6 +- .../snapenhance/ui/config/ConfigActivity.kt | 124 ++++--- .../download/DebugSettingsLayoutInflater.kt | 6 +- .../ui/download/DownloadListAdapter.kt | 129 +++++-- .../ui/download/DownloadManagerActivity.kt | 4 +- .../ui/menu/impl/ChatActionMenu.kt | 61 +++- .../ui/menu/impl/FriendFeedInfoMenu.kt | 23 +- .../ui/menu/impl/MenuViewInjector.kt | 22 +- .../ui/menu/impl/OperaContextActionMenu.kt | 2 +- .../snapenhance/ui/menu/impl/SettingsMenu.kt | 14 +- .../ui/spoof/DeviceSpooferActivity.kt | 111 +++++++ .../util/ActivityResultCallback.kt | 5 + .../util/AndroidCompatExtensions.kt | 13 + .../util/export/MessageExporter.kt | 3 +- .../util/snap/MediaDownloaderHelper.kt | 10 +- .../snapenhance/util/snap/PreviewUtils.kt | 1 - .../res/layout/device_spoofer_activity.xml | 27 ++ build.gradle | 10 - build.gradle.kts | 8 + gradle/libs.versions.toml | 36 ++ gradle/wrapper/gradle-wrapper.properties | 2 +- mapper/.gitignore | 1 + mapper/build.gradle.kts | 21 ++ .../rhunk/snapmapper/AbstractClassMapper.kt | 9 + .../main/kotlin/me/rhunk/snapmapper/Mapper.kt | 90 +++++ .../me/rhunk/snapmapper/MapperContext.kt | 58 ++++ .../me/rhunk/snapmapper/ext/DexClassDef.kt | 23 ++ .../me/rhunk/snapmapper/ext/DexMethod.kt | 44 +++ .../snapmapper/impl/BCryptClassMapper.kt | 34 ++ .../rhunk/snapmapper/impl/CallbackMapper.kt | 27 ++ .../snapmapper/impl/DefaultMediaItemMapper.kt | 21 ++ .../me/rhunk/snapmapper/impl/EnumMapper.kt | 72 ++++ .../impl/FriendsFeedEventDispatcherMapper.kt | 27 ++ .../impl/MediaQualityLevelProviderMapper.kt | 24 ++ .../impl/OperaPageViewControllerMapper.kt | 52 +++ .../impl/PlatformAnalyticsCreatorMapper.kt | 23 ++ .../snapmapper/impl/PlusSubscriptionMapper.kt | 28 ++ .../snapmapper/impl/ScCameraSettingsMapper.kt | 20 ++ .../snapmapper/impl/StoryBoostStateMapper.kt | 23 ++ mapper/src/test/kotlin/android/util/Log.kt | 27 ++ .../snapenhance/mapper/tests/TestMappings.kt | 33 ++ settings.gradle => settings.gradle.kts | 8 +- 124 files changed, 2676 insertions(+), 2367 deletions(-) delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts delete mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl create mode 100644 app/src/main/aidl/me/rhunk/snapenhance/bridge/DownloadCallback.aidl delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/AbstractBridgeClient.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/RootBridgeClient.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/ServiceBridgeClient.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessage.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessageType.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/download/DownloadContentRequest.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/download/DownloadContentResult.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/FileAccessRequest.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/FileAccessResult.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/locale/LocaleRequest.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/locale/LocaleResult.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerListResult.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerRequest.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerResult.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt rename app/src/main/kotlin/me/rhunk/snapenhance/bridge/{common/impl/file => types}/BridgeFileType.kt (89%) rename app/src/main/kotlin/me/rhunk/snapenhance/bridge/{ => wrapper}/ConfigWrapper.kt (92%) rename app/src/main/kotlin/me/rhunk/snapenhance/bridge/{ => wrapper}/MessageLoggerWrapper.kt (98%) rename app/src/main/kotlin/me/rhunk/snapenhance/bridge/{ => wrapper}/TranslationWrapper.kt (93%) rename app/src/main/kotlin/me/rhunk/snapenhance/download/{DownloadManagerReceiver.kt => DownloadProcessor.kt} (69%) create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/Mapper.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/BCryptClassMapper.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/CallbackMapper.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/DefaultMediaItemMapper.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/OperaPageViewControllerMapper.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlatformAnalyticsCreatorMapper.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlusSubscriptionMapper.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/ScCameraSettingsMapper.kt delete mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/StoryBoostStateMapper.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt rename app/src/main/kotlin/me/rhunk/snapenhance/ui/{menu => }/ViewAppearanceHelper.kt (94%) create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt create mode 100644 app/src/main/res/layout/device_spoofer_activity.xml delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml create mode 100644 mapper/.gitignore create mode 100644 mapper/build.gradle.kts create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/AbstractClassMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/Mapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/MapperContext.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/ext/DexClassDef.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/ext/DexMethod.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/BCryptClassMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/CallbackMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/DefaultMediaItemMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/EnumMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/FriendsFeedEventDispatcherMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/MediaQualityLevelProviderMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/OperaPageViewControllerMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlatformAnalyticsCreatorMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlusSubscriptionMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/ScCameraSettingsMapper.kt create mode 100644 mapper/src/main/kotlin/me/rhunk/snapmapper/impl/StoryBoostStateMapper.kt create mode 100644 mapper/src/test/kotlin/android/util/Log.kt create mode 100644 mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt rename settings.gradle => settings.gradle.kts (82%) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index d56ed474..d3637f4b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -71,7 +71,9 @@ jobs: path: app/build/outputs/apk/armv7/debug/*.apk - name: CI Upload armv8 + if: github.ref_name == 'main' run: node ./.github/workflows/upload.js -t "${{ secrets.TELEGRAM_BOT_TOKEN }}" -f "app/build/outputs/apk/armv8/debug/app-${{ env.version }}-armv8-${{ steps.version-env.outputs.sha_short }}.apk" --caption "A new commit has been pushed to the ${{ env.GIT_BRANCH_NAME }} branch! ${{ steps.version-env.outputs.sha_short }}" --chatid "${{ secrets.TELEGRAM_CHAT_ID }}" - name: CI Upload armv7 + if: github.ref_name == 'main' run: node ./.github/workflows/upload.js -t "${{ secrets.TELEGRAM_BOT_TOKEN }}" -f "app/build/outputs/apk/armv7/debug/app-${{ env.version }}-armv7-${{ steps.version-env.outputs.sha_short }}.apk" --chatid "${{ secrets.TELEGRAM_CHAT_ID }}" diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 39388d03..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,110 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' -} - -def appVersionName = "1.1.0" -def appVersionCode = 7 - -android { - compileSdk 33 - buildToolsVersion = "33.0.2" - - defaultConfig { - applicationId "me.rhunk.snapenhance" - minSdk 28 - targetSdk 33 - versionCode appVersionCode - versionName appVersionName - multiDexEnabled true - } - - buildTypes { - release { - minifyEnabled false - shrinkResources false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - applicationVariants.configureEach { variant -> - variant.outputs.configureEach { - outputFileName = "app-${appVersionName}-${variant.flavorName}.apk" - } - } - - flavorDimensions "release" - - productFlavors { - armv8 { - getIsDefault().set(true) - ndk { - //noinspection ChromeOsAbiSupport - abiFilters "arm64-v8a" - } - dimension "release" - } - armv7 { - ndk { - //noinspection ChromeOsAbiSupport - abiFilters "armeabi-v7a" - } - packagingOptions { - exclude 'lib/armeabi-v7a/*_neon.so' - } - dimension "release" - } - } - - kotlinOptions { - jvmTarget = '1.8' - } - namespace 'me.rhunk.snapenhance' -} - -afterEvaluate { - //auto install for debug purpose - getTasks().getByPath(":app:assembleArmv8Debug").doLast { - def apkDebugFile = android.applicationVariants.find { it.buildType.name == "debug" && it.flavorName == "armv8" }.outputs[0].outputFile - try { - println "Killing Snapchat" - exec { - commandLine "adb", "shell", "am", "force-stop", "com.snapchat.android" - } - println "Installing debug build" - exec() { - commandLine "adb", "install", "-r", "-d", apkDebugFile.absolutePath - } - println "Starting Snapchat" - exec { - commandLine "adb", "shell", "am", "start", "com.snapchat.android" - } - } catch (Throwable t) { - println "Failed to install debug build" - t.printStackTrace() - } - } -} - -task getVersion { - doLast { - def version = new File('app/build/version.txt') - version.text = android.defaultConfig.versionName - } -} - -dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' - implementation 'org.jetbrains.kotlin:kotlin-reflect:1.8.21' - implementation 'androidx.recyclerview:recyclerview:1.3.0' - - compileOnly files('libs/LSPosed-api-1.0-SNAPSHOT.jar') - implementation 'com.google.code.gson:gson:2.10.1' - implementation 'com.arthenica:ffmpeg-kit-full-gpl:5.1.LTS' - implementation 'org.osmdroid:osmdroid-android:6.1.16' - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..56ccc7e1 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,119 @@ +import com.android.build.gradle.internal.api.BaseVariantOutputImpl + +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.kotlinAndroid) +} + +val appVersionName = "1.1.0" +val appVersionCode = 7 + +android { + namespace = "me.rhunk.snapenhance" + compileSdk = 33 + + buildFeatures { + aidl = true + } + + defaultConfig { + applicationId = "me.rhunk.snapenhance" + minSdk = 28 + //noinspection OldTargetApi + targetSdk = 33 + + versionCode = appVersionCode + versionName = appVersionName + multiDexEnabled = true + } + + + buildTypes { + release { + isMinifyEnabled = false + isShrinkResources = false + } + } + + flavorDimensions += "abi" + + productFlavors { + create("armv8") { + ndk { + abiFilters.add("arm64-v8a") + } + + dimension = "abi" + } + + create("armv7") { + ndk { + abiFilters.add("armeabi-v7a") + } + packaging { + jniLibs { + excludes += "**/*_neon.so" + } + } + dimension = "abi" + } + } + + properties["debug_flavor"]?.let { + android.productFlavors[it.toString()].setIsDefault(true) + } + + applicationVariants.all { + outputs.map { it as BaseVariantOutputImpl }.forEach { variant -> + variant.outputFileName = "app-${appVersionName}-${variant.name}.apk" + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + compileOnly(files("libs/LSPosed-api-1.0-SNAPSHOT.jar")) + implementation(libs.coroutines) + implementation(libs.kotlin.reflect) + implementation(libs.recyclerview) + implementation(libs.gson) + implementation(libs.ffmpeg.kit) + implementation(libs.osmdroid.android) + implementation(libs.okhttp) + implementation(libs.androidx.documentfile) + + implementation(project(":mapper")) +} + +tasks.register("getVersion") { + doLast { + val versionFile = File("app/build/version.txt") + versionFile.writeText(android.defaultConfig.versionName.toString()) + } +} + +afterEvaluate { + properties["debug_assemble_task"]?.let { tasks.named(it.toString()) }?.orNull?.doLast { + runCatching { + val apkDebugFile = android.applicationVariants.find { it.buildType.name == "debug" && it.flavorName == properties["debug_flavor"] }?.outputs?.first()?.outputFile ?: return@doLast + exec { + commandLine("adb", "shell", "am", "force-stop", "com.snapchat.android") + } + exec { + commandLine("adb", "install", "-r", "-d", apkDebugFile.absolutePath) + } + exec { + commandLine("adb", "shell", "am", "start", "com.snapchat.android") + } + } + } +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c698c9a..a56d65c3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,18 +32,11 @@ android:resource="@array/sc_scope" /> - - - - - - + + \ No newline at end of file diff --git a/app/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/app/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl new file mode 100644 index 00000000..3e015a6a --- /dev/null +++ b/app/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -0,0 +1,108 @@ +package me.rhunk.snapenhance.bridge; + +import java.util.List; +import me.rhunk.snapenhance.bridge.DownloadCallback; + +interface BridgeInterface { + /** + * Create a file if it doesn't exist, and read it + * + * @param fileType the type of file to create and read + * @param defaultContent the default content to write to the file if it doesn't exist + * @return the content of the file + */ + byte[] createAndReadFile(int fileType, in byte[] defaultContent); + + /** + * Read a file + * + * @param fileType the type of file to read + * @return the content of the file + */ + byte[] readFile(int fileType); + + /** + * Write a file + * + * @param fileType the type of file to write + * @param content the content to write to the file + * @return true if the file was written successfully + */ + boolean writeFile(int fileType, in byte[] content); + + /** + * Delete a file + * + * @param fileType the type of file to delete + * @return true if the file was deleted successfully + */ + boolean deleteFile(int fileType); + + /** + * Check if a file exists + * + * @param fileType the type of file to check + * @return true if the file exists + */ + boolean isFileExists(int fileType); + + /** + * Get the content of a logged message from the database + * + * @param conversationId the ID of the conversation + * @return the content of the message + */ + long[] getLoggedMessageIds(String conversationId, int limit); + + /** + * Get the content of a logged message from the database + * + * @param id the ID of the message logger message + * @return the content of the message + */ + @nullable byte[] getMessageLoggerMessage(String conversationId, long id); + + /** + * Add a message to the message logger database + * + * @param id the ID of the message logger message + * @param message the content of the message + */ + void addMessageLoggerMessage(String conversationId, long id, in byte[] message); + + /** + * Delete a message from the message logger database + * + * @param id the ID of the message logger message + */ + void deleteMessageLoggerMessage(String conversationId, long id); + + /** + * Clear the message logger database + */ + void clearMessageLogger(); + + /** + * Fetch the translations + * + * @return the translations result + */ + Map fetchTranslations(); + + /** + * Get check for updates last time + * @return the last time check for updates was done + */ + long getAutoUpdaterTime(); + + /** + * Set check for updates last time + * @param time the time to set + */ + void setAutoUpdaterTime(long time); + + /** + * Enqueue a download + */ + void enqueueDownload(in Intent intent, DownloadCallback callback); +} \ No newline at end of file diff --git a/app/src/main/aidl/me/rhunk/snapenhance/bridge/DownloadCallback.aidl b/app/src/main/aidl/me/rhunk/snapenhance/bridge/DownloadCallback.aidl new file mode 100644 index 00000000..473a31a4 --- /dev/null +++ b/app/src/main/aidl/me/rhunk/snapenhance/bridge/DownloadCallback.aidl @@ -0,0 +1,7 @@ +package me.rhunk.snapenhance.bridge; + +oneway interface DownloadCallback { + void onSuccess(String outputPath); + void onProgress(String message); + void onFailure(String message, @nullable String throwable); +} diff --git a/app/src/main/assets/lang/en_US.json b/app/src/main/assets/lang/en_US.json index 5a2a50af..12cfd3fd 100644 --- a/app/src/main/assets/lang/en_US.json +++ b/app/src/main/assets/lang/en_US.json @@ -50,13 +50,9 @@ "name": "Unlimited Snap View Time", "description": "Removes the time limit for viewing Snaps" }, - "prevent_screenshot_notifications": { - "name": "Prevent Screenshot Notifications", - "description": "Prevents anyone from knowing that you've taken a screenshot" - }, - "prevent_status_notifications": { - "name": "Prevent Status Notifications", - "description": "Prevents sending status notifications\ne.g. Saved to camera roll" + "prevent_sending_messages": { + "name": "Prevent Sending Messages", + "description": "Prevents sending certain types of messages" }, "anonymous_story_view": { "name": "Anonymous Story View", @@ -95,6 +91,10 @@ "name": "Force Media Source Quality", "description": "Overrides the media source quality" }, + "download_logging": { + "name": "Download Logging", + "description": "Show a toast when media is downloading" + }, "enable_friend_feed_menu_bar": { "name": "Friend Feed Menu Bar", @@ -164,6 +164,10 @@ "name": "Override Startup Page", "description": "Overrides the startup page" }, + "disable_google_play_dialogs": { + "name": "Disable Google Play Services Dialogs", + "description": "Prevent Google Play Services availability dialogs from being shown" + }, "auto_updater": { "name": "Auto Updater", @@ -218,6 +222,18 @@ "unlimited_multi_snap": { "name": "Unlimited Multi Snap", "description": "Allows you to take an unlimited amount of multi snaps" + }, + "device_spoof": { + "name": "Spoof Device Values", + "description": "Spoofs the devices values" + }, + "device_fingerprint": { + "name": "Device Fingerprint", + "description": "Spoofs the device fingerprint" + }, + "android_id": { + "name": "Android ID", + "description": "Spoofs the devices Android ID" } }, @@ -226,7 +242,8 @@ "better_notifications": { "chat": "Show chat messages", "snap": "Show medias", - "reply_button": "Add reply button" + "reply_button": "Add reply button", + "download_button": "Add download button" }, "friend_feed_menu_buttons": { "auto_download_blacklist": "\u2B07\uFE0F Auto Download Blacklist", @@ -249,6 +266,12 @@ "public_stories": "Public Stories", "spotlight": "Spotlight" }, + "download_logging": { + "started": "Started", + "success": "Success", + "progress": "Progress", + "failure": "Failure" + }, "auto_save_messages": { "NOTE": "Audio Note", "CHAT": "Chat", @@ -256,10 +279,19 @@ "SNAP": "Snap", "STICKER": "Sticker" }, - "notification_blacklist": { + "notifications": { + "chat_screenshot": "Screenshot", + "chat_screen_record": "Screen Record", + "camera_roll_save": "Camera Roll Save", "chat": "Chat", + "chat_reply": "Chat Reply", "snap": "Snap", - "typing": "Typing" + "typing": "Typing", + "stories": "Stories", + "initiate_audio": "Incoming Audio Call", + "abandon_audio": "Missed Audio Call", + "initiate_video": "Incoming Video Call", + "abandon_video": "Missed Video Call" }, "gallery_media_send_override": { "OFF": "Off", @@ -287,6 +319,7 @@ "VERTICAL_STORY_VIEWER": "Enable Vertical Story Viewer" }, "hide_story_section": { + "hide_friend_suggestions": "Hide friend suggestions", "hide_friends": "Hide friends section", "hide_following": "Hide following section", "hide_for_you": "Hide For You section" @@ -401,16 +434,21 @@ "clear_cache_title": "Clear Cache", "reset_all_title": "Reset all settings", "reset_all_confirmation": "Are you sure you want to reset all settings?", - "success_toast": "Success!" + "success_toast": "Success!", + "device_spoofer": "Device Spoofer" } }, - "download_manager_receiver": { + "download_processor": { + "download_started_toast": "Download started", + "unsupported_content_type_toast": "Unsupported content type!", + "failed_no_longer_available_toast": "Media no longer available", "already_queued_toast": "Media already in queue!", "already_downloaded_toast": "Media already downloaded!", "saved_toast": "Saved to {path}", "download_toast": "Downloading {path}...", "processing_toast": "Processing {path}...", "failed_generic_toast": "Failed to download", + "failed_to_create_preview_toast": "Failed to create preview", "failed_processing_toast": "Failed to process {error}", "failed_gallery_toast": "Failed to save to gallery {error}" }, @@ -418,5 +456,8 @@ "title": "SnapEnhance Settings", "selected_text": "{count} selected", "invalid_number_toast": "Invalid number!" + }, + "spoof_activity": { + "title": "Spoof Settings" } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt index 40bb9214..42886b91 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -11,9 +11,9 @@ import android.widget.Toast import com.google.gson.Gson import com.google.gson.GsonBuilder import kotlinx.coroutines.asCoroutineDispatcher -import me.rhunk.snapenhance.bridge.AbstractBridgeClient -import me.rhunk.snapenhance.bridge.ConfigWrapper -import me.rhunk.snapenhance.bridge.TranslationWrapper +import me.rhunk.snapenhance.bridge.BridgeClient +import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper +import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper import me.rhunk.snapenhance.data.MessageSender import me.rhunk.snapenhance.database.DatabaseAccess import me.rhunk.snapenhance.features.Feature @@ -35,7 +35,7 @@ class ModContext { lateinit var androidContext: Context var mainActivity: Activity? = null - lateinit var bridgeClient: AbstractBridgeClient + lateinit var bridgeClient: BridgeClient val gson: Gson = GsonBuilder().create() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt index b2588e6c..34da6bf4 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt @@ -7,7 +7,8 @@ import android.content.Intent import android.os.Build import android.os.Environment import android.provider.Settings -import me.rhunk.snapenhance.bridge.TranslationWrapper +import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper +import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper import me.rhunk.snapenhance.download.DownloadTaskManager import kotlin.system.exitProcess @@ -17,6 +18,7 @@ import kotlin.system.exitProcess object SharedContext { lateinit var downloadTaskManager: DownloadTaskManager lateinit var translation: TranslationWrapper + lateinit var config: ConfigWrapper private fun askForStoragePermission(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -36,17 +38,7 @@ object SharedContext { context.requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE), 0) } - fun ensureInitialized(context: Context) { - if (!this::downloadTaskManager.isInitialized) { - downloadTaskManager = DownloadTaskManager().apply { - init(context) - } - } - if (!this::translation.isInitialized) { - translation = TranslationWrapper().apply { - loadFromContext(context) - } - } + private fun askForPermissions(context: Context) { //ask for storage permission val hasStoragePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -72,4 +64,22 @@ object SharedContext { } .show() } + + fun ensureInitialized(context: Context) { + if (!this::downloadTaskManager.isInitialized) { + downloadTaskManager = DownloadTaskManager().apply { + init(context) + } + } + if (!this::translation.isInitialized) { + translation = TranslationWrapper().apply { + loadFromContext(context) + } + } + if (!this::config.isInitialized) { + config = ConfigWrapper().apply { loadFromContext(context) } + } + + askForPermissions(context) + } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt index 7b6b3523..b818d165 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -1,16 +1,17 @@ package me.rhunk.snapenhance -import android.annotation.SuppressLint import android.app.Activity import android.app.Application import android.content.Context +import android.content.pm.PackageManager import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import me.rhunk.snapenhance.bridge.AbstractBridgeClient -import me.rhunk.snapenhance.bridge.client.ServiceBridgeClient +import me.rhunk.snapenhance.bridge.BridgeClient import me.rhunk.snapenhance.data.SnapClassCache import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.hook.hook +import me.rhunk.snapenhance.util.getApplicationInfoCompat import kotlin.time.ExperimentalTime import kotlin.time.measureTime @@ -22,16 +23,24 @@ class SnapEnhance { } } private val appContext = ModContext() + private var isBridgeInitialized = false init { Hooker.hook(Application::class.java, "attach", HookStage.BEFORE) { param -> appContext.androidContext = param.arg(0).also { classLoader = it.classLoader } - appContext.bridgeClient = provideBridgeClient() + appContext.bridgeClient = BridgeClient(appContext) + + //for lspatch builds, we need to check if the service is correctly installed + runCatching { + appContext.androidContext.packageManager.getApplicationInfoCompat(BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA) + }.onFailure { + appContext.crash("SnapEnhance bridge service is not installed. Please download stable version from https://github.com/rhunk/SnapEnhance/releases") + return@hook + } appContext.bridgeClient.apply { - this.context = appContext start { bridgeResult -> if (!bridgeResult) { Logger.xposedLog("Cannot connect to bridge service") @@ -42,6 +51,8 @@ class SnapEnhance { runBlocking { init() } + }.onSuccess { + isBridgeInitialized = true }.onFailure { Logger.xposedLog("Failed to initialize", it) } @@ -49,7 +60,7 @@ class SnapEnhance { } } - Hooker.hook(Activity::class.java, "onCreate", HookStage.AFTER) { + Activity::class.java.hook( "onCreate", HookStage.AFTER, { isBridgeInitialized }) { val activity = it.thisObject() as Activity if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook val isMainActivityNotNull = appContext.mainActivity != null @@ -61,7 +72,7 @@ class SnapEnhance { var activityWasResumed = false //we need to reload the config when the app is resumed - Hooker.hook(Activity::class.java, "onResume", HookStage.AFTER) { + Activity::class.java.hook("onResume", HookStage.AFTER, { isBridgeInitialized }) { val activity = it.thisObject() as Activity if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook @@ -76,14 +87,6 @@ class SnapEnhance { } } - @SuppressLint("ObsoleteSdkInt") - private fun provideBridgeClient(): AbstractBridgeClient { - /*if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { - return RootBridgeClient() - }*/ - return ServiceBridgeClient() - } - @OptIn(ExperimentalTime::class) private suspend fun init() { //load translations in a coroutine to speed up initialization @@ -100,7 +103,7 @@ class SnapEnhance { features.init() } }.also { time -> - Logger.debug("initialized in $time") + Logger.debug("init took $time") } } @@ -112,7 +115,7 @@ class SnapEnhance { actionManager.init() } }.also { time -> - Logger.debug("onActivityCreate in $time") + Logger.debug("onActivityCreate took $time") } } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt b/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt index b3d1d223..059fd5ea 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt @@ -19,6 +19,7 @@ import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.database.objects.FriendFeedInfo import me.rhunk.snapenhance.features.impl.Messaging +import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.util.CallbackBuilder import me.rhunk.snapenhance.util.export.ExportFormat import me.rhunk.snapenhance.util.export.MessageExporter @@ -61,7 +62,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { private suspend fun askExportType() = suspendCancellableCoroutine { cont -> context.runOnUiThread { - AlertDialog.Builder(context.mainActivity) + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) .setTitle(context.translation["chat_export.select_export_format"]) .setItems(ExportFormat.values().map { it.name }.toTypedArray()) { _, which -> cont.resumeWith(Result.success(ExportFormat.values()[which])) @@ -82,7 +83,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { ContentType.NOTE, ContentType.STICKER ) - AlertDialog.Builder(context.mainActivity) + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) .setTitle(context.translation["chat_export.select_media_type"]) .setMultiChoiceItems(contentTypes.map { it.name }.toTypedArray(), BooleanArray(contentTypes.size) { false }) { _, which, isChecked -> val media = contentTypes[which] @@ -110,7 +111,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { val friendFeedEntries = context.database.getFriendFeed(20) val selectedConversations = mutableListOf() - AlertDialog.Builder(context.mainActivity) + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) .setTitle(context.translation["chat_export.select_conversation"]) .setMultiChoiceItems( friendFeedEntries.map { it.feedDisplayName ?: it.friendDisplayName!!.split("|").firstOrNull() }.toTypedArray(), @@ -248,7 +249,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") { dialogLogs.clear() val jobs = mutableListOf() - currentActionDialog = AlertDialog.Builder(context.mainActivity) + currentActionDialog = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) .setTitle(context.translation["chat_export.exporting_chats"]) .setCancelable(false) .setMessage("") diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt b/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt index 798a4521..b2be23f5 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt @@ -1,7 +1,7 @@ package me.rhunk.snapenhance.action.impl import me.rhunk.snapenhance.action.AbstractAction -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.types.BridgeFileType class RefreshMappings : AbstractAction("action.refresh_mappings") { override fun run() { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/AbstractBridgeClient.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/AbstractBridgeClient.kt deleted file mode 100644 index 5b0691b5..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/AbstractBridgeClient.kt +++ /dev/null @@ -1,122 +0,0 @@ -package me.rhunk.snapenhance.bridge - -import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType -import me.rhunk.snapenhance.bridge.common.impl.locale.LocaleResult - -abstract class AbstractBridgeClient { - lateinit var context: ModContext - - /** - * Start the bridge client - * - * @param callback the callback to call when the initialization is done - */ - abstract fun start(callback: (Boolean) -> Unit = {}) - - /** - * Create a file if it doesn't exist, and read it - * - * @param fileType the type of file to create and read - * @param defaultContent the default content to write to the file if it doesn't exist - * @return the content of the file - */ - abstract fun createAndReadFile(fileType: BridgeFileType, defaultContent: ByteArray): ByteArray - - /** - * Read a file - * - * @param fileType the type of file to read - * @return the content of the file - */ - abstract fun readFile(fileType: BridgeFileType): ByteArray - - /** - * Write a file - * - * @param fileType the type of file to write - * @param content the content to write to the file - * @return true if the file was written successfully - */ - abstract fun writeFile(fileType: BridgeFileType, content: ByteArray?): Boolean - - /** - * Delete a file - * - * @param fileType the type of file to delete - * @return true if the file was deleted successfully - */ - abstract fun deleteFile(fileType: BridgeFileType): Boolean - - /** - * Check if a file exists - * - * @param fileType the type of file to check - * @return true if the file exists - */ - abstract fun isFileExists(fileType: BridgeFileType): Boolean - - /** - * Download content from a URL and save it to a file - * - * @param url the URL to download content from - * @param path the path to save the content to - * @return true if the content was downloaded successfully - */ - abstract fun downloadContent(url: String, path: String): Boolean - - /** - * Get the content of a logged message from the database - * - * @param conversationId the ID of the conversation - * @return the content of the message - */ - abstract fun getLoggedMessageIds(conversationId: String, limit: Int): List - - /** - * Get the content of a logged message from the database - * - * @param id the ID of the message logger message - * @return the content of the message - */ - abstract fun getMessageLoggerMessage(conversationId: String, id: Long): ByteArray? - - /** - * Add a message to the message logger database - * - * @param id the ID of the message logger message - * @param message the content of the message - */ - abstract fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) - - /** - * Delete a message from the message logger database - * - * @param id the ID of the message logger message - */ - abstract fun deleteMessageLoggerMessage(conversationId: String, id: Long) - - /** - * Clear the message logger database - */ - abstract fun clearMessageLogger() - - /** - * Fetch the translations - * - * @return the translations result - */ - abstract fun fetchTranslations(): LocaleResult - - /** - * Get check for updates last time - * @return the last time check for updates was done - */ - abstract fun getAutoUpdaterTime(): Long - - /** - * Set check for updates last time - * @param time the time to set - */ - abstract fun setAutoUpdaterTime(time: Long) -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt new file mode 100644 index 00000000..7885d926 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt @@ -0,0 +1,126 @@ +package me.rhunk.snapenhance.bridge + + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.os.IBinder +import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.BuildConfig +import me.rhunk.snapenhance.Logger.xposedLog +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.bridge.types.BridgeFileType +import me.rhunk.snapenhance.data.LocalePair +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import kotlin.system.exitProcess + + +class BridgeClient( + private val context: ModContext +): ServiceConnection { + private lateinit var future: CompletableFuture + private lateinit var service: BridgeInterface + + fun start(callback: (Boolean) -> Unit) { + this.future = CompletableFuture() + + with(context.androidContext) { + //ensure the remote process is running + startActivity(Intent() + .setClassName(BuildConfig.APPLICATION_ID, ForceStartActivity::class.java.name) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + ) + + val intent = Intent() + .setClassName(BuildConfig.APPLICATION_ID, BridgeService::class.java.name) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + bindService( + intent, + Context.BIND_AUTO_CREATE, + Executors.newSingleThreadExecutor(), + this@BridgeClient + ) + } else { + XposedHelpers.callMethod( + this, + "bindServiceAsUser", + intent, + this@BridgeClient, + Context.BIND_AUTO_CREATE, + Handler(HandlerThread("BridgeClient").apply { + start() + }.looper), + android.os.Process.myUserHandle() + ) + } + } + callback(future.get()) + } + + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + this.service = BridgeInterface.Stub.asInterface(service) + future.complete(true) + } + + override fun onNullBinding(name: ComponentName) { + xposedLog("failed to connect to bridge service") + future.complete(false) + } + + override fun onServiceDisconnected(name: ComponentName) { + exitProcess(0) + } + + fun createAndReadFile( + fileType: BridgeFileType, + defaultContent: ByteArray + ): ByteArray = service.createAndReadFile(fileType.value, defaultContent) + + fun readFile(fileType: BridgeFileType): ByteArray = service.readFile(fileType.value) + + fun writeFile( + fileType: BridgeFileType, + content: ByteArray? + ): Boolean = service.writeFile(fileType.value, content) + + fun deleteFile(fileType: BridgeFileType) = service.deleteFile(fileType.value) + + + fun isFileExists(fileType: BridgeFileType) = service.isFileExists(fileType.value) + + fun getLoggedMessageIds(conversationId: String, limit: Int): LongArray = service.getLoggedMessageIds(conversationId, limit) + + fun getMessageLoggerMessage(conversationId: String, id: Long): ByteArray? = service.getMessageLoggerMessage(conversationId, id) + + fun addMessageLoggerMessage(conversationId: String,id: Long, message: ByteArray) = service.addMessageLoggerMessage(conversationId, id, message) + + fun deleteMessageLoggerMessage(conversationId: String, id: Long) = service.deleteMessageLoggerMessage(conversationId, id) + + fun clearMessageLogger() = service.clearMessageLogger() + + fun fetchTranslations() = service.fetchTranslations().map { + LocalePair(it.key, it.value) + } + + fun getAutoUpdaterTime(): Long { + createAndReadFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, "0".toByteArray()).run { + return if (isEmpty()) { + 0 + } else { + String(this).toLong() + } + } + } + + fun setAutoUpdaterTime(time: Long) { + writeFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, time.toString().toByteArray()) + } + + fun enqueueDownload(intent: Intent, callback: DownloadCallback) = service.enqueueDownload(intent, callback) +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt new file mode 100644 index 00000000..1aa1003f --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -0,0 +1,105 @@ +package me.rhunk.snapenhance.bridge + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.bridge.types.BridgeFileType +import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper +import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper +import me.rhunk.snapenhance.download.DownloadProcessor + +class BridgeService : Service() { + private lateinit var messageLoggerWrapper: MessageLoggerWrapper + override fun onBind(intent: Intent): IBinder { + messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() } + return BridgeBinder() + } + + inner class BridgeBinder : BridgeInterface.Stub() { + override fun createAndReadFile(fileType: Int, defaultContent: ByteArray?): ByteArray { + val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) + ?: return defaultContent ?: ByteArray(0) + + if (!file.exists()) { + if (defaultContent == null) { + return ByteArray(0) + } + + file.writeBytes(defaultContent) + } + + return file.readBytes() + } + + override fun readFile(fileType: Int): ByteArray { + val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) + ?: return ByteArray(0) + + if (!file.exists()) { + return ByteArray(0) + } + + return file.readBytes() + } + + override fun writeFile(fileType: Int, content: ByteArray?): Boolean { + val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) + ?: return false + + if (content == null) { + return false + } + + file.writeBytes(content) + return true + } + + override fun deleteFile(fileType: Int): Boolean { + val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) + ?: return false + + if (!file.exists()) { + return false + } + + return file.delete() + } + + override fun isFileExists(fileType: Int): Boolean { + val file = BridgeFileType.fromValue(fileType)?.resolve(this@BridgeService) + ?: return false + + return file.exists() + } + + override fun getLoggedMessageIds(conversationId: String, limit: Int) = messageLoggerWrapper.getMessageIds(conversationId, limit).toLongArray() + + override fun getMessageLoggerMessage(conversationId: String, id: Long) = messageLoggerWrapper.getMessage(conversationId, id).second + + override fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) { + messageLoggerWrapper.addMessage(conversationId, id, message) + } + + override fun deleteMessageLoggerMessage(conversationId: String, id: Long) = messageLoggerWrapper.deleteMessage(conversationId, id) + + override fun clearMessageLogger() = messageLoggerWrapper.clearMessages() + + override fun fetchTranslations() = TranslationWrapper.fetchLocales(context = this@BridgeService).associate { + it.locale to it.content + } + + override fun getAutoUpdaterTime(): Long { + throw UnsupportedOperationException() + } + + override fun setAutoUpdaterTime(time: Long) { + throw UnsupportedOperationException() + } + + override fun enqueueDownload(intent: Intent, callback: DownloadCallback) { + SharedContext.ensureInitialized(this@BridgeService) + DownloadProcessor(this@BridgeService, callback).onReceive(intent) + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt new file mode 100644 index 00000000..be6cca1f --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.bridge + +import android.app.Activity +import android.os.Bundle + +class ForceStartActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + finish() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/RootBridgeClient.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/RootBridgeClient.kt deleted file mode 100644 index 760705e8..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/RootBridgeClient.kt +++ /dev/null @@ -1,155 +0,0 @@ -package me.rhunk.snapenhance.bridge.client - -import android.os.Environment -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.bridge.AbstractBridgeClient -import me.rhunk.snapenhance.bridge.MessageLoggerWrapper -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType -import me.rhunk.snapenhance.bridge.common.impl.locale.LocaleResult -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.OutputStream -import java.util.zip.ZipInputStream - -class RootBridgeClient : AbstractBridgeClient() { - private lateinit var messageLoggerWrapper: MessageLoggerWrapper - companion object { - private val MOD_FOLDER = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),"SnapEnhance") - } - - override fun start(callback: (Boolean) -> Unit) { - if (!MOD_FOLDER.exists()) { - MOD_FOLDER.mkdirs() - } - messageLoggerWrapper = MessageLoggerWrapper(File(MOD_FOLDER, BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() } - callback(true) - } - - override fun createAndReadFile(fileType: BridgeFileType, defaultContent: ByteArray): ByteArray { - val file = File(MOD_FOLDER, fileType.fileName) - if (file.exists()) { - return readFile(fileType) - } - val outputStream = openFileWritable(file) - outputStream.write(defaultContent) - outputStream.close() - return defaultContent - } - - override fun readFile(fileType: BridgeFileType): ByteArray { - return File(MOD_FOLDER, fileType.fileName).readBytes() - } - - override fun writeFile(fileType: BridgeFileType, content: ByteArray?): Boolean { - val outputStream = openFileWritable(File(MOD_FOLDER, fileType.fileName)) - outputStream.write(content) - outputStream.close() - return true - } - - override fun deleteFile(fileType: BridgeFileType): Boolean { - val file = File(MOD_FOLDER, fileType.fileName) - val exists = file.exists() - if (exists) { - rootOperation("rm ${file.absolutePath}") - } - return exists - } - - override fun isFileExists(fileType: BridgeFileType): Boolean { - return File(MOD_FOLDER, fileType.fileName).exists() - } - - override fun downloadContent(url: String, path: String): Boolean { - return true - } - - override fun getLoggedMessageIds(conversationId: String, limit: Int): List { - return messageLoggerWrapper.getMessageIds(conversationId, limit) - } - - override fun getMessageLoggerMessage(conversationId: String, id: Long): ByteArray? { - val (state, messageData) = messageLoggerWrapper.getMessage(conversationId, id) - if (state) { - return messageData - } - return null - } - - override fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) { - messageLoggerWrapper.addMessage(conversationId, id, message) - } - - override fun deleteMessageLoggerMessage(conversationId: String, id: Long) { - messageLoggerWrapper.deleteMessage(conversationId, id) - } - - override fun clearMessageLogger() { - messageLoggerWrapper.clearMessages() - } - - override fun fetchTranslations(): LocaleResult { - val locale = "en_US"//Locale.getDefault().toString() - - //https://github.com/LSPosed/LSPosed/blob/master/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java#L36 - val moduleApk = javaClass.classLoader.javaClass.declaredFields.first { it.type == String::class.java }.let { - it.isAccessible = true - it.get(javaClass.classLoader) as String - } - - val langJsonData: ByteArray? = ZipInputStream(FileInputStream(moduleApk)).let { zip -> - while (true) { - val entry = zip.nextEntry ?: break - if (entry.name == "assets/lang/$locale.json") { - return@let zip.readBytes() - } - } - return@let null - } - - if (langJsonData != null) { - Logger.debug("Fetched translations for $locale") - return LocaleResult(arrayOf(locale), arrayOf(langJsonData.toString(Charsets.UTF_8))) - } - - throw Throwable("Failed to fetch translations for $locale") - } - - override fun getAutoUpdaterTime(): Long { - readFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP).run { - return if (isEmpty()) { - 0 - } else { - String(this).toLong() - } - } - } - - override fun setAutoUpdaterTime(time: Long) { - writeFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, time.toString().toByteArray(Charsets.UTF_8)) - } - - private fun rootOperation(command: String): String { - val process = Runtime.getRuntime().exec("su -c $command") - process.waitFor() - process.errorStream?.bufferedReader()?.let { - val error = it.readText() - if (error.isNotEmpty()) { - throw Throwable("Failed to execute root operation: $error") - } - } - Logger.debug("Root operation executed: $command") - return process.inputStream.bufferedReader().readText() - } - - private fun openFileWritable(file: File): OutputStream { - runCatching { - if (!file.exists()) rootOperation("touch ${file.absolutePath}") - }.onFailure { - Logger.error("Failed to set file permissions: ${it.message}") - } - - return FileOutputStream(file) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/ServiceBridgeClient.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/ServiceBridgeClient.kt deleted file mode 100644 index 879d0e23..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/ServiceBridgeClient.kt +++ /dev/null @@ -1,270 +0,0 @@ -package me.rhunk.snapenhance.bridge.client - - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.HandlerThread -import android.os.IBinder -import android.os.Message -import android.os.Messenger -import de.robv.android.xposed.XposedHelpers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.suspendCancellableCoroutine -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.Logger.xposedLog -import me.rhunk.snapenhance.bridge.AbstractBridgeClient -import me.rhunk.snapenhance.bridge.common.BridgeMessage -import me.rhunk.snapenhance.bridge.common.BridgeMessageType -import me.rhunk.snapenhance.bridge.common.impl.download.DownloadContentRequest -import me.rhunk.snapenhance.bridge.common.impl.download.DownloadContentResult -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType -import me.rhunk.snapenhance.bridge.common.impl.file.FileAccessRequest -import me.rhunk.snapenhance.bridge.common.impl.file.FileAccessResult -import me.rhunk.snapenhance.bridge.common.impl.locale.LocaleRequest -import me.rhunk.snapenhance.bridge.common.impl.locale.LocaleResult -import me.rhunk.snapenhance.bridge.common.impl.messagelogger.MessageLoggerListResult -import me.rhunk.snapenhance.bridge.common.impl.messagelogger.MessageLoggerRequest -import me.rhunk.snapenhance.bridge.common.impl.messagelogger.MessageLoggerResult -import me.rhunk.snapenhance.bridge.service.BridgeService -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executors -import kotlin.reflect.KClass -import kotlin.system.exitProcess - - -class ServiceBridgeClient: AbstractBridgeClient(), ServiceConnection { - private val handlerThread = HandlerThread("BridgeClient") - - private lateinit var messenger: Messenger - private lateinit var future: CompletableFuture - - override fun start(callback: (Boolean) -> Unit) { - this.future = CompletableFuture() - this.handlerThread.start() - - with(context.androidContext) { - val intent = Intent() - .setClassName(BuildConfig.APPLICATION_ID, BridgeService::class.java.name) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - bindService( - intent, - Context.BIND_AUTO_CREATE, - Executors.newSingleThreadExecutor(), - this@ServiceBridgeClient - ) - } else { - XposedHelpers.callMethod( - this, - "bindServiceAsUser", - intent, - this@ServiceBridgeClient, - Context.BIND_AUTO_CREATE, - Handler(handlerThread.looper), - android.os.Process.myUserHandle() - ) - } - } - callback(future.get()) - } - - private fun handleResponseMessage( - msg: Message - ): BridgeMessage { - val message: BridgeMessage = when (BridgeMessageType.fromValue(msg.what)) { - BridgeMessageType.FILE_ACCESS_RESULT -> FileAccessResult() - BridgeMessageType.DOWNLOAD_CONTENT_RESULT -> DownloadContentResult() - BridgeMessageType.MESSAGE_LOGGER_RESULT -> MessageLoggerResult() - BridgeMessageType.MESSAGE_LOGGER_LIST_RESULT -> MessageLoggerListResult() - BridgeMessageType.LOCALE_RESULT -> LocaleResult() - else -> throw IllegalStateException("Unknown message type: ${msg.what}") - } - - with(message) { - read(msg.data) - return this - } - } - - @Suppress("UNCHECKED_CAST", "UNUSED_PARAMETER") - private fun sendMessage( - messageType: BridgeMessageType, - bridgeMessage: BridgeMessage, - resultType: KClass? = null - ) = runBlocking { - return@runBlocking suspendCancellableCoroutine { continuation -> - with(Message.obtain()) { - what = messageType.value - replyTo = Messenger(object : Handler(handlerThread.looper) { - override fun handleMessage(msg: Message) { - if (continuation.isCompleted) { - continuation.cancel(Throwable("Already completed")) - return - } - continuation.resumeWith(Result.success(handleResponseMessage(msg) as T)) - } - }) - data = Bundle() - bridgeMessage.write(data) - messenger.send(this) - } - } - } - - override fun createAndReadFile( - fileType: BridgeFileType, - defaultContent: ByteArray - ): ByteArray { - sendMessage( - BridgeMessageType.FILE_ACCESS_REQUEST, - FileAccessRequest(FileAccessRequest.FileAccessAction.EXISTS, fileType, null), - FileAccessResult::class - ).run { - if (state!!) { - return readFile(fileType) - } - writeFile(fileType, defaultContent) - return defaultContent - } - } - - override fun readFile(fileType: BridgeFileType): ByteArray { - sendMessage( - BridgeMessageType.FILE_ACCESS_REQUEST, - FileAccessRequest(FileAccessRequest.FileAccessAction.READ, fileType, null), - FileAccessResult::class - ).run { - return content!! - } - } - - override fun writeFile( - fileType: BridgeFileType, - content: ByteArray? - ): Boolean { - sendMessage( - BridgeMessageType.FILE_ACCESS_REQUEST, - FileAccessRequest(FileAccessRequest.FileAccessAction.WRITE, fileType, content), - FileAccessResult::class - ).run { - return state!! - } - } - - override fun deleteFile(fileType: BridgeFileType): Boolean { - sendMessage( - BridgeMessageType.FILE_ACCESS_REQUEST, - FileAccessRequest(FileAccessRequest.FileAccessAction.DELETE, fileType, null), - FileAccessResult::class - ).run { - return state!! - } - } - - - override fun isFileExists(fileType: BridgeFileType): Boolean { - sendMessage( - BridgeMessageType.FILE_ACCESS_REQUEST, - FileAccessRequest(FileAccessRequest.FileAccessAction.EXISTS, fileType, null), - FileAccessResult::class - ).run { - return state!! - } - } - - override fun downloadContent(url: String, path: String): Boolean { - sendMessage( - BridgeMessageType.DOWNLOAD_CONTENT_REQUEST, - DownloadContentRequest(url, path), - DownloadContentResult::class - ).run { - return state!! - } - } - - override fun getLoggedMessageIds(conversationId: String, limit: Int): List { - sendMessage( - BridgeMessageType.MESSAGE_LOGGER_REQUEST, - MessageLoggerRequest(MessageLoggerRequest.Action.LIST_IDS, conversationId, limit.toLong()), - MessageLoggerListResult::class - ).run { - return messages!! - } - } - - override fun getMessageLoggerMessage(conversationId: String, id: Long): ByteArray? { - sendMessage( - BridgeMessageType.MESSAGE_LOGGER_REQUEST, - MessageLoggerRequest(MessageLoggerRequest.Action.GET, conversationId, id), - MessageLoggerResult::class - ).run { - return message - } - } - - override fun addMessageLoggerMessage(conversationId: String,id: Long, message: ByteArray) { - sendMessage( - BridgeMessageType.MESSAGE_LOGGER_REQUEST, - MessageLoggerRequest(MessageLoggerRequest.Action.ADD, conversationId, id, message), - MessageLoggerResult::class - ) - } - - override fun deleteMessageLoggerMessage(conversationId: String,id: Long) { - sendMessage( - BridgeMessageType.MESSAGE_LOGGER_REQUEST, - MessageLoggerRequest(MessageLoggerRequest.Action.DELETE, conversationId, id), - MessageLoggerResult::class - ) - } - - override fun clearMessageLogger() { - sendMessage( - BridgeMessageType.MESSAGE_LOGGER_REQUEST, - MessageLoggerRequest(MessageLoggerRequest.Action.CLEAR), - MessageLoggerResult::class - ) - } - - override fun fetchTranslations(): LocaleResult { - sendMessage( - BridgeMessageType.LOCALE_REQUEST, - LocaleRequest(), - LocaleResult::class - ).run { - return this - } - } - - override fun getAutoUpdaterTime(): Long { - createAndReadFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, "0".toByteArray()).run { - return if (isEmpty()) { - 0 - } else { - String(this).toLong() - } - } - } - - override fun setAutoUpdaterTime(time: Long) { - writeFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, time.toString().toByteArray()) - } - - override fun onServiceConnected(name: ComponentName, service: IBinder) { - messenger = Messenger(service) - future.complete(true) - } - - override fun onNullBinding(name: ComponentName) { - xposedLog("failed to connect to bridge service") - future.complete(false) - } - - override fun onServiceDisconnected(name: ComponentName) { - exitProcess(0) - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessage.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessage.kt deleted file mode 100644 index e12e59d7..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessage.kt +++ /dev/null @@ -1,16 +0,0 @@ -package me.rhunk.snapenhance.bridge.common - -import android.os.Bundle -import android.os.Message - -abstract class BridgeMessage { - abstract fun write(bundle: Bundle) - abstract fun read(bundle: Bundle) - - fun toMessage(what: Int): Message { - val message = Message.obtain(null, what) - message.data = Bundle() - write(message.data) - return message - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessageType.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessageType.kt deleted file mode 100644 index 62a8432d..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/BridgeMessageType.kt +++ /dev/null @@ -1,23 +0,0 @@ -package me.rhunk.snapenhance.bridge.common - - -enum class BridgeMessageType( - val value: Int = 0 -) { - UNKNOWN(-1), - FILE_ACCESS_REQUEST(0), - FILE_ACCESS_RESULT(1), - DOWNLOAD_CONTENT_REQUEST(2), - DOWNLOAD_CONTENT_RESULT(3), - LOCALE_REQUEST(4), - LOCALE_RESULT(5), - MESSAGE_LOGGER_REQUEST(6), - MESSAGE_LOGGER_RESULT(7), - MESSAGE_LOGGER_LIST_RESULT(8); - - companion object { - fun fromValue(value: Int): BridgeMessageType { - return values().firstOrNull { it.value == value } ?: UNKNOWN - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/download/DownloadContentRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/download/DownloadContentRequest.kt deleted file mode 100644 index e1854a0d..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/download/DownloadContentRequest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package me.rhunk.snapenhance.bridge.common.impl.download - -import android.os.Bundle -import me.rhunk.snapenhance.bridge.common.BridgeMessage - -class DownloadContentRequest( - var url: String? = null, - var path: String? = null -) : BridgeMessage() { - - override fun write(bundle: Bundle) { - bundle.putString("url", url) - bundle.putString("path", path) - } - - override fun read(bundle: Bundle) { - url = bundle.getString("url") - path = bundle.getString("path") - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/download/DownloadContentResult.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/download/DownloadContentResult.kt deleted file mode 100644 index 4c664f90..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/download/DownloadContentResult.kt +++ /dev/null @@ -1,17 +0,0 @@ -package me.rhunk.snapenhance.bridge.common.impl.download - -import android.os.Bundle -import me.rhunk.snapenhance.bridge.common.BridgeMessage - -class DownloadContentResult( - var state: Boolean? = null -) : BridgeMessage() { - - override fun write(bundle: Bundle) { - bundle.putBoolean("state", state!!) - } - - override fun read(bundle: Bundle) { - state = bundle.getBoolean("state") - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/FileAccessRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/FileAccessRequest.kt deleted file mode 100644 index 33f81ef4..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/FileAccessRequest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package me.rhunk.snapenhance.bridge.common.impl.file - -import android.os.Bundle -import me.rhunk.snapenhance.bridge.common.BridgeMessage - -class FileAccessRequest( - var action: FileAccessAction? = null, - var fileType: BridgeFileType? = null, - var content: ByteArray? = null -) : BridgeMessage() { - - override fun write(bundle: Bundle) { - bundle.putInt("action", action!!.value) - bundle.putInt("fileType", fileType!!.value) - bundle.putByteArray("content", content) - } - - override fun read(bundle: Bundle) { - action = FileAccessAction.fromValue(bundle.getInt("action")) - fileType = BridgeFileType.fromValue(bundle.getInt("fileType")) - content = bundle.getByteArray("content") - } - - enum class FileAccessAction(val value: Int) { - READ(0), WRITE(1), DELETE(2), EXISTS(3); - - companion object { - fun fromValue(value: Int): FileAccessAction? { - return values().firstOrNull { it.value == value } - } - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/FileAccessResult.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/FileAccessResult.kt deleted file mode 100644 index fa1d9104..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/FileAccessResult.kt +++ /dev/null @@ -1,20 +0,0 @@ -package me.rhunk.snapenhance.bridge.common.impl.file - -import android.os.Bundle -import me.rhunk.snapenhance.bridge.common.BridgeMessage - -class FileAccessResult( - var state: Boolean? = null, - var content: ByteArray? = null -) : BridgeMessage() { - - override fun write(bundle: Bundle) { - bundle.putBoolean("state", state!!) - bundle.putByteArray("content", content) - } - - override fun read(bundle: Bundle) { - state = bundle.getBoolean("state") - content = bundle.getByteArray("content") - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/locale/LocaleRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/locale/LocaleRequest.kt deleted file mode 100644 index cfeb4c1f..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/locale/LocaleRequest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package me.rhunk.snapenhance.bridge.common.impl.locale - -import android.os.Bundle -import me.rhunk.snapenhance.bridge.common.BridgeMessage - -class LocaleRequest() : BridgeMessage() { - override fun write(bundle: Bundle) { - } - - override fun read(bundle: Bundle) { - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/locale/LocaleResult.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/locale/LocaleResult.kt deleted file mode 100644 index 310b4c09..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/locale/LocaleResult.kt +++ /dev/null @@ -1,31 +0,0 @@ -package me.rhunk.snapenhance.bridge.common.impl.locale - -import android.os.Bundle -import me.rhunk.snapenhance.bridge.common.BridgeMessage -import me.rhunk.snapenhance.data.LocalePair - -class LocaleResult( - private var locales: Array? = null, - private var localContentArray: Array? = null -) : BridgeMessage(){ - - fun getLocales(): List { - val locales = locales ?: return emptyList() - val localContentArray = localContentArray ?: return emptyList() - return locales.mapIndexed { index, locale -> - LocalePair(locale, localContentArray[index]) - }.asReversed() - } - - - override fun write(bundle: Bundle) { - bundle.putStringArray("locales", locales) - bundle.putSerializable("localContentArray", localContentArray) - } - - @Suppress("UNCHECKED_CAST", "DEPRECATION") - override fun read(bundle: Bundle) { - locales = bundle.getStringArray("locales") - localContentArray = bundle.getSerializable("localContentArray") as? Array - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerListResult.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerListResult.kt deleted file mode 100644 index 1c879fee..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerListResult.kt +++ /dev/null @@ -1,18 +0,0 @@ -package me.rhunk.snapenhance.bridge.common.impl.messagelogger - -import android.os.Bundle -import me.rhunk.snapenhance.bridge.common.BridgeMessage - - -class MessageLoggerListResult( - var messages: List? = null -) : BridgeMessage() { - - override fun write(bundle: Bundle) { - bundle.putLongArray("messages", messages!!.map { it }.toLongArray()) - } - - override fun read(bundle: Bundle) { - messages = bundle.getLongArray("messages")?.toList() ?: emptyList() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerRequest.kt deleted file mode 100644 index 01253071..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerRequest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package me.rhunk.snapenhance.bridge.common.impl.messagelogger - -import android.os.Bundle -import me.rhunk.snapenhance.bridge.common.BridgeMessage - -class MessageLoggerRequest( - var action: Action? = null, - var conversationId: String? = null, - var index: Long? = null, - var message: ByteArray? = null -) : BridgeMessage(){ - - override fun write(bundle: Bundle) { - bundle.putString("action", action!!.name) - bundle.putString("conversationId", conversationId) - bundle.putLong("messageId", index ?: 0) - bundle.putByteArray("message", message) - } - - override fun read(bundle: Bundle) { - action = Action.valueOf(bundle.getString("action")!!) - conversationId = bundle.getString("conversationId") - index = bundle.getLong("messageId") - message = bundle.getByteArray("message") - } - - enum class Action { - ADD, - GET, - CLEAR, - DELETE, - LIST_IDS - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerResult.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerResult.kt deleted file mode 100644 index f88e2b5d..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/messagelogger/MessageLoggerResult.kt +++ /dev/null @@ -1,20 +0,0 @@ -package me.rhunk.snapenhance.bridge.common.impl.messagelogger - -import android.os.Bundle -import me.rhunk.snapenhance.bridge.common.BridgeMessage - -class MessageLoggerResult( - var state: Boolean? = null, - var message: ByteArray? = null -) : BridgeMessage() { - - override fun write(bundle: Bundle) { - bundle.putBoolean("state", state!!) - bundle.putByteArray("message", message) - } - - override fun read(bundle: Bundle) { - state = bundle.getBoolean("state") - message = bundle.getByteArray("message") - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt deleted file mode 100644 index 4b832bf0..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt +++ /dev/null @@ -1,189 +0,0 @@ -package me.rhunk.snapenhance.bridge.service - -import android.annotation.SuppressLint -import android.app.DownloadManager -import android.app.Service -import android.content.* -import android.net.Uri -import android.os.* -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.bridge.MessageLoggerWrapper -import me.rhunk.snapenhance.bridge.TranslationWrapper -import me.rhunk.snapenhance.bridge.common.BridgeMessageType -import me.rhunk.snapenhance.bridge.common.impl.* -import me.rhunk.snapenhance.bridge.common.impl.download.DownloadContentRequest -import me.rhunk.snapenhance.bridge.common.impl.download.DownloadContentResult -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType -import me.rhunk.snapenhance.bridge.common.impl.file.FileAccessRequest -import me.rhunk.snapenhance.bridge.common.impl.file.FileAccessResult -import me.rhunk.snapenhance.bridge.common.impl.locale.LocaleRequest -import me.rhunk.snapenhance.bridge.common.impl.locale.LocaleResult -import me.rhunk.snapenhance.bridge.common.impl.messagelogger.MessageLoggerListResult -import me.rhunk.snapenhance.bridge.common.impl.messagelogger.MessageLoggerRequest -import me.rhunk.snapenhance.bridge.common.impl.messagelogger.MessageLoggerResult -import java.io.File -import java.util.* - -class BridgeService : Service() { - private lateinit var messageLoggerWrapper: MessageLoggerWrapper - - override fun onBind(intent: Intent): IBinder { - messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() } - - return Messenger(object : Handler(Looper.getMainLooper()) { - override fun handleMessage(msg: Message) { - runCatching { - this@BridgeService.handleMessage(msg) - }.onFailure { - Logger.error("Failed to handle message ${BridgeMessageType.fromValue(msg.what)}", it) - } - } - }).binder - } - - private fun handleMessage(msg: Message) { - val replyMessenger = msg.replyTo - when (BridgeMessageType.fromValue(msg.what)) { - BridgeMessageType.FILE_ACCESS_REQUEST -> { - with(FileAccessRequest()) { - read(msg.data) - handleFileAccess(this) { message -> - replyMessenger.send(message) - } - } - } - BridgeMessageType.DOWNLOAD_CONTENT_REQUEST -> { - with(DownloadContentRequest()) { - read(msg.data) - handleDownloadContent(this) { message -> - replyMessenger.send(message) - } - } - } - BridgeMessageType.LOCALE_REQUEST -> { - with(LocaleRequest()) { - read(msg.data) - handleLocaleRequest { message -> - replyMessenger.send(message) - } - } - } - BridgeMessageType.MESSAGE_LOGGER_REQUEST -> { - with(MessageLoggerRequest()) { - read(msg.data) - handleMessageLoggerRequest(this) { message -> - replyMessenger.send(message) - } - } - } - - else -> Logger.log("Unknown message type: " + msg.what) - } - } - - private fun handleMessageLoggerRequest(msg: MessageLoggerRequest, reply: (Message) -> Unit) { - when (msg.action) { - MessageLoggerRequest.Action.ADD -> { - val isSuccess = messageLoggerWrapper.addMessage(msg.conversationId!!, msg.index!!, msg.message!!) - reply(MessageLoggerResult(isSuccess).toMessage(BridgeMessageType.MESSAGE_LOGGER_RESULT.value)) - return - } - MessageLoggerRequest.Action.CLEAR -> { - messageLoggerWrapper.clearMessages() - } - MessageLoggerRequest.Action.DELETE -> { - messageLoggerWrapper.deleteMessage(msg.conversationId!!, msg.index!!) - } - MessageLoggerRequest.Action.GET -> { - val (state, messageData) = messageLoggerWrapper.getMessage(msg.conversationId!!, msg.index!!) - reply(MessageLoggerResult(state, messageData).toMessage(BridgeMessageType.MESSAGE_LOGGER_RESULT.value)) - return - } - MessageLoggerRequest.Action.LIST_IDS -> { - val messageIds = messageLoggerWrapper.getMessageIds(msg.conversationId!!, msg.index!!.toInt()) - reply(MessageLoggerListResult(messageIds).toMessage(BridgeMessageType.MESSAGE_LOGGER_LIST_RESULT.value)) - return - } - else -> { - Logger.log(Exception("Unknown message logger action: ${msg.action}")) - } - } - - reply(MessageLoggerResult(true).toMessage(BridgeMessageType.MESSAGE_LOGGER_RESULT.value)) - } - - private fun handleLocaleRequest(reply: (Message) -> Unit) { - val locales = sortedSetOf() - val contentArray = sortedSetOf() - - TranslationWrapper.fetchLocales(context = this).forEach { pair -> - locales.add(pair.locale) - contentArray.add(pair.content) - } - - reply(LocaleResult(locales.toTypedArray(), contentArray.toTypedArray()).toMessage(BridgeMessageType.LOCALE_RESULT.value)) - } - - @SuppressLint("UnspecifiedRegisterReceiverFlag") - private fun handleDownloadContent(msg: DownloadContentRequest, reply: (Message) -> Unit) { - if (!msg.url!!.startsWith("http://127.0.0.1:")) return - - val outputFile = File(msg.path!!) - outputFile.parentFile?.let { - if (!it.exists()) it.mkdirs() - } - val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager - val request = DownloadManager.Request(Uri.parse(msg.url)) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) - .setAllowedOverMetered(true) - .setAllowedOverRoaming(true) - .setDestinationUri(Uri.fromFile(outputFile)) - val downloadId = downloadManager.enqueue(request) - registerReceiver(object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != downloadId) return - unregisterReceiver(this) - reply(DownloadContentResult(true).toMessage(BridgeMessageType.DOWNLOAD_CONTENT_RESULT.value)) - } - }, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) - } - - private fun handleFileAccess(msg: FileAccessRequest, reply: (Message) -> Unit) { - val fileFolder = if (msg.fileType!!.isDatabase) { - File(dataDir, "databases") - } else { - File(filesDir.absolutePath) - } - val requestFile = File(fileFolder, msg.fileType!!.fileName) - - val result: FileAccessResult = when (msg.action) { - FileAccessRequest.FileAccessAction.READ -> { - if (!requestFile.exists()) { - FileAccessResult(false, null) - } else { - FileAccessResult(true, requestFile.readBytes()) - } - } - FileAccessRequest.FileAccessAction.WRITE -> { - if (!requestFile.exists()) { - requestFile.createNewFile() - } - requestFile.writeBytes(msg.content!!) - FileAccessResult(true, null) - } - FileAccessRequest.FileAccessAction.DELETE -> { - if (!requestFile.exists()) { - FileAccessResult(false, null) - } else { - requestFile.delete() - FileAccessResult(true, null) - } - } - FileAccessRequest.FileAccessAction.EXISTS -> FileAccessResult(requestFile.exists(), null) - else -> throw Exception("Unknown action: " + msg.action) - } - - reply(result.toMessage(BridgeMessageType.FILE_ACCESS_RESULT.value)) - } - -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/BridgeFileType.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt similarity index 89% rename from app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/BridgeFileType.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt index 382cab51..65b13d06 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/common/impl/file/BridgeFileType.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt @@ -1,10 +1,10 @@ -package me.rhunk.snapenhance.bridge.common.impl.file +package me.rhunk.snapenhance.bridge.types import android.content.Context import java.io.File -enum class BridgeFileType(val value: Int, val fileName: String, val displayName: String, val isDatabase: Boolean = false) { +enum class BridgeFileType(val value: Int, val fileName: String, val displayName: String, private val isDatabase: Boolean = false) { CONFIG(0, "config.json", "Config"), MAPPINGS(1, "mappings.json", "Mappings"), MESSAGE_LOGGER_DATABASE(2, "message_logger.db", "Message Logger",true), diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ConfigWrapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt similarity index 92% rename from app/src/main/kotlin/me/rhunk/snapenhance/bridge/ConfigWrapper.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt index db46f91a..df338ac4 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ConfigWrapper.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt @@ -1,10 +1,11 @@ -package me.rhunk.snapenhance.bridge +package me.rhunk.snapenhance.bridge.wrapper import android.content.Context import com.google.gson.GsonBuilder import com.google.gson.JsonObject import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.BridgeClient +import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.config.ConfigAccessor import me.rhunk.snapenhance.config.ConfigProperty @@ -70,7 +71,7 @@ class ConfigWrapper: ConfigAccessor() { load() } - fun loadFromBridge(bridgeClient: AbstractBridgeClient) { + fun loadFromBridge(bridgeClient: BridgeClient) { isFileExistsAction = { bridgeClient.isFileExists(BridgeFileType.CONFIG) } readFileAction = { bridgeClient.createAndReadFile(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8)) } writeFileAction = { bridgeClient.writeFile(BridgeFileType.CONFIG, it) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/MessageLoggerWrapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt similarity index 98% rename from app/src/main/kotlin/me/rhunk/snapenhance/bridge/MessageLoggerWrapper.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt index 601912dd..f25f4dc3 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/MessageLoggerWrapper.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt @@ -1,4 +1,4 @@ -package me.rhunk.snapenhance.bridge +package me.rhunk.snapenhance.bridge.wrapper import android.content.ContentValues import android.database.sqlite.SQLiteDatabase diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/TranslationWrapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt similarity index 93% rename from app/src/main/kotlin/me/rhunk/snapenhance/bridge/TranslationWrapper.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt index a7225f0b..9bd2cc67 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/TranslationWrapper.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt @@ -1,9 +1,10 @@ -package me.rhunk.snapenhance.bridge +package me.rhunk.snapenhance.bridge.wrapper import android.content.Context import com.google.gson.JsonObject import com.google.gson.JsonParser import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.bridge.BridgeClient import me.rhunk.snapenhance.data.LocalePair import java.util.Locale @@ -62,8 +63,8 @@ class TranslationWrapper { scanObject(translations) } - fun loadFromBridge(bridgeClient: AbstractBridgeClient) { - bridgeClient.fetchTranslations().getLocales().forEach { + fun loadFromBridge(bridgeClient: BridgeClient) { + bridgeClient.fetchTranslations().forEach { load(it) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt index 00598280..47f3356c 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt @@ -1,12 +1,14 @@ package me.rhunk.snapenhance.config enum class ConfigCategory( - val key: String + val key: String, + val hidden: Boolean = false ) { SPYING_PRIVACY("spying_privacy"), MEDIA_MANAGEMENT("media_manager"), UI_TWEAKS("ui_tweaks"), UPDATES("updates"), CAMERA("camera"), - EXPERIMENTAL_DEBUGGING("experimental_debugging"); + EXPERIMENTAL_DEBUGGING("experimental_debugging"), + DEVICE_SPOOFER("device_spoofer", hidden = true) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt index 50c40d4f..39fa7390 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt @@ -1,18 +1,18 @@ package me.rhunk.snapenhance.config -import android.os.Environment import me.rhunk.snapenhance.config.impl.ConfigIntegerValue import me.rhunk.snapenhance.config.impl.ConfigStateListValue import me.rhunk.snapenhance.config.impl.ConfigStateSelection import me.rhunk.snapenhance.config.impl.ConfigStateValue import me.rhunk.snapenhance.config.impl.ConfigStringValue +import me.rhunk.snapenhance.data.NotificationType import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks -import java.io.File enum class ConfigProperty( val translationKey: String, val category: ConfigCategory, val valueContainer: ConfigValue<*>, + val valueContainerTranslationKey: String? = null, val shouldAppearInSettings: Boolean = true, val disableValueLocalization: Boolean = false ) { @@ -36,11 +36,12 @@ enum class ConfigProperty( "better_notifications", ConfigCategory.SPYING_PRIVACY, ConfigStateListValue( - listOf("snap", "chat", "reply_button"), + listOf("snap", "chat", "reply_button", "download_button"), mutableMapOf( "snap" to false, "chat" to false, - "reply_button" to false + "reply_button" to false, + "download_button" to false ) ) ), @@ -48,13 +49,10 @@ enum class ConfigProperty( "notification_blacklist", ConfigCategory.SPYING_PRIVACY, ConfigStateListValue( - listOf("snap", "chat", "typing"), - mutableMapOf( - "snap" to false, - "chat" to false, - "typing" to false - ) - ) + NotificationType.getIncomingValues().map { it.key }, + NotificationType.getIncomingValues().associate { it.key to false }.toMutableMap() + ), + valueContainerTranslationKey = "notifications", ), DISABLE_METRICS("disable_metrics", ConfigCategory.SPYING_PRIVACY, @@ -68,15 +66,14 @@ enum class ConfigProperty( ConfigCategory.SPYING_PRIVACY, ConfigStateValue(false) ), - PREVENT_SCREENSHOT_NOTIFICATIONS( - "prevent_screenshot_notifications", + PREVENT_SENDING_MESSAGES( + "prevent_sending_messages", ConfigCategory.SPYING_PRIVACY, - ConfigStateValue(false) - ), - PREVENT_STATUS_NOTIFICATIONS( - "prevent_status_notifications", - ConfigCategory.SPYING_PRIVACY, - ConfigStateValue(false) + ConfigStateListValue( + NotificationType.getOutgoingValues().map { it.key }, + NotificationType.getOutgoingValues().associate { it.key to false }.toMutableMap() + ), + valueContainerTranslationKey = "notifications", ), ANONYMOUS_STORY_VIEW( "anonymous_story_view", @@ -93,10 +90,7 @@ enum class ConfigProperty( SAVE_FOLDER( "save_folder", ConfigCategory.MEDIA_MANAGEMENT, - ConfigStringValue(File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).absolutePath + "/Snapchat", - "SnapEnhance" - ).absolutePath) + ConfigStringValue("", isFolderPath =true), ), AUTO_DOWNLOAD_OPTIONS( "auto_download_options", @@ -160,6 +154,19 @@ enum class ConfigProperty( ConfigCategory.MEDIA_MANAGEMENT, ConfigStateValue(false) ), + DOWNLOAD_LOGGING( + "download_logging", + ConfigCategory.MEDIA_MANAGEMENT, + ConfigStateListValue( + listOf("started", "success", "progress", "failure"), + mutableMapOf( + "started" to false, + "success" to true, + "progress" to false, + "failure" to true + ) + ) + ), //UI AND TWEAKS ENABLE_FRIEND_FEED_MENU_BAR( @@ -203,8 +210,9 @@ enum class ConfigProperty( "hide_story_section", ConfigCategory.UI_TWEAKS, ConfigStateListValue( - listOf("hide_friends", "hide_following", "hide_for_you"), + listOf("hide_friend_suggestions", "hide_friends", "hide_following", "hide_for_you"), mutableMapOf( + "hide_friend_suggestions" to false, "hide_friends" to false, "hide_following" to false, "hide_for_you" to false @@ -294,7 +302,11 @@ enum class ConfigProperty( "OFF" ) ), - + DISABLE_GOOGLE_PLAY_DIALOGS( + "disable_google_play_dialogs", + ConfigCategory.UI_TWEAKS, + ConfigStateValue(false) + ), //CAMERA CAMERA_DISABLE( @@ -376,8 +388,27 @@ enum class ConfigProperty( "unlimited_multi_snap", ConfigCategory.EXPERIMENTAL_DEBUGGING, ConfigStateValue(false) + ), + + //DEVICE SPOOFER + DEVICE_SPOOF( + "device_spoof", + ConfigCategory.DEVICE_SPOOFER, + ConfigStateValue(false) + ), + FINGERPRINT( + "device_fingerprint", + ConfigCategory.DEVICE_SPOOFER, + ConfigStringValue("") + ), + ANDROID_ID( + "android_id", + ConfigCategory.DEVICE_SPOOFER, + ConfigStringValue("") ); + fun getOptionTranslationKey(key: String) = "option.property.${valueContainerTranslationKey ?: translationKey}.$key" + companion object { fun sortedByCategory(): List { return values().sortedBy { it.category.ordinal } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt index 359c5648..5f991adc 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt @@ -4,6 +4,7 @@ import me.rhunk.snapenhance.config.ConfigValue class ConfigStringValue( private var value: String = "", + val isFolderPath: Boolean = false, val isHidden: Boolean = false ) : ConfigValue() { override fun value() = value diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt index bfdb8125..33d8f114 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.data import java.io.File +import java.io.InputStream enum class FileType( val fileExtension: String? = null, @@ -58,5 +59,11 @@ enum class FileType( val hex = bytesToHex(headerBytes) return fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value ?: UNKNOWN } + + fun fromInputStream(inputStream: InputStream): FileType { + val buffer = ByteArray(16) + inputStream.read(buffer) + return fromByteArray(buffer) + } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt index 3b3b02ff..61f55c08 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt @@ -4,39 +4,39 @@ enum class MessageState { PREPARING, SENDING, COMMITTED, FAILED, CANCELING } -enum class ChatMediaType ( - val value: Int +enum class NotificationType ( + val key: String, + val isIncoming: Boolean = false, + val associatedOutgoingContentType: ContentType? = null, ) { - IMAGE(0), - VIDEO(1), - VIDEO_NO_SOUND(2), - FRIEND_DEPRECATED(3), - BLOB(4), - LAGUNA_SOUND(5), - LAGUNA_NO_SOUND(6), - GIF(7), - FINGERPRINT_HEADER_SIZE(8), - AUDIO_STITCH(9), - PSYCHOMANTIS(10), - SCREAMINGMANTIS(11), - MALIBU_SOUND(12), - MALIBU_NO_SOUND(13), - LAGUNAHD_SOUND(14), - LAGUNAHD_NO_SOUND(15), - GHOSTMANTIS(16), - NEWPORT_SOUND(17), - NEWPORT_NO_SOUND(18), - AUDIO(19), - BLOOP(20), - SPECTACLES_IMAGE(21), - SPECTACLES_VIDEO(22), - SPECTACLES_VIDEO_NO_SOUND(23), - CHEERIOS_IMAGE(24), - CHEERIOS_VIDEO_SOUND(25), - CHEERIOS_VIDEO_NO_SOUND(26), - WEB(27), - UNRECOGNIZED_VALUE(-9999); + SCREENSHOT("chat_screenshot", true, ContentType.STATUS_CONVERSATION_CAPTURE_SCREENSHOT), + SCREEN_RECORD("chat_screen_record", true, ContentType.STATUS_CONVERSATION_CAPTURE_RECORD), + CAMERA_ROLL_SAVE("camera_roll_save", true, ContentType.STATUS_SAVE_TO_CAMERA_ROLL), + SNAP("snap",true), + CHAT("chat",true), + CHAT_REPLY("chat_reply",true), + TYPING("typing", true), + STORIES("stories",true), + INITIATE_AUDIO("initiate_audio",true), + ABANDON_AUDIO("abandon_audio", false, ContentType.STATUS_CALL_MISSED_AUDIO), + INITIATE_VIDEO("initiate_video",true), + ABANDON_VIDEO("abandon_video", false, ContentType.STATUS_CALL_MISSED_VIDEO); + + companion object { + fun getIncomingValues(): List { + return values().filter { it.isIncoming }.toList() + } + + fun getOutgoingValues(): List { + return values().filter { it.associatedOutgoingContentType != null }.toList() + } + + fun fromContentType(contentType: ContentType): NotificationType? { + return values().firstOrNull { it.associatedOutgoingContentType == contentType } + } + } } + enum class ContentType(val id: Int) { UNKNOWN(-1), SNAP(0), @@ -56,7 +56,8 @@ enum class ContentType(val id: Int) { CREATIVE_TOOL_ITEM(14), FAMILY_CENTER_INVITE(15), FAMILY_CENTER_ACCEPT(16), - FAMILY_CENTER_LEAVE(17); + FAMILY_CENTER_LEAVE(17), + STATUS_PLUS_GIFT(18); companion object { fun fromId(i: Int): ContentType { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt index 6fd58f74..a827c028 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt @@ -4,6 +4,7 @@ import me.rhunk.snapenhance.data.wrapper.AbstractWrapper import me.rhunk.snapenhance.util.getObjectField import me.rhunk.snapenhance.util.setObjectField +@Suppress("UNCHECKED_CAST") class MessageDestinations(obj: Any) : AbstractWrapper(obj){ var conversations get() = (instanceNonNull().getObjectField("mConversations") as ArrayList<*>).map { SnapUUID(it) } set(value) = instanceNonNull().setObjectField("mConversations", value.map { it.instanceNonNull() }.toCollection(ArrayList())) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt index 5560f542..d973b2f4 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt @@ -2,83 +2,63 @@ package me.rhunk.snapenhance.download import android.content.Intent import android.os.Bundle -import me.rhunk.snapenhance.BuildConfig import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.bridge.DownloadCallback +import me.rhunk.snapenhance.download.data.DashOptions +import me.rhunk.snapenhance.download.data.DownloadMetadata import me.rhunk.snapenhance.download.data.DownloadRequest +import me.rhunk.snapenhance.download.data.InputMedia import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair import me.rhunk.snapenhance.download.enums.DownloadMediaType class DownloadManagerClient ( private val context: ModContext, - private val outputPath: String, - private val mediaDisplaySource: String?, - private val mediaDisplayType: String?, - private val iconUrl: String?, - private val uniqueHash: String? + private val metadata: DownloadMetadata, + private val callback: DownloadCallback ) { - private fun sendToBroadcastReceiver(bundle: Bundle) { - val intent = Intent() - intent.setClassName(BuildConfig.APPLICATION_ID, DownloadManagerReceiver::class.java.name) - intent.action = DownloadManagerReceiver.DOWNLOAD_ACTION - intent.putExtras(bundle) - context.androidContext.sendBroadcast(intent) - } - - private fun sendToBroadcastReceiver( - request: DownloadRequest, - extras: Bundle.() -> Unit = {} - ) { - sendToBroadcastReceiver(request.toBundle().apply { - putString("outputPath", outputPath) - putString("mediaDisplaySource", mediaDisplaySource) - putString("mediaDisplayType", mediaDisplayType) - putString("iconUrl", iconUrl) - putString("uniqueHash", uniqueHash) - }.apply(extras)) + private fun enqueueDownloadRequest(request: DownloadRequest) { + context.bridgeClient.enqueueDownload(Intent().apply { + putExtras(Bundle().apply { + putString(DownloadProcessor.DOWNLOAD_REQUEST_EXTRA, context.gson.toJson(request)) + putString(DownloadProcessor.DOWNLOAD_METADATA_EXTRA, context.gson.toJson(metadata)) + }) + }, callback) } fun downloadDashMedia(playlistUrl: String, offsetTime: Long, duration: Long?) { - sendToBroadcastReceiver( + enqueueDownloadRequest( DownloadRequest( - inputMedias = arrayOf(playlistUrl), - inputTypes = arrayOf(DownloadMediaType.REMOTE_MEDIA.name), + inputMedias = arrayOf(InputMedia( + content = playlistUrl, + type = DownloadMediaType.REMOTE_MEDIA + )), + dashOptions = DashOptions(offsetTime, duration), flags = DownloadRequest.Flags.IS_DASH_PLAYLIST ) - ) { - putBundle("dashOptions", Bundle().apply { - putString("offsetTime", offsetTime.toString()) - duration?.let { putString("duration", it.toString()) } - }) - } + ) } - fun downloadMedia(mediaData: String, mediaType: DownloadMediaType, encryption: MediaEncryptionKeyPair? = null) { - sendToBroadcastReceiver( + fun downloadSingleMedia(mediaData: String, mediaType: DownloadMediaType, encryption: MediaEncryptionKeyPair? = null) { + enqueueDownloadRequest( DownloadRequest( - inputMedias = arrayOf(mediaData), - inputTypes = arrayOf(mediaType.name), - mediaEncryption = if (encryption != null) mapOf(mediaData to encryption) else mapOf() + inputMedias = arrayOf(InputMedia( + content = mediaData, + type = mediaType, + encryption = encryption + )) ) ) } fun downloadMediaWithOverlay( - videoData: String, - overlayData: String, - videoType: DownloadMediaType = DownloadMediaType.LOCAL_MEDIA, - overlayType: DownloadMediaType = DownloadMediaType.LOCAL_MEDIA, - videoEncryption: MediaEncryptionKeyPair? = null, - overlayEncryption: MediaEncryptionKeyPair? = null) - { - val encryptionMap = mutableMapOf() - - if (videoEncryption != null) encryptionMap[videoData] = videoEncryption - if (overlayEncryption != null) encryptionMap[overlayData] = overlayEncryption - sendToBroadcastReceiver(DownloadRequest( - inputMedias = arrayOf(videoData, overlayData), - inputTypes = arrayOf(videoType.name, overlayType.name), - mediaEncryption = encryptionMap, - flags = DownloadRequest.Flags.SHOULD_MERGE_OVERLAY - )) + original: InputMedia, + overlay: InputMedia, + ) { + enqueueDownloadRequest( + DownloadRequest( + inputMedias = arrayOf(original, overlay), + flags = DownloadRequest.Flags.MERGE_OVERLAY + ) + ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerReceiver.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt similarity index 69% rename from app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerReceiver.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt index 518e2c98..9dc23792 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerReceiver.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt @@ -1,12 +1,12 @@ package me.rhunk.snapenhance.download import android.annotation.SuppressLint -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Handler import android.widget.Toast +import androidx.documentfile.provider.DocumentFile +import com.google.gson.GsonBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -17,7 +17,11 @@ import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.bridge.DownloadCallback +import me.rhunk.snapenhance.bridge.wrapper.ConfigWrapper +import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.data.FileType +import me.rhunk.snapenhance.download.data.DownloadMetadata import me.rhunk.snapenhance.download.data.DownloadRequest import me.rhunk.snapenhance.download.data.InputMedia import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair @@ -49,33 +53,29 @@ data class DownloadedFile( ) /** - * DownloadManagerReceiver handles the download requests of the user + * DownloadProcessor handles the download requests of the user */ @OptIn(ExperimentalEncodingApi::class) -class DownloadManagerReceiver : BroadcastReceiver() { +class DownloadProcessor ( + private val context: Context, + private val callback: DownloadCallback +) { companion object { - const val DOWNLOAD_ACTION = "me.rhunk.snapenhance.download.DownloadManagerReceiver.DOWNLOAD_ACTION" + const val DOWNLOAD_REQUEST_EXTRA = "request" + const val DOWNLOAD_METADATA_EXTRA = "metadata" } private val translation by lazy { - SharedContext.translation.getCategory("download_manager_receiver") + SharedContext.translation.getCategory("download_processor") } - private lateinit var context: Context - - private fun runOnUIThread(block: () -> Unit) { - Handler(context.mainLooper).post(block) + private val gson by lazy { + GsonBuilder().setPrettyPrinting().create() } - private fun shortToast(text: String) { - runOnUIThread { - Toast.makeText(context, text, Toast.LENGTH_SHORT).show() - } - } - - private fun longToast(text: String) { - runOnUIThread { - Toast.makeText(context, text, Toast.LENGTH_LONG).show() + private fun fallbackToast(message: Any) { + android.os.Handler(context.mainLooper).post { + Toast.makeText(context, message.toString(), Toast.LENGTH_SHORT).show() } } @@ -117,42 +117,58 @@ class DownloadManagerReceiver : BroadcastReceiver() { private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) { if (coroutineContext.job.isCancelled) return + val config = ConfigWrapper().apply { loadFromContext(context) } + runCatching { val fileType = FileType.fromFile(inputFile) if (fileType == FileType.UNKNOWN) { - longToast(translation.format("failed_gallery_toast", "error" to "Unknown media type")) + callback.onFailure(translation.format("failed_gallery_toast", "error" to "Unknown media type"), null) return } - val outputFile = File(pendingDownload.outputPath + "." + fileType.fileExtension).also { createNeededDirectories(it) } - inputFile.copyTo(outputFile, overwrite = true) + val fileName = pendingDownload.metadata.outputPath.substringAfterLast("/") + "." + fileType.fileExtension - pendingDownload.outputFile = outputFile.absolutePath + val outputFolder = DocumentFile.fromTreeUri(context, Uri.parse(config.string(ConfigProperty.SAVE_FOLDER))) + ?: throw Exception("Failed to open output folder") + + val outputFileFolder = pendingDownload.metadata.outputPath.let { + if (it.contains("/")) { + it.substringBeforeLast("/").split("/").fold(outputFolder) { folder, name -> + folder.findFile(name) ?: folder.createDirectory(name)!! + } + } else { + outputFolder + } + } + + val outputFile = outputFileFolder.createFile(fileType.mimeType, fileName)!! + val outputStream = context.contentResolver.openOutputStream(outputFile.uri)!! + + inputFile.inputStream().use { inputStream -> + inputStream.copyTo(outputStream) + } + + pendingDownload.outputFile = outputFile.uri.toString() pendingDownload.downloadStage = DownloadStage.SAVED runCatching { - val contentUri = Uri.fromFile(outputFile) val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE") - mediaScanIntent.setData(contentUri) + mediaScanIntent.setData(outputFile.uri) context.sendBroadcast(mediaScanIntent) }.onFailure { Logger.error("Failed to scan media file", it) - longToast(translation.format("failed_gallery_toast", "error" to it.toString())) + callback.onFailure(translation.format("failed_gallery_toast", "error" to it.toString()), it.message) } Logger.debug("download complete") - - //print the path of the saved media - val parentName = outputFile.parentFile?.parentFile?.absolutePath?.let { - if (!it.endsWith("/")) "$it/" else it + fileName.let { + runCatching { callback.onSuccess(it) }.onFailure { fallbackToast(it) } + } + }.onFailure { exception -> + Logger.error(exception) + translation.format("failed_gallery_toast", "error" to exception.toString()).let { + runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) } } - - shortToast( - translation.format("saved_toast", "path" to outputFile.absolutePath.replace(parentName ?: "", "")) - ) - }.onFailure { - Logger.error(it) - longToast(translation.format("failed_gallery_toast", "error" to it.toString())) pendingDownload.downloadStage = DownloadStage.FAILED } } @@ -165,7 +181,7 @@ class DownloadManagerReceiver : BroadcastReceiver() { val jobs = mutableListOf() val downloadedMedias = mutableMapOf() - downloadRequest.getInputMedias().forEach { inputMedia -> + downloadRequest.inputMedias.forEach { inputMedia -> fun handleInputStream(inputStream: InputStream) { createMediaTempFile().apply { if (inputMedia.encryption != null) { @@ -210,9 +226,9 @@ class DownloadManagerReceiver : BroadcastReceiver() { downloadedMedias } - private suspend fun downloadRemoteMedia(pendingDownloadObject: PendingDownload, downloadedMedias: Map, downloadRequest: DownloadRequest) { - downloadRequest.getInputMedias().first().let { inputMedia -> - val mediaType = downloadRequest.getInputType(0)!! + private suspend fun downloadRemoteMedia(pendingDownloadObject: PendingDownload, downloadedMedias: Map, downloadRequest: DownloadRequest) { + downloadRequest.inputMedias.first().let { inputMedia -> + val mediaType = inputMedia.type val media = downloadedMedias[inputMedia]!! if (!downloadRequest.isDashPlaylist) { @@ -231,13 +247,15 @@ class DownloadManagerReceiver : BroadcastReceiver() { baseUrlNode.textContent = "${RemoteMediaResolver.CF_ST_CDN_D}$baseUrl" } - val dashOptions = downloadRequest.getDashOptions()!! + val dashOptions = downloadRequest.dashOptions!! val dashPlaylistFile = renameFromFileType(media.file, FileType.MPD) val xmlData = dashPlaylistFile.outputStream() TransformerFactory.newInstance().newTransformer().transform(DOMSource(playlistXml), StreamResult(xmlData)) - longToast(translation.format("download_toast", "path" to dashPlaylistFile.nameWithoutExtension)) + translation.format("download_toast", "path" to dashPlaylistFile.nameWithoutExtension).let { + runCatching { callback.onProgress(it) }.onFailure { fallbackToast(it) } + } val outputFile = File.createTempFile("dash", ".mp4") runCatching { MediaDownloaderHelper.downloadDashChapterFile( @@ -246,10 +264,12 @@ class DownloadManagerReceiver : BroadcastReceiver() { startTime = dashOptions.offsetTime, duration = dashOptions.duration) saveMediaToGallery(outputFile, pendingDownloadObject) - }.onFailure { + }.onFailure { exception -> if (coroutineContext.job.isCancelled) return@onFailure - Logger.error(it) - longToast(translation.format("failed_processing_toast", "error" to it.toString())) + Logger.error(exception) + translation.format("failed_processing_toast", "error" to exception.toString()).let { + runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) } + } pendingDownloadObject.downloadStage = DownloadStage.FAILED } @@ -265,28 +285,25 @@ class DownloadManagerReceiver : BroadcastReceiver() { return newFile } - override fun onReceive(context: Context, intent: Intent) { - if (intent.action != DOWNLOAD_ACTION) return - this.context = context - Logger.debug("onReceive download") + fun onReceive(intent: Intent) { + CoroutineScope(Dispatchers.IO).launch { + val downloadMetadata = gson.fromJson(intent.getStringExtra(DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java) + val downloadRequest = gson.fromJson(intent.getStringExtra(DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java) - SharedContext.ensureInitialized(context) - - val downloadRequest = DownloadRequest.fromBundle(intent.extras!!) - - SharedContext.downloadTaskManager.canDownloadMedia(downloadRequest.getUniqueHash())?.let { downloadStage -> - shortToast( + SharedContext.downloadTaskManager.canDownloadMedia(downloadMetadata.mediaIdentifier)?.let { downloadStage -> translation[if (downloadStage.isFinalStage) { "already_downloaded_toast" } else { "already_queued_toast" - }] - ) - return - } + }].let { + runCatching { callback.onFailure(it, null) }.onFailure { fallbackToast(it) } + } + return@launch + } - CoroutineScope(Dispatchers.IO).launch { - val pendingDownloadObject = PendingDownload.fromBundle(intent.extras!!) + val pendingDownloadObject = PendingDownload( + metadata = downloadMetadata + ) SharedContext.downloadTaskManager.addTask(pendingDownloadObject) pendingDownloadObject.apply { @@ -330,7 +347,9 @@ class DownloadManagerReceiver : BroadcastReceiver() { val renamedOverlayMedia = renameFromFileType(overlayMedia.file, overlayMedia.fileType) val mergedOverlay: File = File.createTempFile("merged", "." + media.fileType.fileExtension) runCatching { - longToast(translation.format("download_toast", "path" to media.file.nameWithoutExtension)) + translation.format("download_toast", "path" to media.file.nameWithoutExtension).let { + runCatching { callback.onProgress(it) }.onFailure { fallbackToast(it) } + } pendingDownloadObject.downloadStage = DownloadStage.MERGING MediaDownloaderHelper.mergeOverlayFile( @@ -340,10 +359,12 @@ class DownloadManagerReceiver : BroadcastReceiver() { ) saveMediaToGallery(mergedOverlay, pendingDownloadObject) - }.onFailure { + }.onFailure { exception -> if (coroutineContext.job.isCancelled) return@onFailure - Logger.error(it) - longToast(translation.format("failed_processing_toast", "error" to it.toString())) + Logger.error(exception) + translation.format("failed_processing_toast", "error" to exception.toString()).let { + runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) } + } pendingDownloadObject.downloadStage = DownloadStage.MERGE_FAILED } @@ -354,10 +375,12 @@ class DownloadManagerReceiver : BroadcastReceiver() { } downloadRemoteMedia(pendingDownloadObject, downloadedMedias, downloadRequest) - }.onFailure { + }.onFailure { exception -> pendingDownloadObject.downloadStage = DownloadStage.FAILED - Logger.error(it) - longToast(translation["failed_generic_toast"]) + Logger.error(exception) + translation["failed_generic_toast"].let { + runCatching { callback.onFailure(it, exception.message) }.onFailure { fallbackToast(it) } + } } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt index cfc84636..952bbaea 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt @@ -3,6 +3,7 @@ package me.rhunk.snapenhance.download import android.annotation.SuppressLint import android.content.Context import android.database.sqlite.SQLiteDatabase +import me.rhunk.snapenhance.download.data.DownloadMetadata import me.rhunk.snapenhance.download.data.PendingDownload import me.rhunk.snapenhance.download.enums.DownloadStage import me.rhunk.snapenhance.ui.download.MediaFilter @@ -35,42 +36,42 @@ class DownloadTaskManager { fun addTask(task: PendingDownload): Int { taskDatabase.execSQL("INSERT INTO tasks (hash, outputPath, outputFile, mediaDisplayType, mediaDisplaySource, iconUrl, downloadStage) VALUES (?, ?, ?, ?, ?, ?, ?)", arrayOf( - task.uniqueHash, - task.outputPath, + task.metadata.mediaIdentifier, + task.metadata.outputPath, task.outputFile, - task.mediaDisplayType, - task.mediaDisplaySource, - task.iconUrl, + task.metadata.mediaDisplayType, + task.metadata.mediaDisplaySource, + task.metadata.iconUrl, task.downloadStage.name ) ) - task.id = taskDatabase.rawQuery("SELECT last_insert_rowid()", null).use { + task.downloadId = taskDatabase.rawQuery("SELECT last_insert_rowid()", null).use { it.moveToFirst() it.getInt(0) } - pendingTasks[task.id] = task - return task.id + pendingTasks[task.downloadId] = task + return task.downloadId } fun updateTask(task: PendingDownload) { taskDatabase.execSQL("UPDATE tasks SET hash = ?, outputPath = ?, outputFile = ?, mediaDisplayType = ?, mediaDisplaySource = ?, iconUrl = ?, downloadStage = ? WHERE id = ?", arrayOf( - task.uniqueHash, - task.outputPath, + task.metadata.mediaIdentifier, + task.metadata.outputPath, task.outputFile, - task.mediaDisplayType, - task.mediaDisplaySource, - task.iconUrl, + task.metadata.mediaDisplayType, + task.metadata.mediaDisplaySource, + task.metadata.iconUrl, task.downloadStage.name, - task.id + task.downloadId ) ) //if the task is no longer active, move it to the cached tasks if (task.isJobActive()) { - pendingTasks[task.id] = task + pendingTasks[task.downloadId] = task } else { - pendingTasks.remove(task.id) - cachedTasks[task.id] = task + pendingTasks.remove(task.downloadId) + cachedTasks[task.downloadId] = task } } @@ -107,20 +108,20 @@ class DownloadTaskManager { } fun removeTask(task: PendingDownload) { - removeTask(task.id) + removeTask(task.downloadId) } fun queryAllTasks(filter: MediaFilter): Map { val isPendingFilter = filter == MediaFilter.PENDING val tasks = mutableMapOf() - tasks.putAll(pendingTasks.filter { isPendingFilter || filter.matches(it.value.mediaDisplayType) }) + tasks.putAll(pendingTasks.filter { isPendingFilter || filter.matches(it.value.metadata.mediaDisplayType) }) if (isPendingFilter) { return tasks.toSortedMap(reverseOrder()) } tasks.putAll(queryTasks( - from = tasks.values.lastOrNull()?.id ?: Int.MAX_VALUE, + from = tasks.values.lastOrNull()?.downloadId ?: Int.MAX_VALUE, amount = 30, filter = filter )) @@ -147,13 +148,15 @@ class DownloadTaskManager { while (cursor.moveToNext()) { val task = PendingDownload( - id = cursor.getInt(cursor.getColumnIndex("id")), + downloadId = cursor.getInt(cursor.getColumnIndex("id")), outputFile = cursor.getString(cursor.getColumnIndex("outputFile")), - outputPath = cursor.getString(cursor.getColumnIndex("outputPath")), - mediaDisplayType = cursor.getString(cursor.getColumnIndex("mediaDisplayType")), - mediaDisplaySource = cursor.getString(cursor.getColumnIndex("mediaDisplaySource")), - uniqueHash = cursor.getString(cursor.getColumnIndex("hash")), - iconUrl = cursor.getString(cursor.getColumnIndex("iconUrl")) + metadata = DownloadMetadata( + outputPath = cursor.getString(cursor.getColumnIndex("outputPath")), + mediaIdentifier = cursor.getString(cursor.getColumnIndex("hash")), + mediaDisplayType = cursor.getString(cursor.getColumnIndex("mediaDisplayType")), + mediaDisplaySource = cursor.getString(cursor.getColumnIndex("mediaDisplaySource")), + iconUrl = cursor.getString(cursor.getColumnIndex("iconUrl")) + ) ).apply { downloadStage = DownloadStage.valueOf(cursor.getString(cursor.getColumnIndex("downloadStage"))) //if downloadStage is not saved, it means the app was killed before the download was finished @@ -161,7 +164,7 @@ class DownloadTaskManager { downloadStage = DownloadStage.FAILED } } - result[task.id] = task + result[task.downloadId] = task } cursor.close() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt new file mode 100644 index 00000000..e66df166 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt @@ -0,0 +1,9 @@ +package me.rhunk.snapenhance.download.data + +data class DownloadMetadata( + val mediaIdentifier: String?, + val outputPath: String, + val mediaDisplaySource: String?, + val mediaDisplayType: String?, + val iconUrl: String? +) \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt index b68989e8..653dba45 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt @@ -1,6 +1,5 @@ package me.rhunk.snapenhance.download.data -import android.os.Bundle import me.rhunk.snapenhance.download.enums.DownloadMediaType @@ -12,64 +11,12 @@ data class InputMedia( ) class DownloadRequest( - private val outputPath: String = "", - private val inputMedias: Array, - private val inputTypes: Array, - private val mediaEncryption: Map = emptyMap(), + val inputMedias: Array, + val dashOptions: DashOptions? = null, private val flags: Int = 0, - private val dashOptions: Map? = null, - private val mediaDisplaySource: String? = null, - private val mediaDisplayType: String? = null, - private val uniqueHash: String? = null ) { - companion object { - fun fromBundle(bundle: Bundle): DownloadRequest { - return DownloadRequest( - outputPath = bundle.getString("outputPath")!!, - mediaDisplaySource = bundle.getString("mediaDisplaySource"), - mediaDisplayType = bundle.getString("mediaDisplayType"), - inputMedias = bundle.getStringArray("inputMedias")!!, - inputTypes = bundle.getStringArray("inputTypes")!!, - mediaEncryption = bundle.getStringArray("mediaEncryption")?.associate { entry -> - entry.split("|").let { - it[0] to MediaEncryptionKeyPair(it[1], it[2]) - } - } ?: emptyMap(), - dashOptions = bundle.getBundle("dashOptions")?.let { options -> - options.keySet().associateWith { key -> - options.getString(key) - } - }, - flags = bundle.getInt("flags", 0), - uniqueHash = bundle.getString("uniqueHash") - ) - } - } - - fun toBundle(): Bundle { - return Bundle().apply { - putString("outputPath", outputPath) - putString("mediaDisplaySource", mediaDisplaySource) - putString("mediaDisplayType", mediaDisplayType) - putStringArray("inputMedias", inputMedias) - putStringArray("inputTypes", inputTypes) - putStringArray("mediaEncryption", mediaEncryption.map { entry -> - "${entry.key}|${entry.value.key}|${entry.value.iv}" - }.toTypedArray()) - putBundle("dashOptions", dashOptions?.let { bundle -> - Bundle().apply { - bundle.forEach { (key, value) -> - putString(key, value) - } - } - }) - putInt("flags", flags) - putString("uniqueHash", uniqueHash) - } - } - object Flags { - const val SHOULD_MERGE_OVERLAY = 1 + const val MERGE_OVERLAY = 1 const val IS_DASH_PLAYLIST = 2 } @@ -77,30 +24,5 @@ class DownloadRequest( get() = flags and Flags.IS_DASH_PLAYLIST != 0 val shouldMergeOverlay: Boolean - get() = flags and Flags.SHOULD_MERGE_OVERLAY != 0 - - fun getDashOptions(): DashOptions? { - return dashOptions?.let { - DashOptions( - offsetTime = it["offsetTime"]?.toLong() ?: 0, - duration = it["duration"]?.toLong() - ) - } - } - - fun getInputMedias(): List { - return inputMedias.mapIndexed { index, uri -> - InputMedia( - content = uri, - type = DownloadMediaType.valueOf(inputTypes[index]), - encryption = mediaEncryption.getOrDefault(uri, null) - ) - } - } - - fun getInputType(index: Int): DownloadMediaType? { - return inputTypes.getOrNull(index)?.let { DownloadMediaType.valueOf(it) } - } - - fun getUniqueHash() = uniqueHash + get() = flags and Flags.MERGE_OVERLAY != 0 } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt index c11d083e..7960661e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt @@ -1,33 +1,16 @@ package me.rhunk.snapenhance.download.data -import android.os.Bundle import kotlinx.coroutines.Job import me.rhunk.snapenhance.SharedContext import me.rhunk.snapenhance.download.enums.DownloadStage data class PendingDownload( + var downloadId: Int = 0, var outputFile: String? = null, var job: Job? = null, - var id: Int = 0, - val outputPath: String, - val mediaDisplayType: String?, - val mediaDisplaySource: String?, - val iconUrl: String?, - val uniqueHash: String? + val metadata : DownloadMetadata ) { - companion object { - fun fromBundle(bundle: Bundle): PendingDownload { - return PendingDownload( - outputPath = bundle.getString("outputPath")!!, - mediaDisplayType = bundle.getString("mediaDisplayType"), - mediaDisplaySource = bundle.getString("mediaDisplaySource"), - iconUrl = bundle.getString("iconUrl"), - uniqueHash = bundle.getString("uniqueHash") - ) - } - } - var changeListener = { _: DownloadStage, _: DownloadStage -> } private var _stage: DownloadStage = DownloadStage.PENDING var downloadStage: DownloadStage diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt new file mode 100644 index 00000000..a1ffc26e --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt @@ -0,0 +1,5 @@ +package me.rhunk.snapenhance.download.data + +enum class SplitMediaAssetType { + ORIGINAL, OVERLAY +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt index 3603ff2b..f03bdc0d 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.features -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.types.BridgeFileType import java.io.BufferedReader import java.io.ByteArrayInputStream import java.io.InputStreamReader diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt index ab632492..df9fa6e4 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt @@ -28,4 +28,8 @@ abstract class Feature( * called on a dedicated thread when the Snapchat Activity is created */ open fun asyncOnActivityCreate() {} + + protected fun findClass(name: String): Class<*> { + return context.androidContext.classLoader.loadClass(name) + } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt index 59a8c529..7e6996e3 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt @@ -14,6 +14,7 @@ import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.ui.ViewAppearanceHelper import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONArray @@ -61,7 +62,7 @@ class AutoUpdater : Feature("AutoUpdater", loadParams = FeatureLoadParams.ACTIVI val downloadEndpoint = latestRelease.getJSONArray("assets").getJSONObject(0).getString("browser_download_url") context.runOnUiThread { - AlertDialog.Builder(context.mainActivity) + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) .setTitle(context.translation["auto_updater.dialog_title"]) .setMessage( context.translation.format("auto_updater.dialog_message", diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt index 606f6065..bbb6d08e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt @@ -1,5 +1,6 @@ package me.rhunk.snapenhance.features.impl +import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.features.Feature @@ -13,9 +14,9 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C lateinit var conversationManager: Any var openedConversationUUID: SnapUUID? = null - var lastOpenedConversationUUID: SnapUUID? = null var lastFetchConversationUserUUID: SnapUUID? = null var lastFetchConversationUUID: SnapUUID? = null + var lastFetchGroupConversationUUID: SnapUUID? = null var lastFocusedMessageId: Long = -1 override fun init() { @@ -25,6 +26,17 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } override fun onActivityCreate() { + context.mappings.getMappedObjectNullable("FriendsFeedEventDispatcher").let { it as? Map<*, *> }?.let { mappings -> + findClass(mappings["class"].toString()).hook("onItemLongPress", HookStage.BEFORE) { param -> + val viewItemContainer = param.arg(0) + val viewItem = viewItemContainer.getObjectField(mappings["viewModelField"].toString()).toString() + val conversationId = viewItem.substringAfter("conversationId: ").substring(0, 36).also { + if (it.startsWith("null")) return@hook + } + lastFetchGroupConversationUUID = SnapUUID.fromString(conversationId) + } + } + context.mappings.getMappedClass("callbacks", "GetOneOnOneConversationIdsCallback").hook("onSuccess", HookStage.BEFORE) { param -> val userIdToConversation = (param.arg>(0)) .takeIf { it.isNotEmpty() } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt index 8cff13f0..1b3f4f7a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.features.impl.downloader -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.features.BridgeFileFeature import me.rhunk.snapenhance.features.FeatureLoadParams diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt index c44d5fb2..df5308cc 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -1,6 +1,5 @@ package me.rhunk.snapenhance.features.impl.downloader -import android.app.AlertDialog import android.content.DialogInterface import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -9,10 +8,13 @@ import android.widget.ImageView import com.arthenica.ffmpegkit.FFmpegKit import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.Constants.ARROYO_URL_KEY_PROTO_PATH +import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.Logger.xposedLog +import me.rhunk.snapenhance.bridge.DownloadCallback import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.FileType +import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.data.wrapper.impl.media.MediaInfo import me.rhunk.snapenhance.data.wrapper.impl.media.dash.LongformVideoPlaylistItem import me.rhunk.snapenhance.data.wrapper.impl.media.dash.SnapPlaylistItem @@ -20,6 +22,9 @@ import me.rhunk.snapenhance.data.wrapper.impl.media.opera.Layer import me.rhunk.snapenhance.data.wrapper.impl.media.opera.ParamMap import me.rhunk.snapenhance.database.objects.FriendInfo import me.rhunk.snapenhance.download.DownloadManagerClient +import me.rhunk.snapenhance.download.data.DownloadMetadata +import me.rhunk.snapenhance.download.data.InputMedia +import me.rhunk.snapenhance.download.data.SplitMediaAssetType import me.rhunk.snapenhance.download.data.toKeyPair import me.rhunk.snapenhance.download.enums.DownloadMediaType import me.rhunk.snapenhance.features.Feature @@ -29,6 +34,7 @@ import me.rhunk.snapenhance.features.impl.spying.MessageLogger import me.rhunk.snapenhance.hook.HookAdapter import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.ui.download.MediaFilter import me.rhunk.snapenhance.util.download.RemoteMediaResolver import me.rhunk.snapenhance.util.getObjectField @@ -36,20 +42,17 @@ import me.rhunk.snapenhance.util.protobuf.ProtoReader import me.rhunk.snapenhance.util.snap.BitmojiSelfie import me.rhunk.snapenhance.util.snap.EncryptionHelper import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper -import me.rhunk.snapenhance.util.snap.MediaType import me.rhunk.snapenhance.util.snap.PreviewUtils -import java.io.File import java.nio.file.Paths import java.text.SimpleDateFormat import java.util.Locale import kotlin.coroutines.suspendCoroutine import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi -import kotlin.io.path.inputStream @OptIn(ExperimentalEncodingApi::class) class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - private var lastSeenMediaInfoMap: MutableMap? = null + private var lastSeenMediaInfoMap: MutableMap? = null private var lastSeenMapParams: ParamMap? = null private val isFFmpegPresent by lazy { runCatching { FFmpegKit.execute("-version") }.isSuccess @@ -70,22 +73,47 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam BitmojiSelfie.getBitmojiSelfie(it.bitmojiSelfieId!!, it.bitmojiAvatarId!!, BitmojiSelfie.BitmojiSelfieType.THREE_D) } - val outputPath = File( - context.config.string(ConfigProperty.SAVE_FOLDER), - createNewFilePath(generatedHash, mediaDisplayType, pathSuffix) - ).absolutePath + val downloadLogging = context.config.options(ConfigProperty.DOWNLOAD_LOGGING) + if (downloadLogging["started"] == true) { + context.shortToast(context.translation["download_processor.download_started_toast"]) + } + + val outputPath = createNewFilePath(generatedHash, mediaDisplayType, pathSuffix) return DownloadManagerClient( context = context, - mediaDisplaySource = mediaDisplaySource, - mediaDisplayType = mediaDisplayType, - iconUrl = iconUrl, - uniqueHash = - // If duplicate is allowed, we don't need to pass the hash - if (context.config.options(ConfigProperty.DOWNLOAD_OPTIONS)["allow_duplicate"] == false) { - generatedHash - } else null, - outputPath = outputPath + metadata = DownloadMetadata( + mediaIdentifier = if (context.config.options(ConfigProperty.DOWNLOAD_OPTIONS)["allow_duplicate"] == false) { + generatedHash + } else null, + mediaDisplaySource = mediaDisplaySource, + mediaDisplayType = mediaDisplayType, + iconUrl = iconUrl, + outputPath = outputPath + ), + callback = object: DownloadCallback.Stub() { + override fun onSuccess(outputFile: String) { + if (downloadLogging["success"] != true) return + Logger.debug("onSuccess: outputFile=$outputFile") + context.shortToast(context.translation.format("download_processor.saved_toast", "path" to outputFile.split("/").takeLast(2).joinToString("/"))) + } + + override fun onProgress(message: String) { + if (downloadLogging["progress"] != true) return + Logger.debug("onProgress: message=$message") + context.shortToast(message) + } + + override fun onFailure(message: String, throwable: String?) { + if (downloadLogging["failure"] != true) return + Logger.debug("onFailure: message=$message, throwable=$throwable") + throwable?.let { + context.longToast((message + it.takeIf { it.isNotEmpty() }.orEmpty())) + return + } + context.shortToast(message) + } + } ) } @@ -162,28 +190,31 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } } - private fun downloadOperaMedia(downloadManagerClient: DownloadManagerClient, mediaInfoMap: Map) { + private fun downloadOperaMedia(downloadManagerClient: DownloadManagerClient, mediaInfoMap: Map) { if (mediaInfoMap.isEmpty()) return - val originalMediaInfo = mediaInfoMap[MediaType.ORIGINAL]!! - val overlay = mediaInfoMap[MediaType.OVERLAY] - + val originalMediaInfo = mediaInfoMap[SplitMediaAssetType.ORIGINAL]!! val originalMediaInfoReference = handleLocalReferences(originalMediaInfo.uri) - val overlayReference = overlay?.let { handleLocalReferences(it.uri) } - overlay?.let { + mediaInfoMap[SplitMediaAssetType.OVERLAY]?.let { overlay -> + val overlayReference = handleLocalReferences(overlay.uri) + downloadManagerClient.downloadMediaWithOverlay( - originalMediaInfoReference, - overlayReference!!, - DownloadMediaType.fromUri(Uri.parse(originalMediaInfoReference)), - DownloadMediaType.fromUri(Uri.parse(overlayReference)), - videoEncryption = originalMediaInfo.encryption?.toKeyPair(), - overlayEncryption = overlay.encryption?.toKeyPair() + original = InputMedia( + originalMediaInfoReference, + DownloadMediaType.fromUri(Uri.parse(originalMediaInfoReference)), + originalMediaInfo.encryption?.toKeyPair() + ), + overlay = InputMedia( + overlayReference, + DownloadMediaType.fromUri(Uri.parse(overlayReference)), + overlay.encryption?.toKeyPair() + ) ) return } - downloadManagerClient.downloadMedia( + downloadManagerClient.downloadSingleMedia( originalMediaInfoReference, DownloadMediaType.fromUri(Uri.parse(originalMediaInfoReference)), originalMediaInfo.encryption?.toKeyPair() @@ -199,7 +230,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam */ private fun handleOperaMedia( paramMap: ParamMap, - mediaInfoMap: Map, + mediaInfoMap: Map, forceDownload: Boolean ) { //messages @@ -229,14 +260,32 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } //private stories - paramMap["PLAYLIST_V2_GROUP"]?.toString()?.takeIf { - it.contains("storyUserId=") && (forceDownload || canAutoDownload("friend_stories")) + paramMap["PLAYLIST_V2_GROUP"]?.takeIf { + forceDownload || canAutoDownload("friend_stories") }?.let { playlistGroup -> - val storyUserId = (playlistGroup.indexOf("storyUserId=") + 12).let { - playlistGroup.substring(it, playlistGroup.indexOf(",", it)) + val playlistGroupString = playlistGroup.toString() + + val storyUserId = if (playlistGroupString.contains("storyUserId=")) { + (playlistGroupString.indexOf("storyUserId=") + 12).let { + playlistGroupString.substring(it, playlistGroupString.indexOf(",", it)) + } + } else { + //story replies + val arroyoMessageId = playlistGroup::class.java.methods.firstOrNull { it.name == "getId" } + ?.invoke(playlistGroup)?.toString() + ?.split(":")?.getOrNull(2) ?: return@let + + val conversationMessage = context.database.getConversationMessageFromId(arroyoMessageId.toLong()) ?: return@let + val conversationParticipants = context.database.getConversationParticipants(conversationMessage.client_conversation_id.toString()) ?: return@let + + conversationParticipants.firstOrNull { it != conversationMessage.sender_id } } - val author = context.database.getFriendInfo(if (storyUserId == "null") context.database.getMyUserId()!! else storyUserId) ?: return + val author = context.database.getFriendInfo( + if (storyUserId == null || storyUserId == "null") + context.database.getMyUserId()!! + else storyUserId + ) ?: throw Exception("Friend not found in database") val authorName = author.usernameForSorting!! downloadOperaMedia(provideClientDownloadManager( @@ -333,7 +382,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } override fun asyncOnActivityCreate() { - val operaViewerControllerClass: Class<*> = context.mappings.getMappedClass("OperaPageViewController", "Class") + val operaViewerControllerClass: Class<*> = context.mappings.getMappedClass("OperaPageViewController", "class") val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> @@ -347,13 +396,13 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) return@onOperaViewStateCallback - val mediaInfoMap = mutableMapOf() + val mediaInfoMap = mutableMapOf() val isVideo = mediaParamMap.containsKey("video_media_info_list") - mediaInfoMap[MediaType.ORIGINAL] = MediaInfo( + mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo( (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!! ) if (canMergeOverlay() && mediaParamMap.containsKey("overlay_image_media_info")) { - mediaInfoMap[MediaType.OVERLAY] = + mediaInfoMap[SplitMediaAssetType.OVERLAY] = MediaInfo(mediaParamMap["overlay_image_media_info"]!!) } lastSeenMapParams = mediaParamMap @@ -371,7 +420,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } } - arrayOf("onDisplayStateChange", "onDisplayStateChange2").forEach { methodName -> + arrayOf("onDisplayStateChange", "onDisplayStateChangeGesture").forEach { methodName -> Hooker.hook( operaViewerControllerClass, context.mappings.getMappedValue("OperaPageViewController", methodName), @@ -380,20 +429,12 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } } - /** - * Called when a message is focused in chat - */ - //TODO: use snapchat classes instead of database (when content is deleted) - fun onMessageActionMenu(isPreviewMode: Boolean) { - //check if the message was focused in a conversation - val messaging = context.feature(Messaging::class) + fun downloadMessageId(messageId: Long, isPreview: Boolean = false) { val messageLogger = context.feature(MessageLogger::class) - - if (messaging.openedConversationUUID == null) return - val message = context.database.getConversationMessageFromId(messaging.lastFocusedMessageId) ?: return + val message = context.database.getConversationMessageFromId(messageId) ?: throw Exception("Message not found in database") //get the message author - val friendInfo: FriendInfo = context.database.getFriendInfo(message.sender_id!!)!! + val friendInfo: FriendInfo = context.database.getFriendInfo(message.sender_id!!) ?: throw Exception("Friend not found in database") val authorName = friendInfo.usernameForSorting!! var messageContent = message.message_content!! @@ -425,10 +466,12 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } } + val translations = context.translation.getCategory("download_processor") + if (contentType != ContentType.NOTE && contentType != ContentType.SNAP && contentType != ContentType.EXTERNAL_MEDIA) { - context.shortToast("Unsupported content type $contentType") + context.shortToast(translations["unsupported_content_type_toast"]) return } @@ -440,7 +483,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } runCatching { - if (!isPreviewMode) { + if (!isPreview) { val encryptionKeys = EncryptionHelper.getEncryptionKeys(contentType, messageReader, isArroyo = isArroyoMessage) provideClientDownloadManager( pathSuffix = authorName, @@ -448,7 +491,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam mediaDisplaySource = authorName, mediaDisplayType = MediaFilter.CHAT_MEDIA.mediaDisplayType, friendInfo = friendInfo - ).downloadMedia( + ).downloadSingleMedia( Base64.UrlSafe.encode(urlProto), DownloadMediaType.PROTO_MEDIA, encryption = encryptionKeys?.toKeyPair() @@ -456,18 +499,23 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam return } + if (EncryptionHelper.getEncryptionKeys(contentType, messageReader, isArroyo = isArroyoMessage) == null) { + context.shortToast(translations["failed_no_longer_available_toast"]) + return + } + val downloadedMediaList = MediaDownloaderHelper.downloadMediaFromReference(urlProto) { EncryptionHelper.decryptInputStream(it, contentType, messageReader, isArroyo = isArroyoMessage) } runCatching { - val originalMedia = downloadedMediaList[MediaType.ORIGINAL] ?: return - val overlay = downloadedMediaList[MediaType.OVERLAY] + val originalMedia = downloadedMediaList[SplitMediaAssetType.ORIGINAL] ?: return + val overlay = downloadedMediaList[SplitMediaAssetType.OVERLAY] var bitmap: Bitmap? = PreviewUtils.createPreview(originalMedia, isVideo = FileType.fromByteArray(originalMedia).isVideo) if (bitmap == null) { - context.shortToast("Failed to create preview") + context.shortToast(translations["failed_to_create_preview_toast"]) return } @@ -475,21 +523,29 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam bitmap = PreviewUtils.mergeBitmapOverlay(bitmap!!, BitmapFactory.decodeByteArray(it, 0, it.size)) } - with(AlertDialog.Builder(context.mainActivity)) { - setTitle("Preview") + with(ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)) { setView(ImageView(context).apply { setImageBitmap(bitmap) }) setPositiveButton("Close") { dialog: DialogInterface, _: Int -> dialog.dismiss() } - this@MediaDownloader.context.runOnUiThread { show() } + this@MediaDownloader.context.runOnUiThread { show()} } }.onFailure { - context.shortToast("Failed to create preview: $it") + context.shortToast(translations["failed_to_create_preview_toast"]) xposedLog(it) } }.onFailure { - context.longToast("Failed to download " + it.message) + context.longToast(translations["failed_generic_toast"]) xposedLog(it) } } + + /** + * Called when a message is focused in chat + */ + fun onMessageActionMenu(isPreviewMode: Boolean) { + val messaging = context.feature(Messaging::class) + if (messaging.openedConversationUUID == null) return + downloadMessageId(messaging.lastFocusedMessageId, isPreviewMode) + } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt index bded13b7..8cbc513a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt @@ -12,6 +12,7 @@ import android.widget.EditText import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.ui.ViewAppearanceHelper //TODO: fingerprint unlock class AppPasscode : Feature("App Passcode", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { @@ -32,7 +33,7 @@ class AppPasscode : Feature("App Passcode", loadParams = FeatureLoadParams.ACTIV val mainActivity = context.mainActivity!! setActivityVisibility(false) - val prompt = AlertDialog.Builder(mainActivity) + val prompt = ViewAppearanceHelper.newAlertDialogBuilder(mainActivity) val createPrompt = { val alertDialog = prompt.create() val textView = EditText(mainActivity) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt new file mode 100644 index 00000000..c8da19ee --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt @@ -0,0 +1,47 @@ +package me.rhunk.snapenhance.features.impl.experiments + +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker + +class DeviceSpooferHook: Feature("device_spoofer", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { + //FINGERPRINT + if(getFingerprint().isNotEmpty()) { + val fingerprintClass = android.os.Build::class.java + Hooker.hook(fingerprintClass, "FINGERPRINT", HookStage.BEFORE) { hookAdapter -> + hookAdapter.setResult(getFingerprint()) + } + Hooker.hook(fingerprintClass, "deriveFingerprint", HookStage.BEFORE) { hookAdapter -> + hookAdapter.setResult(getFingerprint()) + } + } + else { + Logger.xposedLog("Fingerprint is null, not spoofing") + } + + //ANDROID ID + if(getAndroidId().isNotEmpty()) { + val settingsSecureClass = android.provider.Settings.Secure::class.java + Hooker.hook(settingsSecureClass, "getString", HookStage.BEFORE) { hookAdapter -> + if(hookAdapter.args()[1] == "android_id") { + hookAdapter.setResult(getAndroidId()) + } + } + } + else { + Logger.xposedLog("Android ID is null, not spoofing") + } + } + private fun getFingerprint():String { + return context.config.string(ConfigProperty.FINGERPRINT) + } + + private fun getAndroidId():String { + return context.config.string(ConfigProperty.ANDROID_ID) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt index cc22887b..27074562 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt @@ -8,9 +8,11 @@ import me.rhunk.snapenhance.hook.Hooker class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { + val bcrypt = context.mappings.getMappedMap("BCrypt") + Hooker.hook( - context.mappings.getMappedClass("BCryptClass"), - context.mappings.getMappedValue("BCryptClassHashMethod"), + context.androidContext.classLoader.loadClass(bcrypt["class"].toString()), + bcrypt["hashMethod"].toString(), HookStage.BEFORE, { context.config.bool(ConfigProperty.MEO_PASSCODE_BYPASS) }, ) { param -> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt index 8c35d003..9c88c98f 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt @@ -1,36 +1,39 @@ package me.rhunk.snapenhance.features.impl.privacy +import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.NotificationType import me.rhunk.snapenhance.data.wrapper.impl.MessageContent import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.hook.hook -class PreventMessageSending : Feature("Send message override", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { +class PreventMessageSending : Feature("Prevent message sending", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { override fun asyncOnActivityCreate() { - Hooker.hook( - context.classCache.conversationManager, - "sendMessageWithContent", - HookStage.BEFORE - ) { param -> - val message = MessageContent(param.arg(1)) - val contentType = message.contentType - - if (context.config.bool(ConfigProperty.PREVENT_STATUS_NOTIFICATIONS)) { - if (contentType == ContentType.STATUS_SAVE_TO_CAMERA_ROLL || - contentType == ContentType.STATUS_CALL_MISSED_AUDIO || - contentType == ContentType.STATUS_CALL_MISSED_VIDEO) { - param.setResult(null) - } + context.classCache.conversationManager.hook("updateMessage", HookStage.BEFORE) { param -> + val messageUpdate = param.arg(2).toString(); + val options by lazy { + context.config.options(ConfigProperty.PREVENT_SENDING_MESSAGES) + } + if (messageUpdate == "SCREENSHOT" && options["chat_screenshot"] == true) { + param.setResult(null) } - if (context.config.bool(ConfigProperty.PREVENT_SCREENSHOT_NOTIFICATIONS)) { - if (contentType == ContentType.STATUS_CONVERSATION_CAPTURE_SCREENSHOT || - contentType == ContentType.STATUS_CONVERSATION_CAPTURE_RECORD) { - param.setResult(null) - } + if (messageUpdate == "SCREEN_RECORD" && options["chat_screen_record"] == true) { + param.setResult(null) + } + } + + context.classCache.conversationManager.hook("sendMessageWithContent", HookStage.BEFORE) { param -> + val message = MessageContent(param.arg(1)) + val contentType = message.contentType + val associatedType = NotificationType.fromContentType(contentType) ?: return@hook + val options = context.config.options(ConfigProperty.PREVENT_SENDING_MESSAGES) + + if (options[associatedType.key] == true) { + Logger.debug("Preventing message sending for $associatedType") + param.setResult(null) } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt index 43330b68..1e19d3c0 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt @@ -1,5 +1,6 @@ package me.rhunk.snapenhance.features.impl.spying +import android.os.DeadObjectException import com.google.gson.JsonObject import com.google.gson.JsonParser import me.rhunk.snapenhance.Logger @@ -11,6 +12,7 @@ import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker +import java.util.concurrent.Executors import kotlin.time.ExperimentalTime import kotlin.time.measureTime @@ -23,6 +25,8 @@ class MessageLogger : Feature("MessageLogger", const val PREFETCH_FEED_COUNT = 20 } + private val threadPool = Executors.newFixedThreadPool(10) + //two level of cache to avoid querying the database private val fetchedMessages = mutableListOf() private val deletedMessageCache = mutableMapOf() @@ -59,7 +63,7 @@ class MessageLogger : Feature("MessageLogger", measureTime { context.database.getFriendFeed(PREFETCH_FEED_COUNT).forEach { friendFeedInfo -> - fetchedMessages.addAll(context.bridgeClient.getLoggedMessageIds(friendFeedInfo.key!!, PREFETCH_MESSAGE_COUNT)) + fetchedMessages.addAll(context.bridgeClient.getLoggedMessageIds(friendFeedInfo.key!!, PREFETCH_MESSAGE_COUNT).toList()) } }.also { Logger.debug("Loaded ${fetchedMessages.size} cached messages in $it") } } @@ -79,11 +83,13 @@ class MessageLogger : Feature("MessageLogger", if (fetchedMessages.contains(messageId)) return fetchedMessages.add(messageId) - context.executeAsync { - context.bridgeClient.getMessageLoggerMessage(conversationId, messageId)?.let { - return@executeAsync - } - context.bridgeClient.addMessageLoggerMessage(conversationId, messageId, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8)) + threadPool.execute { + try { + context.bridgeClient.getMessageLoggerMessage(conversationId, messageId)?.let { + return@execute + } + context.bridgeClient.addMessageLoggerMessage(conversationId, messageId, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8)) + } catch (ignored: DeadObjectException) {} } return diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt index 14e02500..8757f37d 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.features.impl.spying -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.features.BridgeFileFeature import me.rhunk.snapenhance.features.FeatureLoadParams diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt index 6ea7885b..c4875b14 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.features.impl.tweaks -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.features.BridgeFileFeature import me.rhunk.snapenhance.features.FeatureLoadParams diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt index 5851b41f..094c384e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt @@ -9,6 +9,7 @@ import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.util.protobuf.ProtoReader class GalleryMediaSendOverride : Feature("Gallery Media Send Override", loadParams = FeatureLoadParams.INIT_SYNC) { @@ -24,7 +25,7 @@ class GalleryMediaSendOverride : Feature("Gallery Media Send Override", loadPara if (messageProtoReader.readPath(3)?.getCount(3) != 1) { context.runOnUiThread { - AlertDialog.Builder(context.mainActivity!!) + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!) .setMessage("You can only send one media at a time") .setPositiveButton("OK", null) .show() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt new file mode 100644 index 00000000..d0e9b525 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt @@ -0,0 +1,24 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +import android.app.AlertDialog +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.hook +import java.lang.reflect.Modifier + +class GooglePlayServicesDialogs : Feature("Disable GMS Dialogs", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { + if (!context.config.bool(ConfigProperty.DISABLE_GOOGLE_PLAY_DIALOGS)) return + + findClass("com.google.android.gms.common.GoogleApiAvailability").methods + .first { Modifier.isStatic(it.modifiers) && it.returnType == AlertDialog::class.java }.let { method -> + method.hook(HookStage.BEFORE) { param -> + Logger.debug("GoogleApiAvailability.showErrorDialogFragment() called, returning null") + param.setResult(null) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt index bd98fa34..2bd58a6d 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt @@ -4,14 +4,15 @@ import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.hook.hook class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) { override fun init() { - val enumQualityLevel = context.mappings.getMappedClass("enums", "QualityLevel") + val enumQualityLevel = context.mappings.getMappedClass("EnumQualityLevel") + val mediaQualityLevelProvider = context.mappings.getMappedMap("MediaQualityLevelProvider") - Hooker.hook(context.mappings.getMappedClass("MediaQualityLevelProvider"), - context.mappings.getMappedValue("MediaQualityLevelProviderMethod"), + context.androidContext.classLoader.loadClass(mediaQualityLevelProvider["class"].toString()).hook( + mediaQualityLevelProvider["method"].toString(), HookStage.BEFORE, { context.config.bool(ConfigProperty.FORCE_MEDIA_SOURCE_QUALITY) } ) { param -> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt index 82041feb..b5c20ebc 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt @@ -19,21 +19,24 @@ import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.MediaReferenceType import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.download.data.SplitMediaAssetType import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.Messaging +import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.hook.hook import me.rhunk.snapenhance.util.CallbackBuilder +import me.rhunk.snapenhance.util.protobuf.ProtoReader import me.rhunk.snapenhance.util.snap.EncryptionHelper import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper -import me.rhunk.snapenhance.util.snap.MediaType import me.rhunk.snapenhance.util.snap.PreviewUtils -import me.rhunk.snapenhance.util.protobuf.ProtoReader class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) { companion object{ - const val ACTION_REPLY = "me.rhunk.snapenhance.action.REPLY" + const val ACTION_REPLY = "me.rhunk.snapenhance.action.notification.REPLY" + const val ACTION_DOWNLOAD = "me.rhunk.snapenhance.action.notification.DOWNLOAD" } private val notificationDataQueue = mutableMapOf() // messageId => notification @@ -54,10 +57,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN ) } - private val cancelAsUserMethod by lazy { - XposedHelpers.findMethodExact(NotificationManager::class.java, "cancelAsUser", String::class.java, Int::class.javaPrimitiveType, UserHandle::class.java) - } - private val fetchConversationWithMessagesMethod by lazy { context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessages"} } @@ -66,51 +65,69 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN context.androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } - private fun setNotificationText(notification: Notification, text: String) { + private fun setNotificationText(notification: Notification, conversationId: String) { + val messageText = StringBuilder().apply { + cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.forEach { + if (isNotEmpty()) append("\n") + append(it) + } + }.toString() + with(notification.extras) { - putString("android.text", text) - putString("android.bigText", text) + putString("android.text", messageText) + putString("android.bigText", messageText) + putParcelableArray("android.messages", messageText.split("\n").map { + Bundle().apply { + putBundle("extras", Bundle()) + putString("text", it) + putLong("time", System.currentTimeMillis()) + } + }.toTypedArray()) } } - private fun computeNotificationText(conversationId: String): String { - val messageBuilder = StringBuilder() - cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.forEach { - if (messageBuilder.isNotEmpty()) messageBuilder.append("\n") - messageBuilder.append(it) - } - return messageBuilder.toString() - } + private fun setupNotificationActionButtons(contentType: ContentType, conversationId: String, messageId: Long, notificationData: NotificationData) { + val betterNotifications = context.config.options(ConfigProperty.BETTER_NOTIFICATIONS) - private fun setupNotificationActionButtons(conversationId: String, notificationData: NotificationData) { val notificationBuilder = XposedHelpers.newInstance( Notification.Builder::class.java, context.androidContext, notificationData.notification ) as Notification.Builder - val chatReplyInput = RemoteInput.Builder("chat_reply_input") - .setLabel("Reply") - .build() + val actions = mutableListOf() + actions.addAll(notificationData.notification.actions) - val replyIntent = Intent() - .setClassName(Constants.SNAPCHAT_PACKAGE_NAME, broadcastReceiverClass.name) - .putExtra("conversation_id", conversationId) - .putExtra("notification_id", notificationData.id) - .setAction(ACTION_REPLY) - - val action = Notification.Action.Builder( - null, - "Reply", - PendingIntent.getBroadcast( + fun newAction(title: String, remoteAction: String, filter: (() -> Boolean), builder: (Notification.Action.Builder) -> Unit) { + if (!filter()) return + val intent = Intent().setClassName(Constants.SNAPCHAT_PACKAGE_NAME, broadcastReceiverClass.name) + .putExtra("conversation_id", conversationId) + .putExtra("notification_id", notificationData.id) + .putExtra("message_id", messageId) + .setAction(remoteAction) + val action = Notification.Action.Builder(null, title, PendingIntent.getBroadcast( context.androidContext, System.nanoTime().toInt(), - replyIntent, + intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - ) - ).addRemoteInput(chatReplyInput).build() + )).apply(builder).build() + actions.add(action) + } - notificationBuilder.setActions(action) + newAction("Reply", ACTION_REPLY, { + betterNotifications["reply_button"] == true && contentType == ContentType.CHAT + }) { + val chatReplyInput = RemoteInput.Builder("chat_reply_input") + .setLabel("Reply") + .build() + it.addRemoteInput(chatReplyInput) + } + + newAction("Download", ACTION_DOWNLOAD, { + betterNotifications["download_button"] == true && (contentType == ContentType.EXTERNAL_MEDIA || contentType == ContentType.SNAP) + }) {} + + notificationBuilder.setActions(*actions.toTypedArray()) notificationData.notification = notificationBuilder.build() } @@ -118,12 +135,14 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN Hooker.hook(broadcastReceiverClass, "onReceive", HookStage.BEFORE) { param -> val androidContext = param.arg(0) val intent = param.arg(1) - if (intent.action != ACTION_REPLY) return@hook - param.setResult(null) + val conversationId = intent.getStringExtra("conversation_id") ?: return@hook + val messageId = intent.getLongExtra("message_id", -1) + val notificationId = intent.getIntExtra("notification_id", -1) val notificationManager = androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val updateNotification: (Int, (Notification) -> Unit) -> Unit = { notificationId, notificationBuilder -> - notificationManager.activeNotifications.firstOrNull { it.id == notificationId }?.let { + + val updateNotification: (Int, (Notification) -> Unit) -> Unit = { id, notificationBuilder -> + notificationManager.activeNotifications.firstOrNull { it.id == id }?.let { notificationBuilder(it.notification) XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf( it.tag, it.id, it.notification, it.user @@ -131,22 +150,35 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN } } - val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input") - .toString() - val conversationId = intent.getStringExtra("conversation_id")!! - val notificationId = intent.getIntExtra("notification_id", -1) + when (intent.action) { + ACTION_REPLY -> { + val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input") + .toString() - context.database.getMyUserId()?.let { context.database.getFriendInfo(it) }?.let { myUser -> - cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input") + context.database.getMyUserId()?.let { context.database.getFriendInfo(it) }?.let { myUser -> + cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input") - updateNotification(notificationId) { notification -> - setNotificationText(notification, computeNotificationText(conversationId)) + updateNotification(notificationId) { notification -> + notification.flags = notification.flags or Notification.FLAG_ONLY_ALERT_ONCE + setNotificationText(notification, conversationId) + } + + context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = { + context.longToast("Failed to send message: $it") + }) + } } - - context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = { - context.longToast("Failed to send message: $it") - }) + ACTION_DOWNLOAD -> { + runCatching { + context.feature(MediaDownloader::class).downloadMessageId(messageId, isPreview = false) + }.onFailure { + context.longToast(it) + } + } + else -> return@hook } + + param.setResult(null) } } @@ -160,90 +192,80 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN )) } - notificationDataQueue.entries.onEach { (messageId, notificationData) -> - val snapMessage = messages.firstOrNull { message -> message.orderKey == messageId } ?: return - val senderUsername by lazy { - context.database.getFriendInfo(snapMessage.senderId.toString())?.let { - it.displayName ?: it.username - } - } - - val contentType = snapMessage.messageContent.contentType - val contentData = snapMessage.messageContent.content - - val formatUsername: (String) -> String = { "$senderUsername: $it" } - val notificationCache = cachedMessages.let { it.computeIfAbsent(conversationId) { mutableListOf() } } - val appendNotifications: () -> Unit = { setNotificationText(notificationData.notification, computeNotificationText(conversationId))} - - when (contentType) { - ContentType.NOTE -> { - notificationCache.add(formatUsername("sent audio note")) - appendNotifications() - } - ContentType.CHAT -> { - ProtoReader(contentData).getString(2, 1)?.trim()?.let { - notificationCache.add(formatUsername(it)) + synchronized(notificationDataQueue) { + notificationDataQueue.entries.onEach { (messageId, notificationData) -> + val snapMessage = messages.firstOrNull { message -> message.orderKey == messageId } ?: return + val senderUsername by lazy { + context.database.getFriendInfo(snapMessage.senderId.toString())?.let { + it.displayName ?: it.username } - appendNotifications() } - ContentType.SNAP, ContentType.EXTERNAL_MEDIA-> { - //serialize the message content into a json object - val serializedMessageContent = context.gson.toJsonTree(snapMessage.messageContent.instanceNonNull()).asJsonObject - val mediaReferences = serializedMessageContent["mRemoteMediaReferences"] - .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } - .flatten() - mediaReferences.forEach { media -> - val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() - val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) - runCatching { - val messageReader = ProtoReader(contentData) - val downloadedMediaList = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference) { - EncryptionHelper.decryptInputStream(it, contentType, messageReader, isArroyo = false) + val contentType = snapMessage.messageContent.contentType + val contentData = snapMessage.messageContent.content + + val formatUsername: (String) -> String = { "$senderUsername: $it" } + val notificationCache = cachedMessages.let { it.computeIfAbsent(conversationId) { mutableListOf() } } + val appendNotifications: () -> Unit = { setNotificationText(notificationData.notification, conversationId)} + + setupNotificationActionButtons(contentType, conversationId, snapMessage.messageDescriptor.messageId, notificationData) + + when (contentType) { + ContentType.NOTE -> { + notificationCache.add(formatUsername("sent audio note")) + appendNotifications() + } + ContentType.CHAT -> { + ProtoReader(contentData).getString(2, 1)?.trim()?.let { + notificationCache.add(formatUsername(it)) + } + appendNotifications() + } + ContentType.SNAP, ContentType.EXTERNAL_MEDIA-> { + //serialize the message content into a json object + val serializedMessageContent = context.gson.toJsonTree(snapMessage.messageContent.instanceNonNull()).asJsonObject + val mediaReferences = serializedMessageContent["mRemoteMediaReferences"] + .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } + .flatten() + + mediaReferences.forEach { media -> + val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() + val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) + runCatching { + val messageReader = ProtoReader(contentData) + val downloadedMediaList = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference) { + EncryptionHelper.decryptInputStream(it, contentType, messageReader, isArroyo = false) + } + + var bitmapPreview = PreviewUtils.createPreview(downloadedMediaList[SplitMediaAssetType.ORIGINAL]!!, mediaType.name.contains("VIDEO"))!! + + downloadedMediaList[SplitMediaAssetType.OVERLAY]?.let { + bitmapPreview = PreviewUtils.mergeBitmapOverlay(bitmapPreview, BitmapFactory.decodeByteArray(it, 0, it.size)) + } + + val notificationBuilder = XposedHelpers.newInstance( + Notification.Builder::class.java, + context.androidContext, + notificationData.notification + ) as Notification.Builder + notificationBuilder.setLargeIcon(bitmapPreview) + notificationBuilder.style = Notification.BigPictureStyle().bigPicture(bitmapPreview).bigLargeIcon(null as Bitmap?) + + sendNotificationData(notificationData.copy(notification = notificationBuilder.build()), true) + return@onEach + }.onFailure { + Logger.xposedLog("Failed to send preview notification", it) } - - var bitmapPreview = PreviewUtils.createPreview(downloadedMediaList[MediaType.ORIGINAL]!!, mediaType.name.contains("VIDEO"))!! - - downloadedMediaList[MediaType.OVERLAY]?.let { - bitmapPreview = PreviewUtils.mergeBitmapOverlay(bitmapPreview, BitmapFactory.decodeByteArray(it, 0, it.size)) - } - - val notificationBuilder = XposedHelpers.newInstance( - Notification.Builder::class.java, - context.androidContext, - notificationData.notification - ) as Notification.Builder - notificationBuilder.setLargeIcon(bitmapPreview) - notificationBuilder.style = Notification.BigPictureStyle().bigPicture(bitmapPreview).bigLargeIcon(null as Bitmap?) - - sendNotificationData(notificationData.copy(notification = notificationBuilder.build()), true) - return@onEach - }.onFailure { - Logger.xposedLog("Failed to send preview notification", it) } } + else -> { + notificationCache.add(formatUsername("sent $contentType")) + } } - else -> { - notificationCache.add(formatUsername("sent $contentType")) - } - } - if (contentType == ContentType.CHAT && context.config.options(ConfigProperty.BETTER_NOTIFICATIONS)["reply_button"] == true) { - setupNotificationActionButtons(conversationId, notificationData) - } - - sendNotificationData(notificationData, false) - }.clear() - } - - private fun shouldIgnoreNotification(type: String): Boolean { - val states = context.config.options(ConfigProperty.NOTIFICATION_BLACKLIST) - - states["snap"]?.let { if (type.endsWith("SNAP") && it) return true } - states["chat"]?.let { if (type.endsWith("CHAT") && it) return true } - states["typing"]?.let { if (type.endsWith("TYPING") && it) return true } - - return false + sendNotificationData(notificationData, false) + }.clear() + } } override fun init() { @@ -260,16 +282,16 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN val notificationType = extras.getString("notification_type") ?: return@hook val conversationId = extras.getString("conversation_id") ?: return@hook - if (shouldIgnoreNotification(notificationType)) { - param.setResult(null) - return@hook - } - if (context.config.options(ConfigProperty.BETTER_NOTIFICATIONS) - .filter { it.value }.none { notificationType.endsWith(it.key.uppercase())}) return@hook + .filter { it.value }.map { it.key.uppercase() }.none { + notificationType.contains(it) + }) return@hook val conversationManager: Any = context.feature(Messaging::class).conversationManager - notificationDataQueue[messageId.toLong()] = notificationData + + synchronized(notificationDataQueue) { + notificationDataQueue[messageId.toLong()] = notificationData + } val callback = CallbackBuilder(fetchConversationWithMessagesCallback) .override("onFetchConversationWithMessagesComplete") { callbackParam -> @@ -284,12 +306,32 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN param.setResult(null) } - Hooker.hook(cancelAsUserMethod, HookStage.BEFORE) { param -> + XposedHelpers.findMethodExact( + NotificationManager::class.java, + "cancelAsUser", String::class.java, + Int::class.javaPrimitiveType, + UserHandle::class.java + ).hook(HookStage.BEFORE) { param -> val notificationId = param.arg(1) notificationIdMap[notificationId]?.let { cachedMessages[it]?.clear() } } + + findClass("com.google.firebase.messaging.FirebaseMessagingService").run { + methods.first { it.declaringClass == this && it.returnType == Void::class.javaPrimitiveType && it.parameterCount == 1 && it.parameterTypes[0] == Intent::class.java } + .hook(HookStage.BEFORE) { param -> + val intent = param.argNullable(0) ?: return@hook + val messageType = intent.getStringExtra("type") ?: return@hook + val states = context.config.options(ConfigProperty.NOTIFICATION_BLACKLIST) + + Logger.xposedLog("received message type: $messageType") + + if (states[messageType.replaceFirst("mischief_", "")] == true) { + param.setResult(null) + } + } + } } data class NotificationData( diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt index 7b793cbf..65938625 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt @@ -1,6 +1,6 @@ package me.rhunk.snapenhance.features.impl.ui -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.features.BridgeFileFeature import me.rhunk.snapenhance.features.FeatureLoadParams diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt index 086722ce..3e64d9e9 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt @@ -1,7 +1,9 @@ package me.rhunk.snapenhance.features.impl.ui import android.annotation.SuppressLint +import android.content.Context import android.content.res.Resources +import android.text.SpannableString import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -32,7 +34,7 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE param.setResult(null) } - @SuppressLint("DiscouragedApi") + @SuppressLint("DiscouragedApi", "InternalInsetResource") override fun onActivityCreate() { val blockAds = context.config.bool(ConfigProperty.BLOCK_ADS) val hiddenElements = context.config.options(ConfigProperty.HIDE_UI_ELEMENTS) @@ -92,6 +94,20 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE hideStorySection(param) } + //mappings? + if (hideStorySection["hide_friend_suggestions"] == true && view.javaClass.superclass?.name?.endsWith("StackDrawLayout") == true) { + val layoutParams = view.layoutParams as? FrameLayout.LayoutParams ?: return@hook + if (layoutParams.width == -1 && + layoutParams.height == -2 && + view.javaClass.let { clazz -> + clazz.methods.any { it.returnType == SpannableString::class.java} && + clazz.constructors.any { it.parameterCount == 1 && it.parameterTypes[0] == Context::class.java } + } + ) { + hideStorySection(param) + } + } + if (hideStorySection["hide_following"] == true && (viewId == getIdentifier("df_small_story", "id")) ) { hideStorySection(param) @@ -101,9 +117,20 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE hideStorySection(param) } - if (isImmersiveCamera && view.id == getIdentifier("full_screen_surface_view", "id")) { - Hooker.hookObjectMethod(View::class.java, view, "setLayoutParams", HookStage.BEFORE) { - it.setArg(0, FrameLayout.LayoutParams(displayMetrics.widthPixels, displayMetrics.heightPixels)) + if (isImmersiveCamera) { + if (view.id == getIdentifier("edits_container", "id")) { + val deviceAspectRatio = displayMetrics.widthPixels.toFloat() / displayMetrics.heightPixels.toFloat() + Hooker.hookObjectMethod(View::class.java, view, "layout", HookStage.BEFORE) { + val width = it.arg(2) as Int + val realHeight = (width / deviceAspectRatio).toInt() + it.setArg(3, realHeight) + } + } + if (view.id == getIdentifier("full_screen_surface_view", "id")) { + Hooker.hookObjectMethod(View::class.java, view, "layout", HookStage.BEFORE) { + it.setArg(1, 1) + it.setArg(3, displayMetrics.heightPixels) + } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt b/app/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt index 5bfb3aa2..b3ee2a7a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt @@ -5,82 +5,78 @@ import de.robv.android.xposed.XposedBridge import java.lang.reflect.Member object Hooker { - private fun newMethodHook( + inline fun newMethodHook( stage: HookStage, - consumer: (HookAdapter) -> Unit, - filter: ((HookAdapter) -> Boolean) = { true } + crossinline consumer: (HookAdapter) -> Unit, + crossinline filter: ((HookAdapter) -> Boolean) = { true } ): XC_MethodHook { - val callEvent = { param: XC_MethodHook.MethodHookParam<*> -> - HookAdapter(param).takeIf(filter)?.also(consumer) - } - return if (stage == HookStage.BEFORE) object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam<*>) { - callEvent(param) + HookAdapter(param).takeIf(filter)?.also(consumer) } } else object : XC_MethodHook() { override fun afterHookedMethod(param: MethodHookParam<*>) { - callEvent(param) + HookAdapter(param).takeIf(filter)?.also(consumer) } } } - fun hook( + inline fun hook( clazz: Class<*>, methodName: String, stage: HookStage, - consumer: (HookAdapter) -> Unit + crossinline consumer: (HookAdapter) -> Unit ): Set = hook(clazz, methodName, stage, { true }, consumer) - fun hook( + inline fun hook( clazz: Class<*>, methodName: String, stage: HookStage, - filter: (HookAdapter) -> Boolean, - consumer: (HookAdapter) -> Unit + crossinline filter: (HookAdapter) -> Boolean, + crossinline consumer: (HookAdapter) -> Unit ): Set = XposedBridge.hookAllMethods(clazz, methodName, newMethodHook(stage, consumer, filter)) - fun hook( + inline fun hook( member: Member, stage: HookStage, - consumer: (HookAdapter) -> Unit + crossinline consumer: (HookAdapter) -> Unit ): XC_MethodHook.Unhook { return hook(member, stage, { true }, consumer) } - fun hook( + inline fun hook( member: Member, stage: HookStage, - filter: ((HookAdapter) -> Boolean), - consumer: (HookAdapter) -> Unit + crossinline filter: ((HookAdapter) -> Boolean), + crossinline consumer: (HookAdapter) -> Unit ): XC_MethodHook.Unhook { return XposedBridge.hookMethod(member, newMethodHook(stage, consumer, filter)) } - fun hookConstructor( + inline fun hookConstructor( clazz: Class<*>, stage: HookStage, - consumer: (HookAdapter) -> Unit + crossinline consumer: (HookAdapter) -> Unit ) { XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer)) } - fun hookConstructor( + inline fun hookConstructor( clazz: Class<*>, stage: HookStage, - filter: ((HookAdapter) -> Boolean), - consumer: (HookAdapter) -> Unit + crossinline filter: ((HookAdapter) -> Boolean), + crossinline consumer: (HookAdapter) -> Unit ) { XposedBridge.hookAllConstructors(clazz, newMethodHook(stage, consumer, filter)) } - fun hookObjectMethod( + inline fun hookObjectMethod( clazz: Class<*>, instance: Any, methodName: String, stage: HookStage, - hookConsumer: (HookAdapter) -> Unit + crossinline hookConsumer: (HookAdapter) -> Unit ) { val unhooks: MutableSet = HashSet() hook(clazz, methodName, stage) { param-> @@ -92,11 +88,11 @@ object Hooker { }.also { unhooks.addAll(it) } } - fun ephemeralHook( + inline fun ephemeralHook( clazz: Class<*>, methodName: String, stage: HookStage, - hookConsumer: (HookAdapter) -> Unit + crossinline hookConsumer: (HookAdapter) -> Unit ) { val unhooks: MutableSet = HashSet() hook(clazz, methodName, stage) { param-> @@ -105,12 +101,12 @@ object Hooker { }.also { unhooks.addAll(it) } } - fun ephemeralHookObjectMethod( + inline fun ephemeralHookObjectMethod( clazz: Class<*>, instance: Any, methodName: String, stage: HookStage, - hookConsumer: (HookAdapter) -> Unit + crossinline hookConsumer: (HookAdapter) -> Unit ) { val unhooks: MutableSet = HashSet() hook(clazz, methodName, stage) { param-> @@ -121,37 +117,37 @@ object Hooker { } } -fun Class<*>.hookConstructor( +inline fun Class<*>.hookConstructor( stage: HookStage, - consumer: (HookAdapter) -> Unit + crossinline consumer: (HookAdapter) -> Unit ) = Hooker.hookConstructor(this, stage, consumer) -fun Class<*>.hookConstructor( +inline fun Class<*>.hookConstructor( stage: HookStage, - filter: ((HookAdapter) -> Boolean), - consumer: (HookAdapter) -> Unit + crossinline filter: ((HookAdapter) -> Boolean), + crossinline consumer: (HookAdapter) -> Unit ) = Hooker.hookConstructor(this, stage, filter, consumer) -fun Class<*>.hook( +inline fun Class<*>.hook( methodName: String, stage: HookStage, - consumer: (HookAdapter) -> Unit + crossinline consumer: (HookAdapter) -> Unit ): Set = Hooker.hook(this, methodName, stage, consumer) -fun Class<*>.hook( +inline fun Class<*>.hook( methodName: String, stage: HookStage, - filter: (HookAdapter) -> Boolean, - consumer: (HookAdapter) -> Unit + crossinline filter: (HookAdapter) -> Boolean, + crossinline consumer: (HookAdapter) -> Unit ): Set = Hooker.hook(this, methodName, stage, filter, consumer) -fun Member.hook( +inline fun Member.hook( stage: HookStage, - consumer: (HookAdapter) -> Unit + crossinline consumer: (HookAdapter) -> Unit ): XC_MethodHook.Unhook = Hooker.hook(this, stage, consumer) -fun Member.hook( +inline fun Member.hook( stage: HookStage, - filter: ((HookAdapter) -> Boolean), - consumer: (HookAdapter) -> Unit + crossinline filter: ((HookAdapter) -> Boolean), + crossinline consumer: (HookAdapter) -> Unit ): XC_MethodHook.Unhook = Hooker.hook(this, stage, filter, consumer) \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt index 0620848d..7e14570a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt @@ -3,7 +3,7 @@ package me.rhunk.snapenhance.manager.impl import com.google.gson.JsonObject import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.config.ConfigAccessor import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.manager.Manager diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt index 862e484a..258bb1b9 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -11,35 +11,37 @@ import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.features.impl.experiments.AmoledDarkMode import me.rhunk.snapenhance.features.impl.experiments.AppPasscode +import me.rhunk.snapenhance.features.impl.experiments.DeviceSpooferHook import me.rhunk.snapenhance.features.impl.experiments.InfiniteStoryBoost import me.rhunk.snapenhance.features.impl.experiments.MeoPasscodeBypass import me.rhunk.snapenhance.features.impl.experiments.UnlimitedMultiSnap -import me.rhunk.snapenhance.features.impl.tweaks.AntiAutoSave -import me.rhunk.snapenhance.features.impl.tweaks.AutoSave -import me.rhunk.snapenhance.features.impl.tweaks.DisableVideoLengthRestriction -import me.rhunk.snapenhance.features.impl.tweaks.GalleryMediaSendOverride -import me.rhunk.snapenhance.features.impl.tweaks.LocationSpoofer -import me.rhunk.snapenhance.features.impl.tweaks.MediaQualityLevelOverride -import me.rhunk.snapenhance.features.impl.tweaks.Notifications -import me.rhunk.snapenhance.features.impl.tweaks.SnapchatPlus -import me.rhunk.snapenhance.features.impl.tweaks.UnlimitedSnapViewTime import me.rhunk.snapenhance.features.impl.privacy.DisableMetrics import me.rhunk.snapenhance.features.impl.privacy.PreventMessageSending import me.rhunk.snapenhance.features.impl.spying.AnonymousStoryViewing import me.rhunk.snapenhance.features.impl.spying.MessageLogger import me.rhunk.snapenhance.features.impl.spying.PreventReadReceipts import me.rhunk.snapenhance.features.impl.spying.StealthMode +import me.rhunk.snapenhance.features.impl.tweaks.AntiAutoSave +import me.rhunk.snapenhance.features.impl.tweaks.AutoSave import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks +import me.rhunk.snapenhance.features.impl.tweaks.DisableVideoLengthRestriction +import me.rhunk.snapenhance.features.impl.tweaks.GalleryMediaSendOverride +import me.rhunk.snapenhance.features.impl.tweaks.GooglePlayServicesDialogs +import me.rhunk.snapenhance.features.impl.tweaks.LocationSpoofer +import me.rhunk.snapenhance.features.impl.tweaks.MediaQualityLevelOverride +import me.rhunk.snapenhance.features.impl.tweaks.Notifications +import me.rhunk.snapenhance.features.impl.tweaks.SnapchatPlus +import me.rhunk.snapenhance.features.impl.tweaks.UnlimitedSnapViewTime import me.rhunk.snapenhance.features.impl.ui.PinConversations import me.rhunk.snapenhance.features.impl.ui.StartupPageOverride import me.rhunk.snapenhance.features.impl.ui.UITweaks -import me.rhunk.snapenhance.ui.menu.impl.MenuViewInjector import me.rhunk.snapenhance.manager.Manager +import me.rhunk.snapenhance.ui.menu.impl.MenuViewInjector import java.util.concurrent.Executors import kotlin.reflect.KClass class FeatureManager(private val context: ModContext) : Manager { - private val asyncLoadExecutorService = Executors.newCachedThreadPool() + private val asyncLoadExecutorService = Executors.newFixedThreadPool(5) private val features = mutableListOf() private fun register(featureClass: KClass) { @@ -88,7 +90,9 @@ class FeatureManager(private val context: ModContext) : Manager { register(AmoledDarkMode::class) register(PinConversations::class) register(UnlimitedMultiSnap::class) + register(DeviceSpooferHook::class) register(StartupPageOverride::class) + register(GooglePlayServicesDialogs::class) initializeFeatures() } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt index 3dce4026..86d8ad65 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt @@ -2,45 +2,44 @@ package me.rhunk.snapenhance.manager.impl import android.app.AlertDialog import com.google.gson.JsonElement -import com.google.gson.JsonObject import com.google.gson.JsonParser -import kotlinx.coroutines.Job -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.R -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.manager.Manager -import me.rhunk.snapenhance.mapping.Mapper -import me.rhunk.snapenhance.mapping.impl.BCryptClassMapper -import me.rhunk.snapenhance.mapping.impl.CallbackMapper -import me.rhunk.snapenhance.mapping.impl.DefaultMediaItemMapper -import me.rhunk.snapenhance.mapping.impl.EnumMapper -import me.rhunk.snapenhance.mapping.impl.OperaPageViewControllerMapper -import me.rhunk.snapenhance.mapping.impl.PlatformAnalyticsCreatorMapper -import me.rhunk.snapenhance.mapping.impl.PlusSubscriptionMapper -import me.rhunk.snapenhance.mapping.impl.ScCameraSettingsMapper -import me.rhunk.snapenhance.mapping.impl.StoryBoostStateMapper -import me.rhunk.snapenhance.util.getObjectField +import me.rhunk.snapenhance.ui.ViewAppearanceHelper +import me.rhunk.snapmapper.Mapper +import me.rhunk.snapmapper.impl.BCryptClassMapper +import me.rhunk.snapmapper.impl.CallbackMapper +import me.rhunk.snapmapper.impl.DefaultMediaItemMapper +import me.rhunk.snapmapper.impl.EnumMapper +import me.rhunk.snapmapper.impl.FriendsFeedEventDispatcherMapper +import me.rhunk.snapmapper.impl.MediaQualityLevelProviderMapper +import me.rhunk.snapmapper.impl.OperaPageViewControllerMapper +import me.rhunk.snapmapper.impl.PlatformAnalyticsCreatorMapper +import me.rhunk.snapmapper.impl.PlusSubscriptionMapper +import me.rhunk.snapmapper.impl.ScCameraSettingsMapper +import me.rhunk.snapmapper.impl.StoryBoostStateMapper import java.nio.charset.StandardCharsets import java.util.concurrent.ConcurrentHashMap +import kotlin.system.measureTimeMillis @Suppress("UNCHECKED_CAST") class MappingManager(private val context: ModContext) : Manager { - private val mappers = mutableListOf().apply { - add(CallbackMapper()) - add(EnumMapper()) - add(OperaPageViewControllerMapper()) - add(PlusSubscriptionMapper()) - add(DefaultMediaItemMapper()) - add(BCryptClassMapper()) - add(PlatformAnalyticsCreatorMapper()) - add(ScCameraSettingsMapper()) - add(StoryBoostStateMapper()) - } + private val mappers = arrayOf( + BCryptClassMapper::class, + CallbackMapper::class, + DefaultMediaItemMapper::class, + MediaQualityLevelProviderMapper::class, + EnumMapper::class, + OperaPageViewControllerMapper::class, + PlatformAnalyticsCreatorMapper::class, + PlusSubscriptionMapper::class, + ScCameraSettingsMapper::class, + StoryBoostStateMapper::class, + FriendsFeedEventDispatcherMapper::class + ) private val mappings = ConcurrentHashMap() val areMappingsLoaded: Boolean @@ -69,7 +68,7 @@ class MappingManager(private val context: ModContext) : Manager { return } context.runOnUiThread { - val statusDialogBuilder = AlertDialog.Builder(context.mainActivity, AlertDialog.THEME_DEVICE_DEFAULT_DARK) + val statusDialogBuilder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) .setMessage("Generating mappings, please wait...") .setCancelable(false) .setView(android.widget.ProgressBar(context.mainActivity).apply { @@ -127,70 +126,27 @@ class MappingManager(private val context: ModContext) : Manager { } } - private fun executeMappers(classes: List>) = runBlocking { - val jobs = mutableListOf() - mappers.forEach { mapper -> - mapper.context = context - launch { - runCatching { - mapper.useClasses(context.androidContext.classLoader, classes, mappings) - }.onFailure { - Logger.xposedLog("Failed to execute mapper ${mapper.javaClass.simpleName}", it) - } - }.also { jobs.add(it) } - } - jobs.joinAll() - } - - @Suppress("UNCHECKED_CAST", "DEPRECATION") + @Suppress("DEPRECATION") private fun refresh() { - val classes: MutableList> = ArrayList() + val mapper = Mapper(*mappers) - val classLoader = context.androidContext.classLoader - val dexPathList = classLoader.getObjectField("pathList")!! - val dexElements = dexPathList.getObjectField("dexElements") as Array - - dexElements.forEach { dexElement: Any -> - (dexElement.getObjectField("dexFile") as dalvik.system.DexFile?)?.apply { - entries().toList().forEach fileList@{ className -> - //ignore classes without a dot in them - if (className.contains(".") && !className.startsWith("com.snap")) return@fileList - runCatching { - classLoader.loadClass(className)?.let { - //force load fields to avoid ClassNotFoundExceptions when executing mappers - it.declaredFields - classes.add(it) - } - }.onFailure { - Logger.debug("Failed to load class $className") - } - } - } + runCatching { + mapper.loadApk(context.androidContext.packageManager.getApplicationInfo( + Constants.SNAPCHAT_PACKAGE_NAME, + 0 + ).sourceDir) + }.onFailure { + throw Exception("Failed to load APK", it) } - executeMappers(classes) - write() - } - - private fun write() { - val mappingsObject = JsonObject() - mappingsObject.addProperty("snap_build_number", snapBuildNumber) - mappings.forEach { (key, value) -> - if (value is List<*>) { - mappingsObject.add(key, context.gson.toJsonTree(value)) - return@forEach + measureTimeMillis { + val result = mapper.start().apply { + addProperty("snap_build_number", snapBuildNumber) } - if (value is Map<*, *>) { - mappingsObject.add(key, context.gson.toJsonTree(value)) - return@forEach - } - mappingsObject.addProperty(key, value.toString()) + context.bridgeClient.writeFile(BridgeFileType.MAPPINGS, result.toString().toByteArray()) + }.also { + Logger.xposedLog("Generated mappings in $it ms") } - - context.bridgeClient.writeFile( - BridgeFileType.MAPPINGS, - mappingsObject.toString().toByteArray() - ) } fun getMappedObject(key: String): Any { @@ -200,6 +156,10 @@ class MappingManager(private val context: ModContext) : Manager { throw Exception("No mapping found for $key") } + fun getMappedObjectNullable(key: String): Any? { + return mappings[key] + } + fun getMappedClass(className: String): Class<*> { return context.androidContext.classLoader.loadClass(getMappedObject(className) as String) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/Mapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/Mapper.kt deleted file mode 100644 index c9f3df16..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/Mapper.kt +++ /dev/null @@ -1,13 +0,0 @@ -package me.rhunk.snapenhance.mapping - -import me.rhunk.snapenhance.ModContext - -abstract class Mapper { - lateinit var context: ModContext - - abstract fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/BCryptClassMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/BCryptClassMapper.kt deleted file mode 100644 index 2cfd416c..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/BCryptClassMapper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package me.rhunk.snapenhance.mapping.impl - -import me.rhunk.snapenhance.mapping.Mapper -import java.lang.reflect.Modifier - -class BCryptClassMapper : Mapper() { - override fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) { - for (clazz in classes) { - if (!Modifier.isFinal(clazz.modifiers)) continue - clazz.declaredFields.firstOrNull { it.type == IntArray::class.java && Modifier.isStatic(it.modifiers)}?.let { field -> - val fieldData = field.get(null) - if (fieldData !is IntArray) return@let - if (fieldData.size != 18 || fieldData[0] != 608135816) return@let - mappings["BCryptClass"] = clazz.name - mappings["BCryptClassHashMethod"] = clazz.methods.first { - it.parameterTypes.size == 2 && it.returnType == String::class.java && it.parameterTypes[0] == String::class.java && it.parameterTypes[1] == String::class.java - }.name - return - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/CallbackMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/CallbackMapper.kt deleted file mode 100644 index 6bca4730..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/CallbackMapper.kt +++ /dev/null @@ -1,29 +0,0 @@ -package me.rhunk.snapenhance.mapping.impl - -import me.rhunk.snapenhance.Logger.debug -import me.rhunk.snapenhance.mapping.Mapper -import java.lang.reflect.Method -import java.lang.reflect.Modifier - -class CallbackMapper : Mapper() { - override fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) { - val callbackMappings = HashMap() - classes.forEach { clazz -> - val superClass = clazz.superclass ?: return@forEach - if (!superClass.name.endsWith("Callback") || superClass.name.endsWith("\$Callback")) return@forEach - if (!Modifier.isAbstract(superClass.modifiers)) return@forEach - - if (superClass.declaredMethods.any { method: Method -> - method.name == "onError" - }) { - callbackMappings[superClass.simpleName] = clazz.name - } - } - debug("found " + callbackMappings.size + " callbacks") - mappings["callbacks"] = callbackMappings - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/DefaultMediaItemMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/DefaultMediaItemMapper.kt deleted file mode 100644 index d05754c4..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/DefaultMediaItemMapper.kt +++ /dev/null @@ -1,24 +0,0 @@ -package me.rhunk.snapenhance.mapping.impl - -import android.net.Uri -import me.rhunk.snapenhance.mapping.Mapper -import java.lang.reflect.Modifier - -class DefaultMediaItemMapper : Mapper() { - override fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) { - for (clazz in classes) { - if (clazz.superclass == null || !Modifier.isAbstract(clazz.superclass.modifiers)) continue - if (clazz.superclass.interfaces.isEmpty() || clazz.superclass.interfaces[0] != Comparable::class.java) continue - if (clazz.methods.none { it.returnType == Uri::class.java }) continue - - val constructorParameters = clazz.constructors[0]?.parameterTypes ?: continue - if (constructorParameters.size < 6 || constructorParameters[5] != Long::class.javaPrimitiveType) continue - - mappings["DefaultMediaItem"] = clazz.name - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt deleted file mode 100644 index 61be21fb..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/EnumMapper.kt +++ /dev/null @@ -1,66 +0,0 @@ -package me.rhunk.snapenhance.mapping.impl - -import me.rhunk.snapenhance.Logger.debug -import me.rhunk.snapenhance.mapping.Mapper -import java.lang.reflect.Method -import java.lang.reflect.Modifier -import java.util.Objects - - -class EnumMapper : Mapper() { - override fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) { - val enumMappings = HashMap() - var enumQualityLevel: Class<*>? = null - - //settings classes have an interface that extends Serializable and contains the getName method - //this enum classes are used to store the settings values - //Setting enum class -> implements an interface -> getName method - classes.forEach { clazz -> - if (!clazz.isEnum) return@forEach - - //quality level enum - if (enumQualityLevel == null) { - if (clazz.enumConstants.any { it.toString().startsWith("LEVEL_NONE") }) { - enumMappings["QualityLevel"] = clazz.name - enumQualityLevel = clazz - } - } - - if (clazz.interfaces.isEmpty()) return@forEach - val serializableInterfaceClass = clazz.interfaces[0] - if (serializableInterfaceClass.methods - .filter { method: Method -> method.declaringClass == serializableInterfaceClass } - .none { method: Method -> method.name == "getName" } - ) return@forEach - - runCatching { - val getEnumNameMethod = - serializableInterfaceClass.methods.first { it!!.returnType.isEnum } - clazz.enumConstants?.onEach { enumConstant -> - val enumName = - Objects.requireNonNull(getEnumNameMethod.invoke(enumConstant)).toString() - enumMappings[enumName] = clazz.name - } - } - } - - debug("found " + enumMappings.size + " enums") - mappings["enums"] = enumMappings - - //find the media quality level provider - for (clazz in classes) { - if (!Modifier.isAbstract(clazz.modifiers)) continue - if (clazz.fields.none { Modifier.isTransient(it.modifiers) }) continue - clazz.methods.firstOrNull { it.returnType == enumQualityLevel }?.let { - mappings["MediaQualityLevelProvider"] = clazz.name - mappings["MediaQualityLevelProviderMethod"] = it.name - debug("found MediaQualityLevelProvider: ${clazz.name}.${it.name}") - return - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/OperaPageViewControllerMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/OperaPageViewControllerMapper.kt deleted file mode 100644 index 42f06a3e..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/OperaPageViewControllerMapper.kt +++ /dev/null @@ -1,78 +0,0 @@ -package me.rhunk.snapenhance.mapping.impl - -import me.rhunk.snapenhance.mapping.Mapper -import me.rhunk.snapenhance.util.ReflectionHelper -import java.lang.reflect.Field -import java.lang.reflect.Method -import java.lang.reflect.Modifier -import java.util.Arrays - - -class OperaPageViewControllerMapper : Mapper() { - override fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) { - var operaPageViewControllerClass: Class<*>? = null - for (aClass in classes) { - if (!Modifier.isAbstract(aClass.modifiers)) continue - if (aClass.interfaces.isEmpty()) continue - val foundFields = Arrays.stream(aClass.declaredFields).filter { field: Field -> - val modifiers = field.modifiers - Modifier.isStatic(modifiers) && Modifier.isFinal( - modifiers - ) - }.filter { field: Field -> - try { - return@filter "ad_product_type" == String.format("%s", field[null]) - } catch (e: IllegalAccessException) { - e.printStackTrace() - } - false - }.count() - if (foundFields == 0L) continue - operaPageViewControllerClass = aClass - break - } - if (operaPageViewControllerClass == null) throw RuntimeException("OperaPageViewController not found") - - val members = HashMap() - members["Class"] = operaPageViewControllerClass.name - - operaPageViewControllerClass.fields.forEach { field -> - val fieldType = field.type - if (fieldType.isEnum) { - fieldType.enumConstants.firstOrNull { enumConstant: Any -> enumConstant.toString() == "FULLY_DISPLAYED" } - .let { members["viewStateField"] = field.name } - } - if (fieldType == ArrayList::class.java) { - members["layerListField"] = field.name - } - } - val enumViewStateClass = operaPageViewControllerClass.fields.first { field: Field -> - field.name == members["viewStateField"] - }.type - - //find the method that call the onDisplayStateChange method - members["onDisplayStateChange"] = - operaPageViewControllerClass.methods.first { method: Method -> - if (method.returnType != Void.TYPE || method.parameterTypes.size != 1) return@first false - val firstParameterClass = method.parameterTypes[0] - //check if the class contains a field with the enumViewStateClass type - ReflectionHelper.searchFieldByType(firstParameterClass, enumViewStateClass) != null - }.name - - //find the method that call the onDisplayStateChange method from gestures - members["onDisplayStateChange2"] = - operaPageViewControllerClass.methods.first { method: Method -> - if (method.returnType != Void.TYPE || method.parameterTypes.size != 2) return@first false - val firstParameterClass = method.parameterTypes[0] - val secondParameterClass = method.parameterTypes[1] - firstParameterClass.isEnum && secondParameterClass.isEnum - }.name - - mappings["OperaPageViewController"] = members - return - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlatformAnalyticsCreatorMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlatformAnalyticsCreatorMapper.kt deleted file mode 100644 index fba97138..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlatformAnalyticsCreatorMapper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package me.rhunk.snapenhance.mapping.impl - -import me.rhunk.snapenhance.mapping.Mapper - -class PlatformAnalyticsCreatorMapper : Mapper() { - override fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) { - for (clazz in classes) { - if (clazz.isEnum || clazz.isInterface) continue - val constructors = clazz.constructors - if (constructors.isEmpty()) continue - val firstConstructor = constructors[0] - // 47 is the number of parameters of the constructor - // can change in future versions - if (firstConstructor.parameterCount != 47) continue - if (!firstConstructor.parameterTypes[0].isEnum) continue - if (firstConstructor.parameterTypes[0].enumConstants.none { it.toString() == "IN_APP_NOTIFICATION" }) continue - - mappings["PlatformAnalyticsCreator"] = clazz.name - return - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlusSubscriptionMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlusSubscriptionMapper.kt deleted file mode 100644 index debb4020..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/PlusSubscriptionMapper.kt +++ /dev/null @@ -1,27 +0,0 @@ -package me.rhunk.snapenhance.mapping.impl - -import me.rhunk.snapenhance.mapping.Mapper -import java.lang.reflect.Modifier - - -class PlusSubscriptionMapper : Mapper() { - override fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) { - for (clazz in classes) { - clazz.declaredFields.firstOrNull { - it.type == clazz && - Modifier.isFinal(it.modifiers) && - Modifier.isStatic(it.modifiers) && - runCatching { - it?.get(null).toString().startsWith("PlusSubscriptionState") - }.getOrDefault(false) - } ?: continue - - mappings["SubscriptionInfoClass"] = clazz.constructors[0]!!.parameterTypes[0]!!.name - return - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/ScCameraSettingsMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/ScCameraSettingsMapper.kt deleted file mode 100644 index fe4a6d8c..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/ScCameraSettingsMapper.kt +++ /dev/null @@ -1,21 +0,0 @@ -package me.rhunk.snapenhance.mapping.impl - -import me.rhunk.snapenhance.mapping.Mapper - -class ScCameraSettingsMapper : Mapper() { - override fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) { - for (clazz in classes) { - if (clazz.constructors.isEmpty()) continue - val parameters = clazz.constructors.first().parameterTypes - if (parameters.size < 27) continue - val firstParameter = parameters[0] - if (!firstParameter.isEnum || firstParameter.enumConstants.find { it.toString() == "CONTINUOUS_PICTURE" } == null) continue - mappings["ScCameraSettings"] = clazz.name - return - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/StoryBoostStateMapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/StoryBoostStateMapper.kt deleted file mode 100644 index 46f0abef..00000000 --- a/app/src/main/kotlin/me/rhunk/snapenhance/mapping/impl/StoryBoostStateMapper.kt +++ /dev/null @@ -1,21 +0,0 @@ -package me.rhunk.snapenhance.mapping.impl - -import me.rhunk.snapenhance.mapping.Mapper - -class StoryBoostStateMapper : Mapper(){ - override fun useClasses( - classLoader: ClassLoader, - classes: List>, - mappings: MutableMap - ) { - for (clazz in classes) { - val firstConstructor = clazz.constructors.firstOrNull() ?: continue - if (firstConstructor.parameterCount != 3) continue - if (firstConstructor.parameterTypes[1] != Long::class.javaPrimitiveType || firstConstructor.parameterTypes[2] != Long::class.javaPrimitiveType) continue - val storyBoostEnumClass = firstConstructor.parameterTypes[0] - if (!storyBoostEnumClass.isEnum || storyBoostEnumClass.enumConstants.none { it.toString() == "NeedSubscriptionCannotSubscribe" }) continue - mappings["StoryBoostStateClass"] = clazz.name - return - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt new file mode 100644 index 00000000..c78ae4b2 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt @@ -0,0 +1,85 @@ +package me.rhunk.snapenhance.ui + +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import me.rhunk.snapenhance.R +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.util.ActivityResultCallback +import kotlin.math.abs +import kotlin.random.Random + +class ItemHelper( + private val context : Context +) { + val positiveButtonText by lazy { + SharedContext.translation["button.ok"] + } + + val cancelButtonText by lazy { + SharedContext.translation["button.cancel"] + } + + fun longToast(message: String, context: Context) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + + fun createTranslatedTextView(property: ConfigProperty, shouldTranslatePropertyValue: Boolean = true): TextView { + return object: TextView(context) { + override fun setText(text: CharSequence?, type: BufferType?) { + val newText = text?.takeIf { it.isNotEmpty() }?.let { + if (!shouldTranslatePropertyValue || property.disableValueLocalization) it + else SharedContext.translation[property.getOptionTranslationKey(it.toString())] + }?.let { + if (it.length > 20) { + "...${it.substring(it.length - 20)}" + } else { + it + } + } ?: "" + super.setTextColor(context.getColor(R.color.tertiaryText)) + super.setText(newText, type) + } + } + } + + fun askForValue(property: ConfigProperty, requestedInputType: Int, callback: (String) -> Unit) { + val editText = EditText(context).apply { + inputType = requestedInputType + setText(property.valueContainer.value().toString()) + } + AlertDialog.Builder(context) + .setTitle(SharedContext.translation["property.${property.translationKey}.name"]) + .setView(editText) + .setPositiveButton(positiveButtonText) { _, _ -> + callback(editText.text.toString()) + } + .setNegativeButton(cancelButtonText) { dialog, _ -> + dialog.cancel() + } + .show() + } + + fun askForFolder(activity: Activity, property: ConfigProperty, callback: (String) -> Unit): Pair { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + + val requestCode = abs(Random.nextInt()) + activity.startActivityForResult(intent, requestCode) + + return requestCode to let@{_, resultCode, data -> + if (resultCode != Activity.RESULT_OK) return@let + val uri = data?.data ?: return@let + val value = uri.toString() + activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + property.valueContainer.writeFrom(value) + callback(value) + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/ViewAppearanceHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt similarity index 94% rename from app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/ViewAppearanceHelper.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt index c1378e6a..3f6cdd31 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/ViewAppearanceHelper.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt @@ -1,6 +1,8 @@ -package me.rhunk.snapenhance.ui.menu +package me.rhunk.snapenhance.ui import android.annotation.SuppressLint +import android.app.AlertDialog +import android.content.Context import android.content.res.ColorStateList import android.graphics.Color import android.graphics.drawable.ColorDrawable @@ -90,4 +92,6 @@ object ViewAppearanceHelper { ) } } + + fun newAlertDialogBuilder(context: Context?) = AlertDialog.Builder(context, android.R.style.Theme_DeviceDefault_Dialog_Alert) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt index 6f899bea..0f6093a6 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt @@ -2,13 +2,13 @@ package me.rhunk.snapenhance.ui.config import android.app.Activity import android.app.AlertDialog +import android.content.Intent import android.content.res.ColorStateList import android.os.Bundle import android.text.Html import android.text.InputType import android.view.View import android.view.ViewGroup -import android.widget.EditText import android.widget.ImageButton import android.widget.Switch import android.widget.TextView @@ -16,7 +16,6 @@ import android.widget.Toast import me.rhunk.snapenhance.BuildConfig import me.rhunk.snapenhance.R import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.bridge.ConfigWrapper import me.rhunk.snapenhance.config.ConfigCategory import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.config.impl.ConfigIntegerValue @@ -24,9 +23,14 @@ import me.rhunk.snapenhance.config.impl.ConfigStateListValue import me.rhunk.snapenhance.config.impl.ConfigStateSelection import me.rhunk.snapenhance.config.impl.ConfigStateValue import me.rhunk.snapenhance.config.impl.ConfigStringValue +import me.rhunk.snapenhance.ui.ItemHelper +import me.rhunk.snapenhance.util.ActivityResultCallback +import kotlin.system.exitProcess + class ConfigActivity : Activity() { - private val config = ConfigWrapper() + private val itemHelper = ItemHelper(this) + private val activityResultCallbacks = mutableMapOf() @Deprecated("Deprecated in Java") @Suppress("DEPRECATION") @@ -37,65 +41,20 @@ class ConfigActivity : Activity() { override fun onDestroy() { super.onDestroy() - config.writeConfig() + SharedContext.config.writeConfig() } override fun onPause() { super.onPause() - config.writeConfig() + SharedContext.config.writeConfig() } - private val positiveButtonText by lazy { - SharedContext.translation["button.ok"] - } - - private val cancelButtonText by lazy { - SharedContext.translation["button.cancel"] - } - - private fun longToast(message: String) { - Toast.makeText(this, message, Toast.LENGTH_LONG).show() - } - - private fun createTranslatedTextView(property: ConfigProperty, shouldTranslatePropertyValue: Boolean = true): TextView { - return object: TextView(this) { - override fun setText(text: CharSequence?, type: BufferType?) { - val newText = text?.takeIf { it.isNotEmpty() }?.let { - if (!shouldTranslatePropertyValue || property.disableValueLocalization) it - else SharedContext.translation["option.property." + property.translationKey + "." + it] - }?.let { - if (it.length > 20) { - it.substring(0, 20) + "..." - } else { - it - } - } ?: "" - super.setTextColor(getColor(R.color.tertiaryText)) - super.setText(newText, type) - } - } - } - - private fun askForValue(property: ConfigProperty, requestedInputType: Int, callback: (String) -> Unit) { - val editText = EditText(this).apply { - inputType = requestedInputType - setText(property.valueContainer.value().toString()) - } - AlertDialog.Builder(this) - .setTitle(SharedContext.translation["property.${property.translationKey}.name"]) - .setView(editText) - .setPositiveButton(positiveButtonText) { _, _ -> - callback(editText.text.toString()) - } - .setNegativeButton(cancelButtonText) { dialog, _ -> - dialog.cancel() - } - .show() + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + activityResultCallbacks[requestCode]?.invoke(requestCode, resultCode, data) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - config.loadFromContext(this) SharedContext.ensureInitialized(this) setContentView(R.layout.config_activity) @@ -126,9 +85,37 @@ class ConfigActivity : Activity() { }) } + //check if save folder is set + //TODO: first run activity + run { + val saveFolder = SharedContext.config.string(ConfigProperty.SAVE_FOLDER) + val itemHelper = ItemHelper(this) + + if (saveFolder.isEmpty() || !saveFolder.startsWith("content://")) { + AlertDialog.Builder(this) + .setTitle("Save folder") + .setMessage("Please select a folder where you want to save downloaded files.") + .setPositiveButton("Select") { _, _ -> + val (requestCode, callback) = itemHelper.askForFolder( + this, + ConfigProperty.SAVE_FOLDER + ) {} + activityResultCallbacks[requestCode] = { a1, a2, a3 -> + callback(a1, a2, a3) + Toast.makeText(this, "Save Folder set!", Toast.LENGTH_SHORT).show() + finish() + } + } + .setNegativeButton("Cancel") { _, _ -> + exitProcess(0) + } + .show() + } + } + var currentCategory: ConfigCategory? = null - config.entries().forEach { (property, value) -> + SharedContext.config.entries().filter { !it.key.category.hidden }.forEach { (property, value) -> val configItem = layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false) fun addSeparator() { @@ -140,6 +127,7 @@ class ConfigActivity : Activity() { } if (property.category != currentCategory) { + if(!property.shouldAppearInSettings) return@forEach currentCategory = property.category with(layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false)) { findViewById(R.id.name).apply { @@ -187,22 +175,32 @@ class ConfigActivity : Activity() { addValueView(switch) } is ConfigStringValue, is ConfigIntegerValue -> { - val textView = createTranslatedTextView(property, shouldTranslatePropertyValue = false).also { + val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false).also { it.text = value.value().toString() } configItem.setOnClickListener { + if (value is ConfigStringValue && value.isFolderPath) { + val (requestCode, callback) = itemHelper.askForFolder(this, property) { + value.writeFrom(it) + textView.text = value.value() + } + + activityResultCallbacks[requestCode] = callback + return@setOnClickListener + } + if (value is ConfigIntegerValue) { - askForValue(property, InputType.TYPE_CLASS_NUMBER) { + itemHelper.askForValue(property, InputType.TYPE_CLASS_NUMBER) { try { value.writeFrom(it) textView.text = value.value().toString() } catch (e: NumberFormatException) { - longToast(SharedContext.translation["config_activity.invalid_number_toast"]) + itemHelper.longToast(SharedContext.translation["config_activity.invalid_number_toast"], this) } } return@setOnClickListener } - askForValue(property, InputType.TYPE_CLASS_TEXT) { + itemHelper.askForValue(property, InputType.TYPE_CLASS_TEXT) { value.writeFrom(it) textView.text = value.value().toString() } @@ -210,7 +208,7 @@ class ConfigActivity : Activity() { addValueView(textView) } is ConfigStateListValue -> { - val textView = createTranslatedTextView(property, shouldTranslatePropertyValue = false) + val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false) val values = value.value() fun updateText() { @@ -222,13 +220,13 @@ class ConfigActivity : Activity() { configItem.setOnClickListener { AlertDialog.Builder(this) .setTitle(propertyName) - .setPositiveButton(positiveButtonText) { _, _ -> + .setPositiveButton(itemHelper.positiveButtonText) { _, _ -> updateText() } .setMultiChoiceItems( values.keys.map { if (property.disableValueLocalization) it - else SharedContext.translation["option.property." + property.translationKey + "." + it] + else SharedContext.translation[property.getOptionTranslationKey(it)] }.toTypedArray(), values.map { it.value }.toBooleanArray() ) { _, which, isChecked -> @@ -240,7 +238,7 @@ class ConfigActivity : Activity() { addValueView(textView) } is ConfigStateSelection -> { - val textView = createTranslatedTextView(property, shouldTranslatePropertyValue = true) + val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = true) textView.text = value.value() configItem.setOnClickListener { @@ -250,14 +248,14 @@ class ConfigActivity : Activity() { builder.setSingleChoiceItems( value.keys().toTypedArray().map { if (property.disableValueLocalization) it - else SharedContext.translation["option.property." + property.translationKey + "." + it] + else SharedContext.translation[property.getOptionTranslationKey(it)] }.toTypedArray(), value.keys().indexOf(value.value()) ) { _, which -> value.writeFrom(value.keys()[which]) } - builder.setPositiveButton(positiveButtonText) { _, _ -> + builder.setPositiveButton(itemHelper.positiveButtonText) { _, _ -> textView.text = value.value() } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt index 93f7c5c3..40493870 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt @@ -11,8 +11,9 @@ import android.widget.TextView import android.widget.Toast import me.rhunk.snapenhance.R import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.ui.config.ConfigActivity +import me.rhunk.snapenhance.ui.spoof.DeviceSpooferActivity import java.io.File class ActionListAdapter( @@ -70,6 +71,9 @@ class DebugSettingsLayoutInflater( add(SharedContext.translation["config_activity.title"] to { activity.startActivity(Intent(activity, ConfigActivity::class.java)) }) + add(SharedContext.translation["spoof_activity.title"] to { + activity.startActivity(Intent(activity, DeviceSpooferActivity::class.java)) + }) add(debugSettingsTranslation["clear_cache_title"] to { context.cacheDir.listFiles()?.forEach { it.deleteRecursively() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt index 436661ff..e250db9a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt @@ -1,5 +1,6 @@ package me.rhunk.snapenhance.ui.download +import android.annotation.SuppressLint import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -15,12 +16,13 @@ import android.widget.Toast import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Adapter -import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.job import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.R import me.rhunk.snapenhance.SharedContext import me.rhunk.snapenhance.data.FileType @@ -28,13 +30,16 @@ import me.rhunk.snapenhance.download.data.PendingDownload import me.rhunk.snapenhance.download.enums.DownloadStage import me.rhunk.snapenhance.util.snap.PreviewUtils import java.io.File +import java.io.FileInputStream import java.net.URL import kotlin.concurrent.thread +import kotlin.coroutines.coroutineContext class DownloadListAdapter( private val activity: DownloadManagerActivity, private val downloadList: MutableList ): Adapter() { + private val coroutineScope = CoroutineScope(Dispatchers.IO) private val previewJobs = mutableMapOf() inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) { @@ -62,32 +67,59 @@ class DownloadListAdapter( return downloadList.size } - @OptIn(DelicateCoroutinesApi::class) - private fun handlePreview(download: PendingDownload, holder: ViewHolder) { - download.outputFile?.let { File(it) }?.takeIf { it.exists() }?.let { - GlobalScope.launch(Dispatchers.IO) { - val previewBitmap = PreviewUtils.createPreviewFromFile(it)?.let { preview -> - val offsetY = (preview.height / 2 - holder.viewHeight / 2).coerceAtLeast(0) - - Bitmap.createScaledBitmap( - Bitmap.createBitmap(preview, 0, offsetY, - preview.width.coerceAtMost(holder.viewWidth), - preview.height.coerceAtMost(holder.viewHeight) - ), - holder.viewWidth, - holder.viewHeight, - false - ) - }?: return@launch - - if (coroutineContext.job.isCancelled) return@launch - Handler(holder.view.context.mainLooper).post { - holder.view.background = RoundedBitmapDrawableFactory.create(holder.view.context.resources, previewBitmap).also { - it.cornerRadius = holder.radius.toFloat() + @SuppressLint("Recycle") + private suspend fun handlePreview(download: PendingDownload, holder: ViewHolder) { + download.outputFile?.let { + val uri = Uri.parse(it) + runCatching { + if (uri.scheme == "content") { + val fileType = activity.contentResolver.openInputStream(uri)!!.use { stream -> + FileType.fromInputStream(stream) } + fileType to activity.contentResolver.openInputStream(uri) + } else { + FileType.fromFile(File(it)) to FileInputStream(it) + } + }.getOrNull() + }?.also { (fileType, assetStream) -> + val previewBitmap = assetStream?.use { stream -> + //don't preview files larger than 30MB + if (stream.available() > 30 * 1024 * 1024) return@also + + val tempFile = File.createTempFile("preview", ".${fileType.fileExtension}") + tempFile.outputStream().use { output -> + stream.copyTo(output) + } + runCatching { + PreviewUtils.createPreviewFromFile(tempFile)?.let { preview -> + val offsetY = (preview.height / 2 - holder.viewHeight / 2).coerceAtLeast(0) + + Bitmap.createScaledBitmap( + Bitmap.createBitmap( + preview, 0, offsetY, + preview.width.coerceAtMost(holder.viewWidth), + preview.height.coerceAtMost(holder.viewHeight) + ), + holder.viewWidth, + holder.viewHeight, + false + ) + } + }.onFailure { + Logger.error("failed to create preview $fileType", it) + }.also { + tempFile.delete() + }.getOrNull() + } ?: return@also + + if (coroutineContext.job.isCancelled) return@also + Handler(holder.view.context.mainLooper).post { + holder.view.background = RoundedBitmapDrawableFactory.create( + holder.view.context.resources, + previewBitmap + ).also { + it.cornerRadius = holder.radius.toFloat() } - }.also { job -> - previewJobs[holder.hashCode()] = job } } } @@ -96,7 +128,11 @@ class DownloadListAdapter( holder.status.text = download.downloadStage.toString() holder.view.background = holder.view.context.getDrawable(R.drawable.download_manager_item_background) - handlePreview(download, holder) + coroutineScope.launch { + withTimeout(2000) { + handlePreview(download, holder) + } + } val isSaved = download.downloadStage == DownloadStage.SAVED //if the download is in progress, the user can cancel it @@ -129,7 +165,7 @@ class DownloadListAdapter( holder.bitmojiIcon.setImageResource(R.drawable.bitmoji_blank) - pendingDownload.iconUrl?.let { url -> + pendingDownload.metadata.iconUrl?.let { url -> thread(start = true) { runCatching { val iconBitmap = URL(url).openStream().use { @@ -145,12 +181,12 @@ class DownloadListAdapter( holder.title.visibility = View.GONE holder.subtitle.visibility = View.GONE - pendingDownload.mediaDisplayType?.let { + pendingDownload.metadata.mediaDisplayType?.let { holder.title.text = it holder.title.visibility = View.VISIBLE } - pendingDownload.mediaDisplaySource?.let { + pendingDownload.metadata.mediaDisplaySource?.let { holder.subtitle.text = it holder.subtitle.visibility = View.VISIBLE } @@ -165,13 +201,38 @@ class DownloadListAdapter( } pendingDownload.outputFile?.let { - val file = File(it) - if (!file.exists()) { - Toast.makeText(holder.view.context, activity.translation["file_not_found_toast"], Toast.LENGTH_SHORT).show() + fun showFileNotFound() { + Toast.makeText(holder.view.context, SharedContext.translation["download_manager_activity.file_not_found_toast"], Toast.LENGTH_SHORT).show() + } + + val uri = Uri.parse(it) + val fileType = runCatching { + if (uri.scheme == "content") { + activity.contentResolver.openInputStream(uri)?.use { input -> + FileType.fromInputStream(input) + } ?: run { + showFileNotFound() + return@setOnClickListener + } + } else { + val file = File(it) + if (!file.exists()) { + showFileNotFound() + return@setOnClickListener + } + FileType.fromFile(file) + } + }.onFailure { exception -> + Logger.error("Failed to open file", exception) + }.getOrDefault(FileType.UNKNOWN) + if (fileType == FileType.UNKNOWN) { + showFileNotFound() return@setOnClickListener } + val intent = Intent(Intent.ACTION_VIEW) - intent.setDataAndType(Uri.parse(it), FileType.fromFile(File(it)).mimeType) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + intent.setDataAndType(uri, fileType.mimeType) holder.view.context.startActivity(intent) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt index 0efc0427..2ba92310 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt @@ -17,8 +17,8 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import me.rhunk.snapenhance.BuildConfig import me.rhunk.snapenhance.R -import me.rhunk.snapenhance.bridge.TranslationWrapper import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper import me.rhunk.snapenhance.download.data.PendingDownload class DownloadManagerActivity : Activity() { @@ -133,7 +133,7 @@ class DownloadManagerActivity : Activity() { if (lastVisibleItemPosition == fetchedDownloadTasks.size - 1 && !isLoading) { isLoading = true - SharedContext.downloadTaskManager.queryTasks(fetchedDownloadTasks.last().id, filter = listFilter).forEach { + SharedContext.downloadTaskManager.queryTasks(fetchedDownloadTasks.last().downloadId, filter = listFilter).forEach { fetchedDownloadTasks.add(it.value) adapter?.notifyItemInserted(fetchedDownloadTasks.size - 1) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt index d602009f..dcc16bbb 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt @@ -7,13 +7,15 @@ import android.view.View import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams import android.widget.Button +import android.widget.LinearLayout +import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.Constants.VIEW_INJECTED_CODE import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.features.impl.spying.MessageLogger +import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.ui.menu.AbstractMenu -import me.rhunk.snapenhance.ui.menu.ViewAppearanceHelper class ChatActionMenu : AbstractMenu() { @@ -23,7 +25,7 @@ class ChatActionMenu : AbstractMenu() { return false } - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "DiscouragedApi") fun inject(viewGroup: ViewGroup) { val parent = viewGroup.parent.parent as ViewGroup if (wasInjectedView(parent)) return @@ -41,17 +43,64 @@ class ChatActionMenu : AbstractMenu() { ) } + val defaultGap = viewGroup.resources.getDimensionPixelSize( + viewGroup.resources.getIdentifier( + "default_gap", + "dimen", + Constants.SNAPCHAT_PACKAGE_NAME + ) + ) + + val chatActionMenuItemMargin = viewGroup.resources.getDimensionPixelSize( + viewGroup.resources.getIdentifier( + "chat_action_menu_item_margin", + "dimen", + Constants.SNAPCHAT_PACKAGE_NAME + ) + ) + + val actionMenuItemHeight = viewGroup.resources.getDimensionPixelSize( + viewGroup.resources.getIdentifier( + "action_menu_item_height", + "dimen", + Constants.SNAPCHAT_PACKAGE_NAME + ) + ) + + val buttonContainer = LinearLayout(viewGroup.context).apply layout@{ + orientation = LinearLayout.VERTICAL + layoutParams = MarginLayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + ViewAppearanceHelper.applyTheme(this@layout, parent.width, true) + setMargins(chatActionMenuItemMargin, 0, chatActionMenuItemMargin, defaultGap) + } + } + val injectButton = { button: Button -> - ViewAppearanceHelper.applyTheme(button, parent.width, hasRadius = true) + if (buttonContainer.childCount > 0) { + buttonContainer.addView(View(viewGroup.context).apply { + layoutParams = MarginLayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + height = 1 + } + setBackgroundColor(0x1A000000) + }) + } + + ViewAppearanceHelper.applyTheme(button, parent.width, true) with(button) { layoutParams = MarginLayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { - setMargins(40, 0, 40, 15) + height = actionMenuItemHeight + defaultGap } - parent.addView(this) + buttonContainer.addView(this) } } @@ -91,5 +140,7 @@ class ChatActionMenu : AbstractMenu() { } }) } + + parent.addView(buttonContainer) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt index beff9ada..55a1a466 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt @@ -1,7 +1,6 @@ package me.rhunk.snapenhance.ui.menu.impl import android.annotation.SuppressLint -import android.app.AlertDialog import android.content.Context import android.content.DialogInterface import android.content.res.Resources @@ -32,8 +31,8 @@ import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload import me.rhunk.snapenhance.features.impl.spying.StealthMode import me.rhunk.snapenhance.features.impl.tweaks.AntiAutoSave +import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.ui.menu.AbstractMenu -import me.rhunk.snapenhance.ui.menu.ViewAppearanceHelper import me.rhunk.snapenhance.util.snap.BitmojiSelfie import java.net.HttpURLConnection import java.net.URL @@ -73,7 +72,7 @@ class FriendFeedInfoMenu : AbstractMenu() { val finalIcon = icon context.runOnUiThread { val addedTimestamp: Long = profile.addedTimestamp.coerceAtLeast(profile.reverseAddedTimestamp) - val builder = AlertDialog.Builder(context.mainActivity) + val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) builder.setIcon(finalIcon) builder.setTitle(profile.displayName ?: profile.username) @@ -168,7 +167,7 @@ class FriendFeedInfoMenu : AbstractMenu() { //alert dialog - val builder = AlertDialog.Builder(context.mainActivity) + val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) builder.setTitle(context.translation["conversation_preview.title"]) builder.setMessage(messageBuilder.toString()) builder.setPositiveButton( @@ -200,10 +199,20 @@ class FriendFeedInfoMenu : AbstractMenu() { return BitmapDrawable(context.resources, bitmap) } - private fun getCurrentConversationId(): Pair { + private fun getCurrentConversationInfo(): Pair { val messaging = context.feature(Messaging::class) val focusedConversationTargetUser: String? = messaging.lastFetchConversationUserUUID?.toString() + //mapped conversation fetch (may not work with legacy sc versions) + messaging.lastFetchGroupConversationUUID?.let { + context.database.getFriendFeedInfoByConversationId(it.toString())?.let { friendFeedInfo -> + val participantSize = friendFeedInfo.participantsSize + return it.toString() to if (participantSize == 1) focusedConversationTargetUser else null + } + throw IllegalStateException("No conversation found") + } + + //old conversation fetch val conversationId = if (messaging.lastFetchConversationUUID == null && focusedConversationTargetUser != null) { val conversation: UserConversationLink = context.database.getDMConversationIdFromUserId(focusedConversationTargetUser) ?: throw IllegalStateException("No conversation found") conversation.client_conversation_id!!.trim().lowercase() @@ -211,7 +220,7 @@ class FriendFeedInfoMenu : AbstractMenu() { messaging.lastFetchConversationUUID.toString() } - return Pair(conversationId, focusedConversationTargetUser) + return conversationId to focusedConversationTargetUser } private fun createToggleFeature(viewConsumer: ((View) -> Unit), text: String, isChecked: () -> Boolean, toggle: (Boolean) -> Unit) { @@ -234,7 +243,7 @@ class FriendFeedInfoMenu : AbstractMenu() { val friendFeedMenuOptions = context.config.options(ConfigProperty.FRIEND_FEED_MENU_BUTTONS) if (friendFeedMenuOptions.none { it.value }) return - val (conversationId, targetUser) = getCurrentConversationId() + val (conversationId, targetUser) = getCurrentConversationInfo() if (!context.config.bool(ConfigProperty.ENABLE_FRIEND_FEED_MENU_BAR)) { //preview button diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt index da716a43..d787e682 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt @@ -28,14 +28,6 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar context.resources.getString(context.resources.getIdentifier("new_chat", "string", Constants.SNAPCHAT_PACKAGE_NAME)) } - private val fetchConversationHooks = mutableSetOf() - - private fun unhookFetchConversation() { - fetchConversationHooks.let { - it.removeIf { hook -> hook.unhook() ; true} - } - } - @SuppressLint("ResourceType") override fun asyncOnActivityCreate() { friendFeedInfoMenu.context = context @@ -86,19 +78,25 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar } //TODO: inject in group chat menus - if (viewGroup.id == actionSheetContainer && childView.id == actionMenu) { + if (viewGroup.id == actionSheetContainer && childView.id == actionMenu && messaging.lastFetchGroupConversationUUID != null) { val injectedLayout = LinearLayout(childView.context).apply { orientation = LinearLayout.VERTICAL gravity = Gravity.BOTTOM layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) addView(childView) + addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) {} + override fun onViewDetachedFromWindow(v: View) { + messaging.lastFetchGroupConversationUUID = null + } + }) } - fun injectView() { - val viewList = mutableListOf() + val viewList = mutableListOf() + context.runOnUiThread { friendFeedInfoMenu.inject(injectedLayout) { view -> view.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - setMargins(0, 10, 0, 10) + setMargins(0, 3, 0, 3) } viewList.add(view) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt index 39221abb..ecf26179 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt @@ -11,7 +11,7 @@ import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.ui.menu.AbstractMenu -import me.rhunk.snapenhance.ui.menu.ViewAppearanceHelper.applyTheme +import me.rhunk.snapenhance.ui.ViewAppearanceHelper.applyTheme @SuppressLint("DiscouragedApi") class OperaContextActionMenu : AbstractMenu() { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt index a3b7d720..365f2844 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt @@ -18,7 +18,7 @@ import me.rhunk.snapenhance.config.impl.ConfigStateSelection import me.rhunk.snapenhance.config.impl.ConfigStateValue import me.rhunk.snapenhance.config.impl.ConfigStringValue import me.rhunk.snapenhance.ui.menu.AbstractMenu -import me.rhunk.snapenhance.ui.menu.ViewAppearanceHelper +import me.rhunk.snapenhance.ui.ViewAppearanceHelper class SettingsMenu : AbstractMenu() { @SuppressLint("ClickableViewAccessibility") @@ -48,14 +48,14 @@ class SettingsMenu : AbstractMenu() { if (property.disableValueLocalization) { it } else { - context.translation["option.property." + property.translationKey + "." + it] + context.translation[property.getOptionTranslationKey(it)] } } }) } val textEditor: ((String) -> Unit) -> Unit = { updateValue -> - val builder = AlertDialog.Builder(context.mainActivity!!) + val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!) builder.setTitle(propertyName) val input = EditText(context.androidContext) @@ -121,13 +121,13 @@ class SettingsMenu : AbstractMenu() { updateLocalizedText(button, property.valueContainer.value()) button.setOnClickListener {_ -> - val builder = AlertDialog.Builder(context.mainActivity!!) + val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!) builder.setTitle(propertyName) builder.setSingleChoiceItems( property.valueContainer.keys().toTypedArray().map { if (property.disableValueLocalization) it - else context.translation["option.property." + property.translationKey + "." + it] + else context.translation[property.getOptionTranslationKey(it)] }.toTypedArray(), property.valueContainer.keys().indexOf(property.valueContainer.value()) ) { _, which -> @@ -148,7 +148,7 @@ class SettingsMenu : AbstractMenu() { updateButtonText(button, "(${property.valueContainer.value().count { it.value }})") button.setOnClickListener {_ -> - val builder = AlertDialog.Builder(context.mainActivity!!) + val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!) builder.setTitle(propertyName) val sortedStates = property.valueContainer.value().toSortedMap() @@ -156,7 +156,7 @@ class SettingsMenu : AbstractMenu() { builder.setMultiChoiceItems( sortedStates.toSortedMap().map { if (property.disableValueLocalization) it.key - else context.translation["option.property." + property.translationKey + "." + it.key] + else context.translation[property.getOptionTranslationKey(it.key)] }.toTypedArray(), sortedStates.map { it.value }.toBooleanArray() ) { _, which, isChecked -> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt new file mode 100644 index 00000000..32cc5db0 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt @@ -0,0 +1,111 @@ +package me.rhunk.snapenhance.ui.spoof + +import android.app.Activity +import android.content.res.ColorStateList +import android.os.Bundle +import android.text.InputType +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.Switch +import android.widget.TextView +import me.rhunk.snapenhance.R +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.config.ConfigCategory +import me.rhunk.snapenhance.config.impl.ConfigIntegerValue +import me.rhunk.snapenhance.config.impl.ConfigStateValue +import me.rhunk.snapenhance.config.impl.ConfigStringValue +import me.rhunk.snapenhance.ui.ItemHelper + +class DeviceSpooferActivity: Activity() { + private val itemHelper = ItemHelper(this) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + SharedContext.ensureInitialized(this) + setContentView(R.layout.device_spoofer_activity) + + findViewById(R.id.title).text = "Device Spoofer" + findViewById(R.id.back_button).setOnClickListener { finish() } + val propertyListLayout = findViewById(R.id.spoof_property_list) + + SharedContext.config.entries().filter { it.key.category == ConfigCategory.DEVICE_SPOOFER }.forEach { (property, value) -> + val configItem = layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false) + + val propertyName = SharedContext.translation["property.${property.translationKey}.name"] + + fun addSeparator() { + //add separator + propertyListLayout.addView(View(this).apply { + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1) + setBackgroundColor(getColor(R.color.tertiaryBackground)) + }) + } + + configItem.findViewById(R.id.name).text = propertyName + configItem.findViewById(R.id.description).also { + it.text = SharedContext.translation["property.${property.translationKey}.description"] + it.visibility = if (it.text.isEmpty()) View.GONE else View.VISIBLE + } + + fun addValueView(view: View) { + configItem.findViewById(R.id.value).addView(view) + } + + when (value) { + is ConfigStateValue -> { + val switch = Switch(this) + switch.isChecked = value.value() + switch.trackTintList = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_checked) + ), + intArrayOf( + switch.highlightColor, + getColor(R.color.tertiaryBackground) + ) + ) + switch.setOnCheckedChangeListener { _, isChecked -> + value.writeFrom(isChecked.toString()) + } + configItem.setOnClickListener { switch.toggle() } + addValueView(switch) + } + is ConfigStringValue, is ConfigIntegerValue -> { + val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false).also { + it.text = value.value().toString() + } + configItem.setOnClickListener { + if (value is ConfigIntegerValue) { + itemHelper.askForValue(property, InputType.TYPE_CLASS_NUMBER) { + try { + value.writeFrom(it) + textView.text = value.value().toString() + } catch (e: NumberFormatException) { + itemHelper.longToast(SharedContext.translation["config_activity.invalid_number_toast"], this) + } + } + return@setOnClickListener + } + itemHelper.askForValue(property, InputType.TYPE_CLASS_TEXT) { + value.writeFrom(it) + textView.text = value.value().toString() + } + } + addValueView(textView) + } + } + + propertyListLayout.addView(configItem) + addSeparator() + } + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onBackPressed() { + super.onBackPressed() + finish() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt new file mode 100644 index 00000000..50369c27 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt @@ -0,0 +1,5 @@ +package me.rhunk.snapenhance.util + +import android.content.Intent + +typealias ActivityResultCallback = (requestCode: Int, resultCode: Int, data: Intent?) -> Unit \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt new file mode 100644 index 00000000..bb4c9f32 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt @@ -0,0 +1,13 @@ +package me.rhunk.snapenhance.util + +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ApplicationInfoFlags +import android.os.Build + +fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int) = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getApplicationInfo(packageName, ApplicationInfoFlags.of(flags.toLong())) + } else { + @Suppress("DEPRECATION") + getApplicationInfo(packageName, flags) + } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt index a07d604e..d397e21a 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt @@ -20,6 +20,7 @@ import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.database.objects.FriendFeedInfo import me.rhunk.snapenhance.database.objects.FriendInfo +import me.rhunk.snapenhance.util.getApplicationInfoCompat import me.rhunk.snapenhance.util.protobuf.ProtoReader import me.rhunk.snapenhance.util.snap.EncryptionHelper import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper @@ -190,7 +191,7 @@ class MessageExporter( runCatching { ZipFile( - context.androidContext.packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA).publicSourceDir + context.androidContext.packageManager.getApplicationInfoCompat(BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA).publicSourceDir ).use { apkFile -> //export rawinflate.js apkFile.getEntry("assets/web/rawinflate.js").let { entry -> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt index eb1c3a3d..964da8b8 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.FileType +import me.rhunk.snapenhance.download.data.SplitMediaAssetType import me.rhunk.snapenhance.util.download.RemoteMediaResolver import me.rhunk.snapenhance.util.protobuf.ProtoReader import java.io.ByteArrayInputStream @@ -15,9 +16,6 @@ import java.io.InputStream import java.util.concurrent.Executors import java.util.zip.ZipInputStream -enum class MediaType { - ORIGINAL, OVERLAY -} object MediaDownloaderHelper { fun getMessageMediaInfo(protoReader: ProtoReader, contentType: ContentType, isArroyo: Boolean): ProtoReader? { @@ -32,7 +30,7 @@ object MediaDownloaderHelper { } } - fun downloadMediaFromReference(mediaReference: ByteArray, decryptionCallback: (InputStream) -> InputStream): Map { + fun downloadMediaFromReference(mediaReference: ByteArray, decryptionCallback: (InputStream) -> InputStream): Map { val inputStream: InputStream = RemoteMediaResolver.downloadBoltMedia(mediaReference) ?: throw FileNotFoundException("Unable to get media key. Check the logs for more info") val content = decryptionCallback(inputStream).readBytes() val fileType = FileType.fromByteArray(content) @@ -55,10 +53,10 @@ object MediaDownloaderHelper { } videoData ?: throw FileNotFoundException("Unable to find video file in zip file") overlayData ?: throw FileNotFoundException("Unable to find overlay file in zip file") - return mapOf(MediaType.ORIGINAL to videoData, MediaType.OVERLAY to overlayData) + return mapOf(SplitMediaAssetType.ORIGINAL to videoData, SplitMediaAssetType.OVERLAY to overlayData) } - return mapOf(MediaType.ORIGINAL to content) + return mapOf(SplitMediaAssetType.ORIGINAL to content) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt index 3b77b96e..13d7ff54 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt @@ -8,7 +8,6 @@ import android.media.MediaDataSource import android.media.MediaMetadataRetriever import me.rhunk.snapenhance.data.FileType import java.io.File -import kotlin.math.roundToInt object PreviewUtils { fun createPreview(data: ByteArray, isVideo: Boolean): Bitmap? { diff --git a/app/src/main/res/layout/device_spoofer_activity.xml b/app/src/main/res/layout/device_spoofer_activity.xml new file mode 100644 index 00000000..d2d37271 --- /dev/null +++ b/app/src/main/res/layout/device_spoofer_activity.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 03d5df0e..00000000 --- a/build.gradle +++ /dev/null @@ -1,10 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -plugins { - id 'com.android.application' version '8.0.2' apply false - id 'com.android.library' version '8.0.2' apply false - id 'org.jetbrains.kotlin.android' version '1.8.21' apply false -} - -tasks.register('clean', Delete) { - delete rootProject.buildDir -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..fd8ca944 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,8 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.kotlinAndroid) apply false +} + +true // Needed to make the Suppress annotation work for the plugins block \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..e7e7f3b5 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,36 @@ +[versions] +agp = "8.1.0" +junit = "4.13.2" +kotlin = "1.8.21" +kotlinx-coroutines-android = "1.7.2" +kotlin-reflect = "1.8.21" +recyclerview = "1.3.0" +gson = "2.10.1" +ffmpeg-kit = "5.1.LTS" +osmdroid-android = "6.1.16" +okhttp = "5.0.0-alpha.11" +dexlib2 = "2.5.2" +androidx-documentfile = "1.1.0-alpha01" + + +[libraries] +coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" } +junit = { module = "junit:junit", version.ref = "junit" } +kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin-reflect" } +recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } +ffmpeg-kit = { group = "com.arthenica", name = "ffmpeg-kit-full-gpl", version.ref = "ffmpeg-kit" } +osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid-android" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +dexlib2 = { group = "org.smali", name = "dexlib2", version.ref = "dexlib2" } +androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "androidx-documentfile" } + + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + + +[bundles] + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 63c83be0..93de0496 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri May 12 21:23:16 CEST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/mapper/.gitignore b/mapper/.gitignore new file mode 100644 index 00000000..d1638636 --- /dev/null +++ b/mapper/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/mapper/build.gradle.kts b/mapper/build.gradle.kts new file mode 100644 index 00000000..4de72c40 --- /dev/null +++ b/mapper/build.gradle.kts @@ -0,0 +1,21 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + id("com.android.library") + alias(libs.plugins.kotlinAndroid) +} + +android { + namespace = "me.rhunk.snapenhance.mapper" + compileSdk = 33 + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.gson) + implementation(libs.coroutines) + implementation(libs.dexlib2) + testImplementation(libs.junit) +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/AbstractClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/AbstractClassMapper.kt new file mode 100644 index 00000000..d1496a9f --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/AbstractClassMapper.kt @@ -0,0 +1,9 @@ +package me.rhunk.snapmapper + +import kotlin.reflect.KClass + +abstract class AbstractClassMapper( + vararg val dependsOn: KClass = arrayOf() +) { + abstract fun run(context: MapperContext) +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/Mapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/Mapper.kt new file mode 100644 index 00000000..ae8b1409 --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/Mapper.kt @@ -0,0 +1,90 @@ +package me.rhunk.snapmapper + +import com.google.gson.JsonObject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.jf.dexlib2.Opcodes +import org.jf.dexlib2.dexbacked.DexBackedDexFile +import org.jf.dexlib2.iface.ClassDef +import java.io.BufferedInputStream +import java.io.InputStream +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import kotlin.reflect.KClass + +class Mapper( + private vararg val mappers: KClass = arrayOf() +) { + private val classes = mutableListOf() + fun loadApk(path: String) { + val apkFile = ZipFile(path) + val apkEntries = apkFile.entries().toList() + + fun readClass(stream: InputStream) = runCatching { + classes.addAll( + DexBackedDexFile.fromInputStream(Opcodes.getDefault(), BufferedInputStream(stream)).classes + ) + }.onFailure { + throw Throwable("Failed to load dex file", it) + } + + fun filterDexClasses(name: String) = name.startsWith("classes") && name.endsWith(".dex") + + apkEntries.firstOrNull { it.name.endsWith("lspatch/origin.apk") }?.let { origin -> + val originApk = ZipInputStream(apkFile.getInputStream(origin)) + var nextEntry = originApk.nextEntry + while (nextEntry != null) { + if (filterDexClasses(nextEntry.name)) { + readClass(originApk) + } + originApk.closeEntry() + nextEntry = originApk.nextEntry + } + return + } + + apkEntries.toList().filter { filterDexClasses(it.name) }.forEach { + readClass(apkFile.getInputStream(it)) + } + } + + fun start(): JsonObject { + val mappers = mappers.map { it.java.constructors.first().newInstance() as AbstractClassMapper } + val context = MapperContext(classes.associateBy { it.type }) + + runBlocking { + withContext(Dispatchers.IO) { + val finishedJobs = mutableListOf>() + val dependentsMappers = mappers.filter { it.dependsOn.isNotEmpty() } + + fun onJobFinished(mapper: AbstractClassMapper) { + finishedJobs.add(mapper.javaClass) + + dependentsMappers.filter { it -> + !finishedJobs.contains(it.javaClass) && + it.dependsOn.all { + finishedJobs.contains(it.java) + } + }.forEach { + launch { + it.run(context) + onJobFinished(it) + } + } + } + + mappers.forEach { mapper -> + if (mapper.dependsOn.isNotEmpty()) return@forEach + launch { + mapper.run(context) + onJobFinished(mapper) + } + } + } + } + + return context.exportToJson() + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/MapperContext.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/MapperContext.kt new file mode 100644 index 00000000..729d2547 --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/MapperContext.kt @@ -0,0 +1,58 @@ +package me.rhunk.snapmapper + +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import org.jf.dexlib2.iface.ClassDef + +class MapperContext( + private val classMap: Map +) { + val classes: Collection + get() = classMap.values + + fun getClass(name: String?): ClassDef? { + if (name == null) return null + return classMap[name] + } + + fun getClass(name: CharSequence?): ClassDef? { + if (name == null) return null + return classMap[name.toString()] + } + + private val mappings = mutableMapOf() + + fun addMapping(key: String, vararg array: Pair) { + mappings[key] = array.toMap() + } + + fun addMapping(key: String, value: String) { + mappings[key] = value + } + + fun getStringMapping(key: String): String? { + return mappings[key] as? String + } + + fun getMapMapping(key: String): Map<*, *>? { + return mappings[key] as? Map<*, *> + } + + fun exportToJson(): JsonObject { + val gson = GsonBuilder().setPrettyPrinting().create() + val json = JsonObject() + for ((key, value) in mappings) { + when (value) { + is String -> json.addProperty(key, value) + is Map<*, *> -> { + val obj = JsonObject() + for ((k, v) in value) { + obj.add(k.toString(), gson.toJsonTree(v)) + } + json.add(key, obj) + } + } + } + return json + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/ext/DexClassDef.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/ext/DexClassDef.kt new file mode 100644 index 00000000..4ace60ee --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/ext/DexClassDef.kt @@ -0,0 +1,23 @@ +package me.rhunk.snapmapper.ext + +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.iface.ClassDef + +fun ClassDef.isEnum(): Boolean = accessFlags and AccessFlags.ENUM.value != 0 +fun ClassDef.isAbstract(): Boolean = accessFlags and AccessFlags.ABSTRACT.value != 0 +fun ClassDef.isFinal(): Boolean = accessFlags and AccessFlags.FINAL.value != 0 + +fun ClassDef.hasStaticConstructorString(string: String): Boolean = methods.any { + it.name == "" && it.implementation?.findConstString(string) == true +} + +fun ClassDef.hasConstructorString(string: String): Boolean = methods.any { + it.name == "" && it.implementation?.findConstString(string) == true +} + +fun ClassDef.getStaticConstructor() = methods.firstOrNull { + it.name == "" +} + +fun ClassDef.getClassName() = type.replaceFirst("L", "").replaceFirst(";", "") +fun ClassDef.getSuperClassName() = superclass?.replaceFirst("L", "")?.replaceFirst(";", "") diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/ext/DexMethod.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/ext/DexMethod.kt new file mode 100644 index 00000000..c5e46b6f --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/ext/DexMethod.kt @@ -0,0 +1,44 @@ +package me.rhunk.snapmapper.ext + +import org.jf.dexlib2.iface.MethodImplementation +import org.jf.dexlib2.iface.instruction.formats.Instruction21c +import org.jf.dexlib2.iface.instruction.formats.Instruction22c +import org.jf.dexlib2.iface.reference.FieldReference +import org.jf.dexlib2.iface.reference.StringReference + +fun MethodImplementation.findConstString(string: String, contains: Boolean = false): Boolean = instructions.filterIsInstance(Instruction21c::class.java).any { + (it.reference as? StringReference)?.string?.let { str -> + if (contains) { + str.contains(string) + } else { + str == string + } + } == true +} + +fun MethodImplementation.getAllConstStrings(): List = instructions.filterIsInstance().mapNotNull { + it.reference as? StringReference +}.map { + it.string +} + +fun MethodImplementation.searchNextFieldReference(constString: String, contains: Boolean = false): FieldReference? = this.instructions.let { + var found = false + for (instruction in it) { + if (instruction is Instruction21c && instruction.reference is StringReference) { + val str = (instruction.reference as StringReference).string + if (if (contains) str.contains(constString) else str == constString) { + found = true + } + } + + if (!found) continue + + if (instruction is Instruction22c && + instruction.reference is FieldReference + ) { + return@let (instruction.reference as FieldReference) + } + } + null +} diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/BCryptClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/BCryptClassMapper.kt new file mode 100644 index 00000000..1d0dee10 --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/BCryptClassMapper.kt @@ -0,0 +1,34 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.getStaticConstructor +import me.rhunk.snapmapper.ext.isFinal +import org.jf.dexlib2.iface.instruction.formats.ArrayPayload + +class BCryptClassMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + for (clazz in context.classes) { + if (!clazz.isFinal()) continue + + val isBcryptClass = clazz.getStaticConstructor()?.let { constructor -> + constructor.implementation?.instructions?.filterIsInstance()?.any { it.arrayElements.size == 18 && it.arrayElements[0] == 608135816 } + } + + if (isBcryptClass == true) { + val hashMethod = clazz.methods.first { + it.parameterTypes.size == 2 && + it.parameterTypes[0] == "Ljava/lang/String;" && + it.parameterTypes[1] == "Ljava/lang/String;" && + it.returnType == "Ljava/lang/String;" + } + + context.addMapping("BCrypt", + "class" to clazz.type.replace("L", "").replace(";", ""), + "hashMethod" to hashMethod.name + ) + return + } + } + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/CallbackMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/CallbackMapper.kt new file mode 100644 index 00000000..9f42b643 --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/CallbackMapper.kt @@ -0,0 +1,27 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.getClassName +import me.rhunk.snapmapper.ext.getSuperClassName +import me.rhunk.snapmapper.ext.isFinal + +class CallbackMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + val callbackClasses = context.classes.filter { clazz -> + if (clazz.superclass == null) return@filter false + + val superclassName = clazz.getSuperClassName()!! + if (!superclassName.endsWith("Callback") || superclassName.endsWith("\$Callback")) return@filter false + + val superClass = context.getClass(clazz.superclass) ?: return@filter false + if (superClass.isFinal()) return@filter false + + superClass.virtualMethods.any { it.name == "onError" } + }.map { + it.getSuperClassName()!!.substringAfterLast("/") to it.getClassName() + } + + context.addMapping("callbacks", *callbackClasses.toTypedArray()) + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/DefaultMediaItemMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/DefaultMediaItemMapper.kt new file mode 100644 index 00000000..84320e05 --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/DefaultMediaItemMapper.kt @@ -0,0 +1,21 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.isAbstract + +class DefaultMediaItemMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + for (clazz in context.classes) { + val superClass = context.getClass(clazz.superclass) ?: continue + + if (!superClass.isAbstract() || superClass.interfaces.isEmpty() || superClass.interfaces[0] != "Ljava/lang/Comparable;") continue + if (clazz.methods.none { it.returnType == "Landroid/net/Uri;" }) continue + + val constructorParameters = clazz.directMethods.firstOrNull { it.name == "" }?.parameterTypes ?: continue + if (constructorParameters.size < 6 || constructorParameters[5] != "J") continue + + context.addMapping("DefaultMediaItem", clazz.type.replace("L", "").replace(";", "")) + } + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/EnumMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/EnumMapper.kt new file mode 100644 index 00000000..41e3e478 --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/EnumMapper.kt @@ -0,0 +1,72 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.getClassName +import me.rhunk.snapmapper.ext.getStaticConstructor +import me.rhunk.snapmapper.ext.hasStaticConstructorString +import me.rhunk.snapmapper.ext.isEnum +import org.jf.dexlib2.Opcode +import org.jf.dexlib2.iface.Method +import org.jf.dexlib2.iface.instruction.formats.Instruction21c +import org.jf.dexlib2.iface.reference.FieldReference +import org.jf.dexlib2.iface.reference.StringReference + +class EnumMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + var enumQualityLevel : String? = null + val enums = mutableListOf>() + + for (enumClass in context.classes) { + if (!enumClass.isEnum()) continue + + if (enumQualityLevel == null && enumClass.hasStaticConstructorString("LEVEL_MAX")) { + enumQualityLevel = enumClass.getClassName() + } + + if (enumClass.interfaces.isEmpty()) continue + + //check if it's a config enum + val serializableInterfaceClass = context.getClass(enumClass.interfaces.first()) ?: continue + if (serializableInterfaceClass.methods.none {it.name == "getName" && it.returnType == "Ljava/lang/String;" }) continue + + //find the method which returns the enum name + val getEnumMethod = enumClass.virtualMethods.firstOrNull { context.getClass(it.returnType)?.isEnum() == true } ?: continue + + //search for constant field instruction sget-object + + fun getFirstFieldReference21c(opcode: Opcode, method: Method) = method.implementation!!.instructions.firstOrNull { + it.opcode == opcode && it is Instruction21c + }.let { it as? Instruction21c }?.let { + it.reference as? FieldReference + } + + val fieldReference = getFirstFieldReference21c(Opcode.SGET_OBJECT, getEnumMethod) ?: + getFirstFieldReference21c(Opcode.SGET_OBJECT,enumClass.directMethods.first { it.name == "" }) ?: continue + + //search field name in the class + val enumClassListEnum = context.getClass(fieldReference.definingClass) ?: continue + + enumClassListEnum.getStaticConstructor()?.let { constructor -> + var lastEnumClassName = "" + constructor.implementation!!.instructions.forEach { + if (it.opcode == Opcode.CONST_STRING) { + lastEnumClassName = ((it as Instruction21c).reference as StringReference).string + return@forEach + } + + if (it.opcode == Opcode.SPUT_OBJECT && it is Instruction21c) { + val field = it.reference as? FieldReference ?: return@forEach + if (field.name != fieldReference.name || field.type != fieldReference.type) return@forEach + + enums.add(lastEnumClassName to enumClass.getClassName()) + } + } + } + } + + context.addMapping("EnumQualityLevel", enumQualityLevel!!) + + context.addMapping("enums", *enums.sortedBy { it.first }.toTypedArray()) + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/FriendsFeedEventDispatcherMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/FriendsFeedEventDispatcherMapper.kt new file mode 100644 index 00000000..ca5cf4ea --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/FriendsFeedEventDispatcherMapper.kt @@ -0,0 +1,27 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.findConstString +import me.rhunk.snapmapper.ext.getClassName + + +class FriendsFeedEventDispatcherMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + for (clazz in context.classes) { + if (clazz.methods.count { it.name == "onClickFeed" || it.name == "onItemLongPress" } != 2) continue + val onItemLongPress = clazz.methods.first { it.name == "onItemLongPress" } + val viewHolderContainerClass = context.getClass(onItemLongPress.parameterTypes[0]) ?: continue + + val viewModelField = viewHolderContainerClass.fields.firstOrNull { field -> + val typeClass = context.getClass(field.type) ?: return@firstOrNull false + typeClass.methods.firstOrNull {it.name == "toString"}?.implementation?.findConstString("FriendFeedItemViewModel", contains = true) == true + }?.name ?: continue + + context.addMapping("FriendsFeedEventDispatcher", + "class" to clazz.getClassName(), + "viewModelField" to viewModelField + ) + } + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/MediaQualityLevelProviderMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/MediaQualityLevelProviderMapper.kt new file mode 100644 index 00000000..19ab42b6 --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/MediaQualityLevelProviderMapper.kt @@ -0,0 +1,24 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.isAbstract +import org.jf.dexlib2.AccessFlags + +class MediaQualityLevelProviderMapper : AbstractClassMapper(EnumMapper::class) { + override fun run(context: MapperContext) { + val mediaQualityLevelClass = context.getStringMapping("EnumQualityLevel") ?: return + + for (clazz in context.classes) { + if (!clazz.isAbstract()) continue + if (clazz.fields.none { it.accessFlags and AccessFlags.TRANSIENT.value != 0 }) continue + + clazz.methods.firstOrNull { it.returnType == "L$mediaQualityLevelClass;" }?.let { + context.addMapping("MediaQualityLevelProvider", + "class" to clazz.type.replace("L", "").replace(";", ""), + "method" to it.name + ) + } + } + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/OperaPageViewControllerMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/OperaPageViewControllerMapper.kt new file mode 100644 index 00000000..6f67484b --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/OperaPageViewControllerMapper.kt @@ -0,0 +1,52 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.hasConstructorString +import me.rhunk.snapmapper.ext.hasStaticConstructorString +import me.rhunk.snapmapper.ext.isAbstract +import me.rhunk.snapmapper.ext.isEnum + +class OperaPageViewControllerMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + for (clazz in context.classes) { + if (!clazz.isAbstract()) continue + if (!clazz.hasConstructorString("OperaPageViewController") || !clazz.hasStaticConstructorString("ad_product_type")) { + continue + } + + val viewStateField = clazz.fields.first { field -> + val fieldClass = context.getClass(field.type) ?: return@first false + fieldClass.isEnum() && fieldClass.hasStaticConstructorString("FULLY_DISPLAYED") + } + + val layerListField = clazz.fields.first { it.type == "Ljava/util/ArrayList;" } + + val onDisplayStateChange = clazz.methods.first { + if (it.returnType != "V" || it.parameterTypes.size != 1) return@first false + val firstParameterType = context.getClass(it.parameterTypes[0]) ?: return@first false + //check if the class contains a field with the enumViewStateClass type + firstParameterType.fields.any { field -> + field.type == viewStateField.type + } + } + + val onDisplayStateChangeGesture = clazz.methods.first { + if (it.returnType != "V" || it.parameterTypes.size != 2) return@first false + val firstParameterType = context.getClass(it.parameterTypes[0]) ?: return@first false + val secondParameterType = context.getClass(it.parameterTypes[1]) ?: return@first false + firstParameterType.isEnum() && secondParameterType.isEnum() + } + + context.addMapping("OperaPageViewController", + "class" to clazz.type.replace("L", "").replace(";", ""), + "viewStateField" to viewStateField.name, + "layerListField" to layerListField.name, + "onDisplayStateChange" to onDisplayStateChange.name, + "onDisplayStateChangeGesture" to onDisplayStateChangeGesture.name + ) + + return + } + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlatformAnalyticsCreatorMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlatformAnalyticsCreatorMapper.kt new file mode 100644 index 00000000..ee340f5d --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlatformAnalyticsCreatorMapper.kt @@ -0,0 +1,23 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.findConstString +import me.rhunk.snapmapper.ext.getStaticConstructor +import me.rhunk.snapmapper.ext.isEnum + +class PlatformAnalyticsCreatorMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + for (clazz in context.classes) { + val firstConstructor = clazz.directMethods.firstOrNull { it.name == "" } ?: continue + // 47 is the number of parameters of the constructor + // it may change in future versions + if (firstConstructor.parameters.size != 47) continue + val firstParameterClass = context.getClass(firstConstructor.parameterTypes[0]) ?: continue + if (!firstParameterClass.isEnum()) continue + if (firstParameterClass.getStaticConstructor()?.implementation?.findConstString("IN_APP_NOTIFICATION") != true) continue + + context.addMapping("PlatformAnalyticsCreator", clazz.type.replace("L", "").replace(";", "")) + } + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlusSubscriptionMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlusSubscriptionMapper.kt new file mode 100644 index 00000000..91d038c5 --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/PlusSubscriptionMapper.kt @@ -0,0 +1,28 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.findConstString + +class PlusSubscriptionMapper : AbstractClassMapper(){ + override fun run(context: MapperContext) { + for (clazz in context.classes) { + if (clazz.directMethods.filter { it.name == "" }.none { + it.parameters.size == 4 && + it.parameterTypes[0] == "I" && + it.parameterTypes[1] == "I" && + it.parameterTypes[2] == "J" && + it.parameterTypes[3] == "J" + }) continue + + val isPlusSubscriptionInfoClass = clazz.virtualMethods.firstOrNull { it.name == "toString" }?.implementation?.let { + it.findConstString("SubscriptionInfo", contains = true) && it.findConstString("expirationTimeMillis", contains = true) + } + + if (isPlusSubscriptionInfoClass == true) { + context.addMapping("SubscriptionInfoClass", clazz.type.replace("L", "").replace(";", "")) + return + } + } + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/ScCameraSettingsMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/ScCameraSettingsMapper.kt new file mode 100644 index 00000000..d473a4df --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/ScCameraSettingsMapper.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.findConstString +import me.rhunk.snapmapper.ext.getStaticConstructor +import me.rhunk.snapmapper.ext.isEnum + +class ScCameraSettingsMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + for (clazz in context.classes) { + val firstConstructor = clazz.directMethods.firstOrNull { it.name == "" } ?: continue + if (firstConstructor.parameterTypes.size < 27) continue + val firstParameter = context.getClass(firstConstructor.parameterTypes[0]) ?: continue + if (!firstParameter.isEnum() || firstParameter.getStaticConstructor()?.implementation?.findConstString("CONTINUOUS_PICTURE") != true) continue + + context.addMapping("ScCameraSettings", clazz.type.replace("L", "").replace(";", "")) + } + } +} \ No newline at end of file diff --git a/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/StoryBoostStateMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/StoryBoostStateMapper.kt new file mode 100644 index 00000000..b6ee421f --- /dev/null +++ b/mapper/src/main/kotlin/me/rhunk/snapmapper/impl/StoryBoostStateMapper.kt @@ -0,0 +1,23 @@ +package me.rhunk.snapmapper.impl + +import me.rhunk.snapmapper.AbstractClassMapper +import me.rhunk.snapmapper.MapperContext +import me.rhunk.snapmapper.ext.findConstString +import me.rhunk.snapmapper.ext.getStaticConstructor +import me.rhunk.snapmapper.ext.isEnum + +class StoryBoostStateMapper : AbstractClassMapper() { + override fun run(context: MapperContext) { + for (clazz in context.classes) { + val firstConstructor = clazz.directMethods.firstOrNull { it.name == "" } ?: continue + if (firstConstructor.parameters.size != 3) continue + if (firstConstructor.parameterTypes[1] != "J" || firstConstructor.parameterTypes[2] != "J") continue + + val storyBoostEnumClass = context.getClass(firstConstructor.parameterTypes[0]) ?: continue + if (!storyBoostEnumClass.isEnum()) continue + if (storyBoostEnumClass.getStaticConstructor()?.implementation?.findConstString("NeedSubscriptionCannotSubscribe") != true) continue + + context.addMapping("StoryBoostStateClass", clazz.type.replace("L", "").replace(";", "")) + } + } +} \ No newline at end of file diff --git a/mapper/src/test/kotlin/android/util/Log.kt b/mapper/src/test/kotlin/android/util/Log.kt new file mode 100644 index 00000000..cae88175 --- /dev/null +++ b/mapper/src/test/kotlin/android/util/Log.kt @@ -0,0 +1,27 @@ +package android.util + +object Log { + @JvmStatic + fun d(tag: String, msg: String): Int { + println("[$tag] $msg") + return 0 + } + + @JvmStatic + fun e(tag: String, msg: String): Int { + println("[$tag] $msg") + return 0 + } + + @JvmStatic + fun i(tag: String, msg: String): Int { + println("[$tag] $msg") + return 0 + } + + @JvmStatic + fun v(tag: String, msg: String): Int { + println("[$tag] $msg") + return 0 + } +} \ No newline at end of file diff --git a/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt b/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt new file mode 100644 index 00000000..604c6566 --- /dev/null +++ b/mapper/src/test/kotlin/me/rhunk/snapenhance/mapper/tests/TestMappings.kt @@ -0,0 +1,33 @@ +package me.rhunk.snapenhance.mapper.tests + +import com.google.gson.GsonBuilder +import me.rhunk.snapmapper.Mapper +import me.rhunk.snapmapper.impl.* +import org.junit.Test +import java.io.File + + +class TestMappings { + @Test + fun testMappings() { + val mapper = Mapper( + BCryptClassMapper::class, + CallbackMapper::class, + DefaultMediaItemMapper::class, + MediaQualityLevelProviderMapper::class, + EnumMapper::class, + OperaPageViewControllerMapper::class, + PlatformAnalyticsCreatorMapper::class, + PlusSubscriptionMapper::class, + ScCameraSettingsMapper::class, + StoryBoostStateMapper::class, + FriendsFeedEventDispatcherMapper::class + ) + + val gson = GsonBuilder().setPrettyPrinting().create() + val apkFile = File(System.getenv("SNAPCHAT_APK")!!) + mapper.loadApk(apkFile.absolutePath) + val result = mapper.start() + println("Mappings: ${gson.toJson(result)}") + } +} diff --git a/settings.gradle b/settings.gradle.kts similarity index 82% rename from settings.gradle rename to settings.gradle.kts index e06b8971..ee241784 100644 --- a/settings.gradle +++ b/settings.gradle.kts @@ -1,10 +1,12 @@ pluginManagement { repositories { - gradlePluginPortal() google() mavenCentral() + gradlePluginPortal() } } + +@Suppress("UnstableApiUsage") dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -12,5 +14,7 @@ dependencyResolutionManagement { mavenCentral() } } + rootProject.name = "SnapEnhance" -include ':app' +include(":app") +include(":mapper") \ No newline at end of file