diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt b/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt new file mode 100644 index 0000000..839ec4e --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt @@ -0,0 +1,14 @@ +package io.github.revenge.xposed + +class Constants { + companion object { + const val FILES_DIR = "files/pyoncord" + const val CACHE_DIR = "cache/pyoncord" + const val BUNDLE_FILE = "bundle.js" + + + const val LOG_TAG = "Revenge" + + 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 a80f249..52e62d4 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,12 @@ import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.callbacks.XC_LoadPackage +import io.github.revenge.xposed.modules.FontsModule +import io.github.revenge.xposed.modules.LogBoxModule +import io.github.revenge.xposed.modules.SysColorsModule +import io.github.revenge.xposed.modules.ThemeModule +import io.github.revenge.xposed.Utils.Companion.JSON +import io.github.revenge.xposed.Utils.Log import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* @@ -34,12 +40,24 @@ data class LoaderConfig( val customLoadUrl: CustomLoadUrl = CustomLoadUrl() ) -class Main : IXposedHookLoadPackage { +class Main : Module(), IXposedHookLoadPackage { private lateinit var preloadsDir: File private lateinit var bundle: File private lateinit var onActivityCreate: ((Activity) -> Unit) -> Unit private lateinit var httpJob: Deferred + val PRELOADS_DIR = "preloads" + + val ETAG_FILE = "etag.txt" + val CONFIG_FILE = "loader.json" + + val LOADER_NAME = "RevengeXposed" + + val DEFAULT_BUNDLE_URL = "https://github.com/revenge-mod/revenge-bundle/releases/latest/download/revenge.min.js" + + val TARGET_PACKAGE = "com.discord" + val TARGET_ACTIVITY = "$TARGET_PACKAGE.react_activities.ReactActivity" + private val modules = listOf( ThemeModule(), SysColorsModule(), @@ -47,16 +65,16 @@ class Main : IXposedHookLoadPackage { LogBoxModule() ) - private fun buildLoaderJsonString(): String = Json.encodeToString( + private fun getPayloadString(): String = JSON.encodeToString( buildJsonObject { - put("loaderName", "RevengeXposed") + put("loaderName", LOADER_NAME) put("loaderVersion", BuildConfig.VERSION_NAME) - modules.forEach { it.buildJson(this) } + for (module in modules) module.buildPayload(this) } ) override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) { - val reactActivity = classLoader.safeLoadClass("com.discord.react_activities.ReactActivity") + val reactActivity = classLoader.safeLoadClass(TARGET_ACTIVITY) ?: return var activity: Activity? = null @@ -64,8 +82,8 @@ class Main : IXposedHookLoadPackage { reactActivity.hookMethod("onCreate", Bundle::class.java) { before { - activity = it.thisObject as Activity - onActivityCreateCallbacks.forEach { cb -> cb(activity) } + activity = thisObject as Activity + for (cb in onActivityCreateCallbacks) cb(activity) onActivityCreateCallbacks.clear() } } @@ -80,18 +98,17 @@ class Main : IXposedHookLoadPackage { private fun init( param: XC_LoadPackage.LoadPackageParam, ) = with(param) { - val cacheDir = File(appInfo.dataDir, "cache/pyoncord").apply { mkdirs() } - val filesDir = File(appInfo.dataDir, "files/pyoncord").apply { mkdirs() } + val cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() } + val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() } - preloadsDir = File(filesDir, "preloads").apply { mkdirs() } - bundle = File(cacheDir, "bundle.js") + preloadsDir = File(filesDir, PRELOADS_DIR).apply { mkdirs() } + bundle = File(cacheDir, Constants.BUNDLE_FILE) - val etag = File(cacheDir, "etag.txt") - val configFile = File(filesDir, "loader.json") + val etag = File(cacheDir, ETAG_FILE) + val configFile = File(filesDir, CONFIG_FILE) val config = runCatching { - Json { ignoreUnknownKeys = true } - .decodeFromString(configFile.readText()) + JSON.decodeFromString(configFile.readText()) }.getOrDefault(LoaderConfig()) httpJob = MainScope().async(Dispatchers.IO) { @@ -101,13 +118,13 @@ class Main : IXposedHookLoadPackage { install(HttpTimeout) { requestTimeoutMillis = if (bundle.exists()) 5000 else 10000 } - install(UserAgent) { agent = "RevengeXposed" } + install(UserAgent) { agent = Constants.USER_AGENT } } val url = config.customLoadUrl.takeIf { it.enabled }?.url - ?: "https://github.com/revenge-mod/revenge-bundle/releases/latest/download/revenge.min.js" + ?: DEFAULT_BUNDLE_URL - Log.i("Revenge", "Fetching JS bundle from $url") + Log.i("Fetching JS bundle from $url") val response: HttpResponse = client.get(url) { headers { @@ -124,7 +141,7 @@ class Main : IXposedHookLoadPackage { if (e is RedirectResponseException && e.response.status == HttpStatusCode.NotModified ) { - Log.e("Revenge", "Server responded with 304 - no changes") + Log.i("Server responded with 304 - no changes") } else { onActivityCreate { activity -> activity.runOnUiThread { @@ -135,7 +152,7 @@ class Main : IXposedHookLoadPackage { ).show() } } - Log.e("Revenge", "Failed to download bundle", e) + Log.e("Failed to download bundle", e) } } } @@ -146,18 +163,18 @@ class Main : IXposedHookLoadPackage { ).mapNotNull { classLoader.safeLoadClass(it) } .forEach { hookLoadScript(it) } - modules.forEach { it.onInit(param) } + for (module in modules) module.onInit(param) // Fix resource package name mismatch - if (packageName != "com.discord") { + if (packageName != TARGET_PACKAGE) { Resources::class.java.hookMethod( "getIdentifier", String::class.java, String::class.java, String::class.java ) { - before { mh -> - if (mh.args[2] == packageName) mh.args[2] = "com.discord" + before { + if (args[2] == packageName) args[2] = TARGET_PACKAGE } } } @@ -190,7 +207,7 @@ class Main : IXposedHookLoadPackage { }.onFailure { // Bridgeless compatibility File(preloadsDir, "rv_globals_$key.js").apply { - writeText("this[${Json.encodeToString(key)}]=$json") + writeText("this[${JSON.encodeToString(key)}]=$json") XposedBridge.invokeOriginalMethod( loadScriptFromFile, param.thisObject, @@ -204,7 +221,7 @@ class Main : IXposedHookLoadPackage { val patch = object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam) { runBlocking { httpJob.join() } - setGlobalVariable(param, "__PYON_LOADER__", buildLoaderJsonString()) + setGlobalVariable(param, "__PYON_LOADER__", getPayloadString()) preloadsDir.walk() .filter { it.isFile && it.extension == "js" } @@ -224,37 +241,9 @@ class Main : IXposedHookLoadPackage { } } - listOf(loadScriptFromAssets, loadScriptFromFile).forEach { it.hook(patch) } + for (method in listOf(loadScriptFromAssets, loadScriptFromFile)) method.hook(patch) } private fun ClassLoader.safeLoadClass(name: String): Class<*>? = runCatching { loadClass(name) }.getOrNull() - - private fun Class<*>.method( - name: String, - vararg params: Class<*>? - ): java.lang.reflect.Method = - getDeclaredMethod(name, *params).apply { isAccessible = true } - - private fun java.lang.reflect.Method.hook(hook: XC_MethodHook) = - XposedBridge.hookMethod(this, hook) - - private fun Class<*>.hookMethod( - name: String, - vararg params: Class<*>?, - block: MethodHookBuilder.() -> Unit - ) = method(name, *params).hook(MethodHookBuilder().apply(block).build()) - - private class MethodHookBuilder { - private var beforeHook: ((XC_MethodHook.MethodHookParam) -> Unit)? = null - fun before(block: (XC_MethodHook.MethodHookParam) -> Unit) { - beforeHook = block - } - - fun build() = object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - beforeHook?.invoke(param) - } - } - } } \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Module.kt b/app/src/main/kotlin/io/github/revenge/xposed/Module.kt index b0416fd..2103d25 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/Module.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/Module.kt @@ -1,9 +1,117 @@ package io.github.revenge.xposed +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.callbacks.XC_LoadPackage +import java.lang.reflect.Method import kotlinx.serialization.json.JsonObjectBuilder abstract class Module { - open fun buildJson(builder: JsonObjectBuilder) {} + open fun buildPayload(builder: JsonObjectBuilder) {} + open fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) {} + + protected fun Class<*>.method( + name: String, + vararg params: Class<*>? + ): Method = getDeclaredMethod(name, *params).apply { + isAccessible = true + } + + protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = + XposedBridge.hookMethod(this, hook) + + protected fun Class<*>.hookMethod( + name: String, + vararg params: Class<*>?, + block: MethodHookBuilder.() -> Unit + ): XC_MethodHook.Unhook = + method(name, *params).hook(MethodHookBuilder().apply(block).build()) + + protected class MethodHookBuilder { + private var beforeBlock: (HookScope.() -> Unit)? = null + private var afterBlock: (HookScope.() -> Unit)? = null + + fun before(block: HookScope.() -> Unit) { + beforeBlock = block + } + + fun after(block: HookScope.() -> Unit) { + afterBlock = block + } + + fun build(): XC_MethodHook = + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + val b = beforeBlock + if (b != null) { + val scope = + HookScope( + param = param, + callSuper = { p -> + super.beforeHookedMethod(p) + } + ) + scope.b() + } else { + super.beforeHookedMethod(param) + } + } + + override fun afterHookedMethod(param: MethodHookParam) { + val a = afterBlock + if (a != null) { + val scope = + HookScope( + param = param, + callSuper = { p -> + super.afterHookedMethod(p) + } + ) + scope.a() + } else { + super.afterHookedMethod(param) + } + } + } + } + + /** + * Scope object passed to before/after hook blocks. + * + * Provides: + * - Access to the [param] object + * - [proceed] to call the original XC_MethodHook super method + * - Accessors for `thisObject`, `args`, `result`, and `throwable` + * + * @property param The [XC_MethodHook.MethodHookParam] for the current hook. + * @property callSuper Function that calls the super method. + */ + protected class HookScope internal constructor( + val param: XC_MethodHook.MethodHookParam, + private val callSuper: (XC_MethodHook.MethodHookParam) -> Unit + ) { + /** + * Continues with the default XC_MethodHook super behavior. + * Equivalent to calling `super.beforeHookedMethod(param)` or + * `super.afterHookedMethod(param)` depending on the phase. + */ + fun proceed() = callSuper(param) + + val thisObject: Any? get() = param.thisObject + + val args: Array get() = param.args + + var result: Any? + get() = param.result + set(value) { + param.result = value + } + + var throwable: Throwable? + get() = param.throwable + set(value) { + param.throwable = value + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt b/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt new file mode 100644 index 0000000..5dcca36 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt @@ -0,0 +1,16 @@ +package io.github.revenge.xposed + +import kotlinx.serialization.json.Json + +class Utils { + companion object { + val JSON = Json { ignoreUnknownKeys = true } + } + + object Log { + fun e(msg: String) = android.util.Log.e(Constants.LOG_TAG, msg) + fun e(msg: String, throwable: Throwable) = android.util.Log.e(Constants.LOG_TAG, msg, throwable) + fun i(msg: String) = android.util.Log.i(Constants.LOG_TAG, msg) + fun i(msg: String, throwable: Throwable) = android.util.Log.i(Constants.LOG_TAG, msg, throwable) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/revenge/xposed/FontsModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/FontsModule.kt similarity index 79% rename from app/src/main/kotlin/io/github/revenge/xposed/FontsModule.kt rename to app/src/main/kotlin/io/github/revenge/xposed/modules/FontsModule.kt index 6df52a6..686ec31 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/FontsModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/FontsModule.kt @@ -1,7 +1,4 @@ -// credits to janisslsm from his PR: https://github.com/vendetta-mod/VendettaXposed/pull/17 -// hooks are modified function from RN codebase - -package io.github.revenge.xposed +package io.github.revenge.xposed.modules import android.content.res.AssetManager import android.os.Build @@ -9,16 +6,18 @@ import android.graphics.Typeface import android.graphics.Typeface.CustomFallbackBuilder import android.graphics.fonts.Font import android.graphics.fonts.FontFamily -import android.util.Log import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.callbacks.XC_LoadPackage +import io.github.revenge.xposed.Constants +import io.github.revenge.xposed.Module +import io.github.revenge.xposed.Utils.Log +import io.github.revenge.xposed.Utils.Companion.JSON import kotlinx.serialization.Serializable import kotlinx.serialization.json.* import java.io.IOException import java.io.File import kotlinx.coroutines.* - import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* @@ -26,6 +25,7 @@ import io.ktor.client.engine.cio.* import io.ktor.client.statement.* import io.ktor.client.plugins.* import io.ktor.http.* +import java.lang.StringBuilder @Serializable data class FontDefinition( @@ -44,81 +44,15 @@ class FontsModule: Module() { private lateinit var fontsDownloadsDir: File private var fontsAbsPath: String? = null - override fun buildJson(builder: JsonObjectBuilder) { + override fun buildPayload(builder: JsonObjectBuilder) { builder.apply { put("fontPatch", 2) } } override fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) = with (packageParam) { - try { - // Try to hook the new class (280201+) - hookClass(classLoader, "com.facebook.react.views.text.ReactFontManager\$Companion") - } catch (e: Throwable) { - when (e) { - // Hook old class (280200-) - is NoClassDefFoundError, is XposedHelpers.ClassNotFoundError -> hookClass(classLoader, "com.facebook.react.views.text.ReactFontManager") - else -> throw e - } - } - - val fontDefFile = File(appInfo.dataDir, "files/pyoncord/fonts.json") - if (!fontDefFile.exists()) return@with - - val fontDef = try { - val json = Json { ignoreUnknownKeys = true } - json.decodeFromString(fontDefFile.readText()) - } catch (_: Throwable) { return@with } - - fontsDownloadsDir = File(appInfo.dataDir, "files/pyoncord/downloads/fonts").apply { mkdirs() } - fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { mkdirs() } - fontsAbsPath = fontsDir.absolutePath + "/" - - fontsDir.listFiles()?.forEach { file -> - val fileName = file.name - if (!fileName.startsWith(".")) { - val fontName = fileName.split('.')[0] - if (fontDef.main.keys.none { it == fontName }) { - Log.i("Revenge", "Deleting font file: $fileName") - file.delete() - } - } - } - - // These files should be downloaded by the JS side, but oh well - CoroutineScope(Dispatchers.IO).launch { - fontDef.main.keys.map { name -> - async { - val url = fontDef.main.getValue(name) - try { - Log.i("Revenge", "Downloading $name from $url") - val file = File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}") - if (file.exists()) return@async - - val client = HttpClient(CIO) { - install(UserAgent) { agent = "RevengeXposed" } - } - - val response: HttpResponse = client.get(url) - - if (response.status == HttpStatusCode.OK) { - file.writeBytes(response.body()) - } - - return@async - } catch (e: Throwable) { - Log.e("Revenge", "Failed to download fonts ($name from $url)", e) - } - } - }.awaitAll() - } - - return@with - } - - private fun hookClass(classLoader: ClassLoader, className: String) { XposedHelpers.findAndHookMethod( - className, + "com.facebook.react.views.text.ReactFontManager\$Companion", classLoader, "createAssetTypeface", String::class.java, @@ -131,7 +65,58 @@ class FontsModule: Module() { val assetManager: AssetManager = param.args[2] as AssetManager return createAssetTypeface(fontFamilyName, style, assetManager) } - }) + }) + + val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json") + if (!fontDefFile.exists()) return@with + + val fontDef = try { + JSON.decodeFromString(fontDefFile.readText()) + } catch (_: Throwable) { return@with } + + fontsDownloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply { mkdirs() } + fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { mkdirs() } + fontsAbsPath = fontsDir.absolutePath + "/" + + fontsDir.listFiles()?.forEach { file -> + val fileName = file.name + if (!fileName.startsWith(".")) { + val fontName = fileName.split('.')[0] + if (fontDef.main.keys.none { it == fontName }) { + Log.i("Deleting font file: $fileName") + file.delete() + } + } + } + + // These files should be downloaded by the JS side, but oh well + CoroutineScope(Dispatchers.IO).launch { + fontDef.main.keys.map { name -> + async { + val url = fontDef.main.getValue(name) + try { + Log.i("Downloading $name from $url") + val file = File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}") + if (file.exists()) return@async + + val client = HttpClient(CIO) { + install(UserAgent) { agent = Constants.USER_AGENT } + } + + val response: HttpResponse = client.get(url) + + if (response.status == HttpStatusCode.OK) + file.writeBytes(response.body()) + + return@async + } catch (e: Throwable) { + Log.e("Failed to download fonts ($name from $url)", e) + } + } + }.awaitAll() + } + + return@with } private fun createAssetTypefaceWithFallbacks( @@ -152,13 +137,11 @@ class FontsModule: Module() { val family = FontFamily.Builder(font).build() fontFamilies.add(family) } - } catch (e: Throwable) { - // ignore - } + } catch (_: Throwable) {} for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { for (fileExtension in FILE_EXTENSIONS) { - val fileName = java.lang.StringBuilder() + val fileName = StringBuilder() .append(fontRootPath) .append(fontFamilyName) .append(fileExtension) @@ -168,10 +151,10 @@ class FontsModule: Module() { val font = builder.build() val family = FontFamily.Builder(font).build() fontFamilies.add(family) - } catch (e: java.lang.RuntimeException) { + } catch (_: RuntimeException) { // If the typeface asset does not exist, try another extension. continue - } catch (e: IOException) { + } catch (_: IOException) { // If the font asset does not exist, try another extension. continue } @@ -180,9 +163,8 @@ class FontsModule: Module() { } // If there's some problem constructing fonts, fall back to the default behavior. - if (fontFamilies.size == 0) { - return createAssetTypeface(fontFamilyNames[0], style, assetManager) - } + if (fontFamilies.isEmpty()) return createAssetTypeface(fontFamilyNames[0], style, assetManager) + val fallbackBuilder = CustomFallbackBuilder(fontFamilies[0]) for (i in 1 until fontFamilies.size) { fallbackBuilder.addCustomFallback(fontFamilies[i]) @@ -193,11 +175,11 @@ class FontsModule: Module() { } private fun createAssetTypeface( - fontFamilyName_: String, style: Int, assetManager: AssetManager + fontFamilyName: String, style: Int, assetManager: AssetManager ): Typeface? { // This logic attempts to safely check if the frontend code is attempting to use // fallback fonts, and if it is, to use the fallback typeface creation logic. - var fontFamilyName: String = fontFamilyName_ + var fontFamilyName: String = fontFamilyName val fontFamilyNames = fontFamilyName.split(",".toRegex()) .dropLastWhile { it.isEmpty() } @@ -226,15 +208,13 @@ class FontsModule: Module() { if (!file.exists()) throw Exception() return Typeface.createFromFile(file.absolutePath) } - } catch (e: Throwable) { - // ignore - } + } catch (_: Throwable) {} // Lastly, after all those checks above, this is the original RN logic for // getting the typeface. for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { for (fileExtension in FILE_EXTENSIONS) { - val fileName = java.lang.StringBuilder() + val fileName = StringBuilder() .append(fontRootPath) .append(fontFamilyName) .append(extension) @@ -246,7 +226,7 @@ class FontsModule: Module() { Typeface.createFromFile(fileName) else Typeface.createFromAsset(assetManager, fileName) - } catch (e: java.lang.RuntimeException) { + } catch (_: RuntimeException) { // If the typeface asset does not exist, try another extension. continue } diff --git a/app/src/main/kotlin/io/github/revenge/xposed/LogBoxModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt similarity index 91% rename from app/src/main/kotlin/io/github/revenge/xposed/LogBoxModule.kt rename to app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt index 5222031..e58e90a 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/LogBoxModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt @@ -1,14 +1,16 @@ -package io.github.revenge.xposed +package io.github.revenge.xposed.modules import android.app.AlertDialog import android.app.AndroidAppHelper import android.content.Context import android.content.Intent -import android.util.Log import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.callbacks.XC_LoadPackage +import io.github.revenge.xposed.Constants +import io.github.revenge.xposed.Module +import io.github.revenge.xposed.Utils import java.io.File import kotlin.system.exitProcess @@ -59,7 +61,7 @@ class LogBoxModule: Module() { } } } catch (ex: Exception) { - Log.e("Revenge", "Failed to show dev options dialog: $ex") + Utils.Log.e("Failed to show dev options dialog: $ex") alertDialog = null } @@ -73,9 +75,7 @@ class LogBoxModule: Module() { XposedBridge.hookMethod( handleReloadJSMethod, object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - reloadApp() - } + override fun beforeHookedMethod(param: MethodHookParam) = reloadApp() } ) @@ -91,10 +91,8 @@ class LogBoxModule: Module() { reloadApp() } 1 -> { - val bundleFile = File(packageParam.appInfo.dataDir, "cache/pyoncord/bundle.js") - if (bundleFile.exists()) { - bundleFile.delete() - } + val bundleFile = File(packageParam.appInfo.dataDir, "${Constants.CACHE_DIR}/${Constants.BUNDLE_FILE}") + if (bundleFile.exists()) bundleFile.delete() reloadApp() } diff --git a/app/src/main/kotlin/io/github/revenge/xposed/SysColorsModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/SysColorsModule.kt similarity index 86% rename from app/src/main/kotlin/io/github/revenge/xposed/SysColorsModule.kt rename to app/src/main/kotlin/io/github/revenge/xposed/modules/SysColorsModule.kt index 9ea6db3..1b21e6c 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/SysColorsModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/SysColorsModule.kt @@ -1,9 +1,11 @@ -package io.github.revenge.xposed +package io.github.revenge.xposed.modules +import android.R.color import android.app.AndroidAppHelper import android.content.Context import android.os.Build import androidx.core.content.ContextCompat +import io.github.revenge.xposed.Module import kotlinx.serialization.Serializable import kotlinx.serialization.json.* @@ -20,7 +22,7 @@ class SysColorsModule : Module() { private lateinit var context: Context private fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - override fun buildJson(builder: JsonObjectBuilder) { + override fun buildPayload(builder: JsonObjectBuilder) { context = AndroidAppHelper.currentApplication() val accents = arrayOf("accent1", "accent2", "accent3", "neutral1", "neutral2") val shades = arrayOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) @@ -33,7 +35,7 @@ class SysColorsModule : Module() { val colorName = "system_" + accent + "_" + shade val colorResourceId = runCatching { - android.R.color::class.java.getField(colorName).getInt(null) + color::class.java.getField(colorName).getInt(null) }.getOrElse { 0 } add(convertToColor(colorResourceId)) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/ThemeModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/ThemeModule.kt similarity index 79% rename from app/src/main/kotlin/io/github/revenge/xposed/ThemeModule.kt rename to app/src/main/kotlin/io/github/revenge/xposed/modules/ThemeModule.kt index a32d550..505f9d9 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/ThemeModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/ThemeModule.kt @@ -1,18 +1,18 @@ -package io.github.revenge.xposed +package io.github.revenge.xposed.modules import android.content.Context -import android.graphics.Color import android.content.res.Resources import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.callbacks.XC_LoadPackage import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import java.io.File import kotlinx.serialization.json.* +import androidx.core.graphics.toColorInt +import io.github.revenge.xposed.Constants +import io.github.revenge.xposed.Module +import io.github.revenge.xposed.Utils.Companion.JSON @Serializable data class Author( @@ -41,17 +41,21 @@ class ThemeModule : Module() { private var theme: Theme? = null private val rawColorMap = mutableMapOf() + val THEME_FILE = "current-theme.json" + @ExperimentalSerializationApi - override fun buildJson(builder: JsonObjectBuilder) { + override fun buildPayload(builder: JsonObjectBuilder) { builder.apply { put("hasThemeSupport", true) if (theme != null) - put("storedTheme", Json.encodeToJsonElement(theme!!)) + put("storedTheme", JSON.encodeToJsonElement(theme!!)) else put("storedTheme", null) } } + private fun String.fromScreamingSnakeToCamelCase() = this.split("_").joinToString("") { it -> it.lowercase().replaceFirstChar { it.uppercase() } } + override fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) { param = packageParam theme = getTheme() @@ -66,21 +70,13 @@ class ThemeModule : Module() { } private fun getTheme(): Theme? { - val filesDir = File(param.appInfo.dataDir, "files").apply { mkdirs() } - val pyonDir = File(filesDir, "pyoncord").apply { mkdirs() } - val themeFile = File(pyonDir, "current-theme.json") - - val legacyThemeFile = File(filesDir, "vendetta_theme.json") - if (legacyThemeFile.isValidish() && !themeFile.isValidish()) { - legacyThemeFile.copyTo(themeFile, overwrite = true) - } - + val themeFile = File(param.appInfo.dataDir, "${Constants.FILES_DIR}/${THEME_FILE}") if (!themeFile.isValidish()) return null return try { val themeText = themeFile.readText() - Json { ignoreUnknownKeys = true }.decodeFromString(themeText) - } catch (e: Exception) { null } + JSON.decodeFromString(themeText) + } catch (_: Exception) { null } } fun hookTheme() { @@ -99,7 +95,7 @@ class ThemeModule : Module() { // Apply semanticColors theme.data.semanticColors?.forEach { (key, value) -> // TEXT_NORMAL -> getTextNormal - val methodName = "get${key.split("_").joinToString("") { it.lowercase().replaceFirstChar { it.uppercase() } }}" + val methodName = "get${key.fromScreamingSnakeToCamelCase()}" value.forEachIndexed { index, v -> when (index) { 0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v)) @@ -144,22 +140,19 @@ class ThemeModule : Module() { // Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy val alpha = hexString.substring(7, 9) val rrggbb = hexString.substring(1, 7) - Color.parseColor("#$alpha$rrggbb") - } else Color.parseColor(hexString) + "#$alpha$rrggbb".toColorInt() + } else hexString.toColorInt() } private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) { try { themeClass.getDeclaredMethod(methodName).let { method -> - // Log.i("Hooking $methodName -> ${themeValue.toString(16)}") XposedBridge.hookMethod(method, object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam) { param.result = themeValue } }) } - } catch (ex: NoSuchMethodException) { - // do nothing - } + } catch (_: NoSuchMethodException) {} } } \ No newline at end of file