diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index ca200db..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -RevengeXposed \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7643783..0def1f7 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,121 +1,10 @@ + diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 7b3006b..639c779 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -13,7 +13,6 @@ - diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index e34606c..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index c22b6fa..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml deleted file mode 100644 index f8051a6..0000000 --- a/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 74dd639..2ccd954 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,10 +1,7 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ac52bda..7293f39 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,14 +6,14 @@ plugins { android { namespace = "io.github.revenge.xposed" - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "io.github.revenge.xposed" minSdk = 24 - targetSdk = 35 - versionCode = 1202 - versionName = "1.2.2" + targetSdk = 36 + versionCode = 1400 + versionName = "1.4.0" } buildTypes { @@ -41,9 +41,9 @@ android { dependencies { compileOnly("de.robv.android.xposed:api:82") - implementation("androidx.core:core-ktx:1.16.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1") - implementation("io.ktor:ktor-client-core:3.1.1") - implementation("io.ktor:ktor-client-cio:3.1.1") + implementation("androidx.core:core-ktx:1.17.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") + implementation("io.ktor:ktor-client-core:3.2.3") + implementation("io.ktor:ktor-client-cio:3.2.3") } diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt b/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt index c22a78d..a2f9a00 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt @@ -9,9 +9,11 @@ class Constants { const val CACHE_DIR = "cache/pyoncord" const val MAIN_SCRIPT_FILE = "bundle.js" + const val LOG_TAG = "Revenge" const val LOADER_NAME = "RevengeXposed" + const val USER_AGENT = "RevengeXposed" } } \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Main.kt b/app/src/main/kotlin/io/github/revenge/xposed/Main.kt index ea99f66..b7afc7f 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/Main.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/Main.kt @@ -11,6 +11,7 @@ import io.github.revenge.xposed.modules.* import io.github.revenge.xposed.modules.appearance.FontsModule import io.github.revenge.xposed.modules.appearance.SysColorsModule import io.github.revenge.xposed.modules.appearance.ThemeModule +import io.github.revenge.xposed.modules.bridge.AdditionalBridgeMethodsModule import io.github.revenge.xposed.modules.bridge.BridgeModule class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit { @@ -18,12 +19,15 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit { private val modules = mutableListOf( HookScriptLoaderModule(), BridgeModule(), + AdditionalBridgeMethodsModule(), + PluginsModule(), UpdaterModule(), FixResourcesModule(), LogBoxModule(), + CacheModule(), FontsModule(), ThemeModule(), - SysColorsModule(), + SysColorsModule() ) init { diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/CacheModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/CacheModule.kt new file mode 100644 index 0000000..54db659 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/CacheModule.kt @@ -0,0 +1,270 @@ +package io.github.revenge.xposed.modules + +import android.content.Context +import android.util.AtomicFile +import io.github.revenge.xposed.Module +import io.github.revenge.xposed.Utils.Log +import io.github.revenge.xposed.modules.bridge.BridgeModule +import java.io.* + +/** + * A module for caching versioned modules finds and blacklist, and assets mappings. + * + * See [ModulesCache] and [AssetsCache] for the cache structure. + * + * ## Methods + * + * ### Modules cache + * + * - `revenge.caches.modules.read() : { blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }, version: number } | null` + * - Reads the modules cache. Returns `null` if the cache file does not exist or is invalid. + * + * - `revenge.caches.modules.write(blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }) : void` + * - Writes the modules cache. + * + * ### Assets cache + * + * - `revenge.caches.assets.read() : { data: { [name: string]: { [type: string]: number } }, version: number } | null` + * - Reads the assets cache. Returns `null` if the cache file does not exist or is invalid. + * + * - `revenge.caches.assets.write(data: { [name: string]: { [type: string]: number } }) : void` + * - Writes the assets cache. + * + * The caches are versioned and tied to the app version code. When the app is updated, + * the caches are invalidated and new cache files are created. + */ +class CacheModule : Module() { + private companion object { + const val CACHE_DIR = "revenge" + + const val MODULES_CACHE_PREFIX = "modules" + const val ASSETS_CACHE_PREFIX = "assets" + } + + private lateinit var modulesCache: ModulesCache + private lateinit var assetsCache: AssetsCache + + override fun onContext(context: Context) = with(context) { + val revengeCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() } + val (_, _, _, versionCode) = getAppInfo() + + val modulesCacheFile = File( + revengeCacheDir, "$MODULES_CACHE_PREFIX.$versionCode" + ).apply { asFile() } + + val assetsCacheFile = File( + revengeCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode" + ).apply { asFile() } + + BridgeModule.registerMethod("revenge.caches.modules.read") { + if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile) + ?.let { + modulesCache = it + it.toMap() + } + } + + BridgeModule.registerMethod("revenge.caches.modules.write") { + val (blacklist, finds) = it + @Suppress("UNCHECKED_CAST") + modulesCache = ModulesCache( + blacklist as ArrayList, finds as HashMap?> + ).apply { saveToFile(modulesCacheFile) } + Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})") + } + + BridgeModule.registerMethod("revenge.caches.assets.read") { + if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile) + ?.let { + assetsCache = it + it.toMap() + } + } + + BridgeModule.registerMethod("revenge.caches.assets.write") { + val (data) = it + @Suppress("UNCHECKED_CAST") + assetsCache = + AssetsCache(data as HashMap>).apply { saveToFile(assetsCacheFile) } + Log.i("Assets cache saved: ${assetsCacheFile.absolutePath} (count: ${data.size})") + } + } +} + +private class CacheVersionMismatchException(expected: Int, actual: Int) : + Throwable("Expected cache version: $expected, but got version: $actual") + +data class ModulesCache( + val blacklist: List, val finds: Map?> +) { + fun saveToFile(file: File) { + val atomic = AtomicFile(file) + var fos: FileOutputStream? = null + try { + fos = atomic.startWrite() + val out = DataOutputStream(BufferedOutputStream(fos)) + + out.writeInt(VERSION) + + // Write blacklist + out.writeInt(blacklist.size) + for (num in blacklist) out.writeInt(num.toInt()) + + // Write finds + out.writeInt(finds.size) + for ((filter, matches) in finds) { + out.writeUTF(filter) + if (matches == null) { + out.writeBoolean(false) // null marker + } else { + out.writeBoolean(true) // not null + out.writeInt(matches.size) + for ((id, flag) in matches) { + out.writeInt(id.toInt()) + out.writeInt(flag.toInt()) + } + } + } + + out.flush() + atomic.finishWrite(fos) + } catch (t: Throwable) { + if (fos != null) atomic.failWrite(fos) + throw t + } + } + + fun toMap(): Map { + val findsMap = finds.mapValues { (_, matches) -> matches?.map { (k, v) -> k to v }?.toMap() } + + return mapOf("blacklist" to blacklist, "finds" to findsMap, "version" to VERSION) + } + + companion object { + const val VERSION = 3 + + fun loadFromFileOrNull(file: File): ModulesCache? { + if (!file.exists() || file.length() <= 0L) return null + val atomic = AtomicFile(file) + try { + DataInputStream(BufferedInputStream(atomic.openRead())).use { input -> + val version = input.readInt() + if (version != VERSION) throw CacheVersionMismatchException(VERSION, version) + + val blacklistSize = input.readInt() + val blacklist = MutableList(blacklistSize) { input.readInt().toDouble() } + + val findsSize = input.readInt() + val finds = mutableMapOf?>() + repeat(findsSize) { + val filter = input.readUTF() + val hasMatches = input.readBoolean() + if (!hasMatches) { + finds[filter] = null + } else { + val mapSize = input.readInt() + val matches = mutableMapOf() + repeat(mapSize) { + val id = input.readInt().toString() + val flag = input.readInt().toDouble() + matches[id] = flag + } + finds[filter] = matches + } + } + + return ModulesCache(blacklist, finds) + } + } catch (e: CacheVersionMismatchException) { + Log.i("Modules cache version mismatch: ${e.message}") + } catch (e: EOFException) { + Log.e("Modules cache corrupt: ${e.message}") + } catch (e: IOException) { + Log.e("Failed to read modules cache: ${e.message}") + } + runCatching { file.delete() } + return null + } + } +} + +data class AssetsCache( + val data: Map> +) { + fun saveToFile(file: File) { + val atomic = AtomicFile(file) + var fos: FileOutputStream? = null + try { + fos = atomic.startWrite() + val out = DataOutputStream(BufferedOutputStream(fos)) + + out.writeInt(VERSION) + + out.writeInt(data.size) + for ((name, mappings) in data) { + out.writeUTF(name) + out.writeInt(mappings.size) + for ((type, id) in mappings) { + out.writeUTF(type) + out.writeInt(id.toInt()) + } + } + + out.flush() + atomic.finishWrite(fos) + } catch (t: Throwable) { + if (fos != null) atomic.failWrite(fos) + throw t + } + } + + fun toMap(): Map { + return mapOf( + "data" to data.mapValues { (_, mappings) -> + mappings.map { (type, id) -> type to id }.toMap() + }, "version" to VERSION + ) + } + + companion object { + const val VERSION = 2 + + fun loadFromFileOrNull(file: File): AssetsCache? { + if (!file.exists() || file.length() <= 0L) return null + val atomic = AtomicFile(file) + try { + DataInputStream(BufferedInputStream(atomic.openRead())).use { input -> + val version = input.readInt() + if (version != VERSION) throw CacheVersionMismatchException(VERSION, version) + + val dataSize = input.readInt() + val data = mutableMapOf>() + + repeat(dataSize) { + val name = input.readUTF() + val mappingsSize = input.readInt() + val mappings = mutableMapOf() + + repeat(mappingsSize) { + val type = input.readUTF() + val id = input.readInt().toDouble() + mappings[type] = id + } + + data[name] = mappings + } + + return AssetsCache(data) + } + } catch (e: CacheVersionMismatchException) { + Log.i("Assets cache version mismatch: ${e.message}") + } catch (e: EOFException) { + Log.e("Assets cache corrupt: ${e.message}") + } catch (e: IOException) { + Log.e("Failed to read assets cache: ${e.message}") + } + runCatching { file.delete() } + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/PluginsModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/PluginsModule.kt new file mode 100644 index 0000000..5948b43 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/PluginsModule.kt @@ -0,0 +1,124 @@ +package io.github.revenge.xposed.modules + +import android.content.Context +import android.util.AtomicFile +import io.github.revenge.xposed.Module +import io.github.revenge.xposed.Utils.Log +import io.github.revenge.xposed.modules.bridge.BridgeModule +import java.io.* + +/** + * Manages the states of plugins, allowing reading and writing of plugin states to a file. + * + * ## Methods + * + * - `revenge.plugins.states.read(): { flags: { [pluginId: string]: number } }` + * - Reads the current plugin states from the file and returns them as a map. + * + * - `revenge.plugins.states.write(flags: { [pluginId: string]: number }): void` + * - Writes the provided plugin states to the file. + */ +class PluginsModule : Module() { + private companion object { + const val DATA_DIR = "revenge/plugins" + const val STATES_FILE = "states" + } + + private lateinit var states: PluginStates + + override fun onContext(context: Context) = with(context) { + val dataDir = File(filesDir, DATA_DIR) + + val statesFile = File( + dataDir, STATES_FILE + ).apply { asFile() } + + BridgeModule.registerMethod("revenge.plugins.states.read") { + if (::states.isInitialized) states.toMap() else + PluginStates.loadFromFileOrNull(statesFile)?.let { + states = it + it.toMap() + } + } + + BridgeModule.registerMethod("revenge.plugins.states.write") { + val (flags) = it + @Suppress("UNCHECKED_CAST") + states = PluginStates( + flags as Map + ).apply { saveToFile(statesFile) } + Log.i("Plugin states saved: ${statesFile.absolutePath}") + } + } +} + +private class UnsupportedPluginStatesVersionException(version: Int) : + Throwable("Unsupported plugin states version: $version") + +data class PluginStates( + val flags: Map +) { + fun saveToFile(file: File) { + val atomic = AtomicFile(file) + var fos: FileOutputStream? = null + try { + fos = atomic.startWrite() + val out = DataOutputStream(BufferedOutputStream(fos)) + + out.writeInt(CURRENT_VERSION) + + // Write flags + out.writeInt(flags.size) + for ((id, flags) in flags) { + out.writeUTF(id) + out.writeInt(flags.toInt()) + } + + out.flush() + atomic.finishWrite(fos) + } catch (t: Throwable) { + if (fos != null) atomic.failWrite(fos) + throw t + } + } + + fun toMap(): Map = mapOf("flags" to flags) + + companion object { + const val CURRENT_VERSION = 1 + + fun loadFromFileOrNull(file: File): PluginStates? { + if (!file.exists() || file.length() <= 0L) return null + val atomic = AtomicFile(file) + + try { + DataInputStream(BufferedInputStream(atomic.openRead())).use { input -> + when (val version = input.readInt()) { + 1 -> return loadV1FromFileOrNull(input) + else -> throw UnsupportedPluginStatesVersionException(version) + } + } + } catch (e: UnsupportedPluginStatesVersionException) { + Log.i(e.message!!) + } catch (e: EOFException) { + Log.e("Plugin states corrupt: ${e.message}") + } catch (e: IOException) { + Log.e("Failed to read plugin states: ${e.message}") + } + + runCatching { file.delete() } + return null + } + + private fun loadV1FromFileOrNull(input: DataInputStream): PluginStates { + val flagsSize = input.readInt() + val flags = mutableMapOf() + + repeat(flagsSize) { + flags[input.readUTF()] = input.readInt().toDouble() + } + + return PluginStates(flags) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt index 59df27d..5356f45 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt @@ -47,9 +47,7 @@ class UpdaterModule : Module() { private lateinit var etag: File companion object { - @Volatile var job: Job? = null - private set private const val TIMEOUT_CACHED = 5000L private const val TIMEOUT = 10000L diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt new file mode 100644 index 0000000..eb252f1 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt @@ -0,0 +1,45 @@ +package io.github.revenge.xposed.modules.bridge + +import android.app.AlertDialog +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import android.widget.Toast +import io.github.revenge.xposed.Module + +class AdditionalBridgeMethodsModule : Module() { + override fun onContext(context: Context) = with(context) { + BridgeModule.registerMethod("revenge.alertError") { + val (error, version) = it + val app = getAppInfo() + val errorString = "$error" + + val clipboard = applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Stack Trace", errorString) + + AlertDialog.Builder(this) + .setTitle("Revenge Error") + .setMessage( + """ + Revenge: $version + ${app.name}: ${app.version} (${app.versionCode}) + Device: ${Build.MANUFACTURER} ${Build.MODEL} + + + """.trimIndent() + errorString + ) + .setPositiveButton("OK") { dialog, _ -> + dialog.dismiss() + } + .setNeutralButton("Copy") { dialog, _ -> + clipboard.setPrimaryClip(clip) + Toast.makeText(applicationContext, "Copied stack trace", Toast.LENGTH_SHORT).show() + dialog.dismiss() + } + .show() + + null + } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index bfc2823..d421a94 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.9.2") + classpath("com.android.tools.build:gradle:8.11.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10") // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c8..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME