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