feat: refactor
This commit is contained in:
parent
8ce3e63a58
commit
56ce7f674b
8 changed files with 285 additions and 185 deletions
14
app/src/main/kotlin/io/github/revenge/xposed/Constants.kt
Normal file
14
app/src/main/kotlin/io/github/revenge/xposed/Constants.kt
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Unit>
|
||||
|
||||
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<LoaderConfig>(configFile.readText())
|
||||
JSON.decodeFromString<LoaderConfig>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Any?> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
16
app/src/main/kotlin/io/github/revenge/xposed/Utils.kt
Normal file
16
app/src/main/kotlin/io/github/revenge/xposed/Utils.kt
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FontDefinition>(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<FontDefinition>(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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
@ -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<String, Int>()
|
||||
|
||||
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>(theme!!))
|
||||
put("storedTheme", JSON.encodeToJsonElement<Theme>(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<Theme>(themeText)
|
||||
} catch (e: Exception) { null }
|
||||
JSON.decodeFromString<Theme>(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) {}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue