fix(ci): fix compilation errors from package rename, restore missing imports and helpers, fix 404 bundle URL
This commit is contained in:
parent
109f7d5775
commit
43f024969d
9 changed files with 247 additions and 146 deletions
|
|
@ -2,10 +2,12 @@ package GoonXposed.xposed
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
import de.robv.android.xposed.XC_MethodHook
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
|
import de.robv.android.xposed.XposedHelpers
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -18,13 +20,18 @@ data class AppInfo(
|
||||||
val versionCode: Long,
|
val versionCode: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
abstract class Module {
|
/**
|
||||||
|
* Base class for every Xposed side module.
|
||||||
|
*
|
||||||
|
* The [Main] loader forwards the various [XC_LoadPackage] lifecycle callbacks to every
|
||||||
|
* registered [Module] instance. Modules may override any subset of the hooks below.
|
||||||
|
*/
|
||||||
|
open class Module {
|
||||||
/**
|
/**
|
||||||
* Builds a JSON payload to be injected into the JavaScript context.
|
* Builds a JSON payload to be injected into the JavaScript context.
|
||||||
*/
|
*/
|
||||||
@Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.")
|
@Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.")
|
||||||
open fun buildPayload(builder: JsonObjectBuilder) {
|
open fun buildPayload(builder: JsonObjectBuilder) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called during Zygote initialization.
|
* Called during Zygote initialization.
|
||||||
|
|
@ -63,121 +70,151 @@ abstract class Module {
|
||||||
*/
|
*/
|
||||||
open fun onActivity(activity: Activity) {}
|
open fun onActivity(activity: Activity) {}
|
||||||
|
|
||||||
protected fun Context.getAppInfo(): AppInfo {
|
open fun onResume(activity: Activity) {}
|
||||||
val pInfo = packageManager.getPackageInfo(packageName, 0)
|
|
||||||
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pInfo.longVersionCode
|
|
||||||
else @Suppress("DEPRECATION") pInfo.versionCode.toLong()
|
|
||||||
|
|
||||||
return AppInfo(
|
open fun onPause(activity: Activity) {}
|
||||||
packageManager.getApplicationLabel(applicationInfo).toString(),
|
|
||||||
packageName,
|
|
||||||
pInfo.versionName ?: versionCode.toString(),
|
|
||||||
versionCode,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun File.asDir() {
|
|
||||||
if (!this.isDirectory) this.delete()
|
|
||||||
this.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun File.asFile() {
|
|
||||||
if (!this.isFile) this.deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun Class<*>.method(
|
|
||||||
name: String, vararg params: Class<*>?
|
|
||||||
): Method = getDeclaredMethod(name, *params).apply {
|
|
||||||
isAccessible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun ClassLoader.safeLoadClass(name: String): Class<*>? = runCatching { loadClass(name) }.getOrNull()
|
|
||||||
|
|
||||||
protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = XposedBridge.hookMethod(this, hook)
|
|
||||||
|
|
||||||
protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook =
|
|
||||||
hook(MethodHookBuilder().apply(block).build())
|
|
||||||
|
|
||||||
|
protected fun Context.getAppInfo(): AppInfo = (this as Context).getAppInfo()
|
||||||
|
protected fun File.asDir(): File = (this as File).asDir()
|
||||||
|
protected fun File.asFile(): File = (this as File).asFile()
|
||||||
|
protected fun ClassLoader.safeLoadClass(name: String): Class<*>? = (this as ClassLoader).safeLoadClass(name)
|
||||||
|
protected fun Class<*>.method(name: String, vararg parameterTypes: Class<*>?): Method = (this as Class<*>).method(name, *parameterTypes)
|
||||||
|
protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = (this as Method).hook(hook)
|
||||||
|
protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook = (this as Method).hook(block)
|
||||||
protected fun Class<*>.hookMethod(
|
protected fun Class<*>.hookMethod(
|
||||||
name: String, vararg params: Class<*>?, block: MethodHookBuilder.() -> Unit
|
name: String,
|
||||||
): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build())
|
vararg parameterTypes: Class<*>?,
|
||||||
|
block: MethodHookBuilder.() -> Unit
|
||||||
|
): XC_MethodHook.Unhook = (this as Class<*>).hookMethod(name, *parameterTypes, block = block)
|
||||||
|
}
|
||||||
|
|
||||||
protected class MethodHookBuilder {
|
/**
|
||||||
private var beforeBlock: (HookScope.() -> Unit)? = null
|
* Runtime wrapper around an [XC_MethodHook.MethodHookParam] used as the receiver of every
|
||||||
private var afterBlock: (HookScope.() -> Unit)? = null
|
* `before`/`after` hook block, exposing [thisObject], [args], [result] and [param] directly.
|
||||||
|
*/
|
||||||
|
class HookScope(
|
||||||
|
val param: XC_MethodHook.MethodHookParam,
|
||||||
|
private val proceed: ((XC_MethodHook.MethodHookParam) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
fun proceed() {
|
||||||
|
proceed?.invoke(param)
|
||||||
|
}
|
||||||
|
|
||||||
fun before(block: HookScope.() -> Unit) {
|
val thisObject: Any?
|
||||||
beforeBlock = block
|
get() = param.thisObject
|
||||||
|
|
||||||
|
val args: Array<Any?>
|
||||||
|
get() = param.args
|
||||||
|
|
||||||
|
var result: Any?
|
||||||
|
get() = param.result
|
||||||
|
set(value) {
|
||||||
|
param.result = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun after(block: HookScope.() -> Unit) {
|
var throwable: Throwable?
|
||||||
afterBlock = block
|
get() = param.throwable
|
||||||
|
set(value) {
|
||||||
|
param.throwable = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSL builder for lazily constructed [XC_MethodHook] instances.
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* val hook = MethodHookBuilder().run {
|
||||||
|
* before { result = null }
|
||||||
|
* after { Log.i("done") }
|
||||||
|
* build()
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class MethodHookBuilder {
|
||||||
|
private val beforeCallbacks = mutableListOf<HookScope.() -> Unit>()
|
||||||
|
private val afterCallbacks = mutableListOf<HookScope.() -> Unit>()
|
||||||
|
|
||||||
|
fun before(callback: HookScope.() -> Unit) {
|
||||||
|
beforeCallbacks += callback
|
||||||
|
}
|
||||||
|
|
||||||
|
fun after(callback: HookScope.() -> Unit) {
|
||||||
|
afterCallbacks += callback
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): XC_MethodHook = object : XC_MethodHook() {
|
||||||
|
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||||
|
val scope = HookScope(param = param, proceed = { p -> super.beforeHookedMethod(p) })
|
||||||
|
beforeCallbacks.forEach { scope.run(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(): XC_MethodHook = object : XC_MethodHook() {
|
override fun afterHookedMethod(param: MethodHookParam) {
|
||||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
val scope = HookScope(param = param, proceed = { p -> super.afterHookedMethod(p) })
|
||||||
val b = beforeBlock
|
afterCallbacks.forEach { scope.run(it) }
|
||||||
if (b != null) {
|
|
||||||
val scope = HookScope(
|
|
||||||
param = param, proceed = { 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, proceed = { p ->
|
|
||||||
super.afterHookedMethod(p)
|
|
||||||
})
|
|
||||||
scope.a()
|
|
||||||
} else {
|
|
||||||
super.afterHookedMethod(param)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
companion object {
|
||||||
* Scope object passed to before/after hook blocks.
|
fun from(block: MethodHookBuilder.() -> Unit): XC_MethodHook = MethodHookBuilder().run {
|
||||||
*
|
block()
|
||||||
* Provides:
|
build()
|
||||||
* - 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 proceed Function that calls the super method.
|
|
||||||
*/
|
|
||||||
protected class HookScope internal constructor(
|
|
||||||
val param: XC_MethodHook.MethodHookParam, private val proceed: (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() = proceed(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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ClassLoader.safeLoadClass(name: String): Class<*>? = try {
|
||||||
|
loadClass(name)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Class<*>.method(name: String, vararg parameterTypes: Class<*>?): Method =
|
||||||
|
XposedHelpers.findMethodExact(this, name, *parameterTypes)
|
||||||
|
|
||||||
|
fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = XposedBridge.hookMethod(this, hook)
|
||||||
|
|
||||||
|
fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook {
|
||||||
|
val builder = MethodHookBuilder()
|
||||||
|
builder.block()
|
||||||
|
return XposedBridge.hookMethod(this, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Class<*>.hookMethod(
|
||||||
|
name: String,
|
||||||
|
vararg parameterTypes: Class<*>?,
|
||||||
|
block: MethodHookBuilder.() -> Unit
|
||||||
|
): XC_MethodHook.Unhook {
|
||||||
|
val method = method(name, *parameterTypes)
|
||||||
|
return method.hook(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun File.asDir(): File {
|
||||||
|
if (!exists() || !isDirectory) mkdirs()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun File.asFile(): File {
|
||||||
|
parentFile?.mkdirs()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getAppInfo(): AppInfo {
|
||||||
|
val label = applicationInfo.loadLabel(packageManager).toString()
|
||||||
|
val packageInfo = try {
|
||||||
|
packageManager.getPackageInfo(packageName, 0)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val versionCode = if (packageInfo != null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
|
||||||
|
else @Suppress("DEPRECATION") packageInfo.versionCode.toLong()
|
||||||
|
} else 0L
|
||||||
|
|
||||||
|
return AppInfo(
|
||||||
|
name = label,
|
||||||
|
packageName = packageName,
|
||||||
|
version = packageInfo?.versionName ?: "",
|
||||||
|
versionCode = versionCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import android.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.asDir
|
||||||
|
import GoonXposed.xposed.asFile
|
||||||
|
import GoonXposed.xposed.getAppInfo
|
||||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.res.Resources
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
|
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
|
import GoonXposed.xposed.hookMethod
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
|
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,15 @@ import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
|
import GoonXposed.xposed.HookScope
|
||||||
import GoonXposed.xposed.HookStateHolder
|
import GoonXposed.xposed.HookStateHolder
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.asDir
|
||||||
|
import GoonXposed.xposed.asFile
|
||||||
|
import GoonXposed.xposed.hook
|
||||||
|
import GoonXposed.xposed.method
|
||||||
|
import GoonXposed.xposed.safeLoadClass
|
||||||
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,13 @@ import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.BuildConfig
|
import GoonXposed.xposed.BuildConfig
|
||||||
import GoonXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
|
import GoonXposed.xposed.MethodHookBuilder
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Companion.JSON
|
import GoonXposed.xposed.Utils.Companion.JSON
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.hook
|
||||||
|
import GoonXposed.xposed.method
|
||||||
|
import GoonXposed.xposed.safeLoadClass
|
||||||
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.asFile
|
||||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,11 @@ object UpdaterModule : Module() {
|
||||||
private const val ETAG_FILE = "etag.txt"
|
private const val ETAG_FILE = "etag.txt"
|
||||||
private const val CONFIG_FILE = "loader.json"
|
private const val CONFIG_FILE = "loader.json"
|
||||||
|
|
||||||
|
val isCustomUrlEnabled: Boolean
|
||||||
|
get() = if (::config.isInitialized) config.customLoadUrl.enabled else false
|
||||||
|
|
||||||
private const val DEFAULT_BUNDLE_URL =
|
private const val DEFAULT_BUNDLE_URL =
|
||||||
"https://github.com/OTKUSteyler/GoonCord/releases/download/latest/gooncord.js"
|
"https://github.com/OTKUSteyler/GoonCord/releases/latest/download/gooncord.js"
|
||||||
|
|
||||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||||
// store app data dir for later checks (LogBox settings live under files/logbox)
|
// store app data dir for later checks (LogBox settings live under files/logbox)
|
||||||
|
|
@ -77,7 +80,7 @@ object UpdaterModule : Module() {
|
||||||
}.getOrDefault(LoaderConfig())
|
}.getOrDefault(LoaderConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadScript(activity: Activity? = null): Job = scope.launch {
|
fun downloadScript(activity: Activity? = null, showUpdateDialog: Boolean = true): Job = scope.launch {
|
||||||
try {
|
try {
|
||||||
// Respect LogBox setting to disable bundle injections.
|
// Respect LogBox setting to disable bundle injections.
|
||||||
// If the file "files/logbox/LOGBOX_SETTINGS" contains the flag bundleInjectionDisabled=true,
|
// If the file "files/logbox/LOGBOX_SETTINGS" contains the flag bundleInjectionDisabled=true,
|
||||||
|
|
@ -129,7 +132,6 @@ object UpdaterModule : Module() {
|
||||||
|
|
||||||
Log.i("Bundle updated (${bytes.size} bytes)")
|
Log.i("Bundle updated (${bytes.size} bytes)")
|
||||||
|
|
||||||
// This is a retry, so we show a dialog
|
|
||||||
if (activity != null) {
|
if (activity != null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
AlertDialog.Builder(activity).setTitle("GoonCord Update Successful")
|
AlertDialog.Builder(activity).setTitle("GoonCord Update Successful")
|
||||||
|
|
@ -139,6 +141,22 @@ object UpdaterModule : Module() {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}.setCancelable(false).show()
|
}.setCancelable(false).show()
|
||||||
}
|
}
|
||||||
|
} else if (showUpdateDialog) {
|
||||||
|
val act = lastActivity?.get()
|
||||||
|
act?.runOnUiThread {
|
||||||
|
AlertDialog.Builder(act)
|
||||||
|
.setTitle("GoonCord Update Downloaded")
|
||||||
|
.setMessage("A reload is required for changes to take effect.")
|
||||||
|
.setPositiveButton("Reload") { dialog, _ ->
|
||||||
|
reloadApp()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.setNegativeButton("Later") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
.setCancelable(false)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import GoonXposed.xposed.Constants
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Companion.JSON
|
import GoonXposed.xposed.Utils.Companion.JSON
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
|
import GoonXposed.xposed.asDir
|
||||||
|
import GoonXposed.xposed.asFile
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@ import android.content.res.Resources
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
|
import GoonXposed.xposed.MethodHookBuilder
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Companion.JSON
|
import GoonXposed.xposed.Utils.Companion.JSON
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.asFile
|
||||||
|
import GoonXposed.xposed.hook
|
||||||
|
import GoonXposed.xposed.method
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
|
|
@ -43,7 +46,7 @@ object ThemesModule : Module() {
|
||||||
|
|
||||||
private const val THEME_FILE = "current-theme.json"
|
private const val THEME_FILE = "current-theme.json"
|
||||||
|
|
||||||
|
@Deprecated("This method is deprecated in the parent class")
|
||||||
@ExperimentalSerializationApi
|
@ExperimentalSerializationApi
|
||||||
override fun buildPayload(builder: JsonObjectBuilder) {
|
override fun buildPayload(builder: JsonObjectBuilder) {
|
||||||
builder.apply {
|
builder.apply {
|
||||||
|
|
@ -82,16 +85,23 @@ object ThemesModule : Module() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hookTheme() {
|
fun hookTheme() {
|
||||||
val theme = this.theme ?: return
|
try {
|
||||||
|
val themeManager = runCatching { param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") }.getOrNull()
|
||||||
|
val darkTheme = runCatching { param.classLoader.loadClass("com.discord.theme.DarkTheme") }.getOrNull()
|
||||||
|
?: runCatching { param.classLoader.loadClass("com.discord.theme.DarkerTheme") }.getOrNull()
|
||||||
|
val onyxTheme = runCatching { param.classLoader.loadClass("com.discord.theme.OnyxTheme") }.getOrNull()
|
||||||
|
val lightTheme = runCatching { param.classLoader.loadClass("com.discord.theme.LightTheme") }.getOrNull()
|
||||||
|
|
||||||
val themeManager = param.classLoader.safeLoadClass("com.discord.theme.utils.ColorUtilsKt")
|
hookThemeInternal(themeManager, darkTheme, onyxTheme, lightTheme)
|
||||||
val darkTheme = param.classLoader.safeLoadClass("com.discord.theme.DarkerTheme")
|
} catch (e: Throwable) {
|
||||||
val lightTheme = param.classLoader.safeLoadClass("com.discord.theme.LightTheme")
|
GoonXposed.xposed.Utils.Log.e("Failed to hook theme: ${e.message}")
|
||||||
|
|
||||||
if (themeManager == null || darkTheme == null || lightTheme == null) {
|
|
||||||
Log.e("Failed to load Discord theme classes, skipping theme hooks")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hookThemeInternal(
|
||||||
|
themeManager: Class<*>?, darkTheme: Class<*>?, onyxTheme: Class<*>?, lightTheme: Class<*>?
|
||||||
|
) {
|
||||||
|
val theme = this.theme ?: return
|
||||||
|
|
||||||
// Apply rawColors
|
// Apply rawColors
|
||||||
theme.data.rawColors?.forEach { (key, value) ->
|
theme.data.rawColors?.forEach { (key, value) ->
|
||||||
|
|
@ -103,53 +113,72 @@ object ThemesModule : Module() {
|
||||||
// TEXT_NORMAL -> getTextNormal
|
// TEXT_NORMAL -> getTextNormal
|
||||||
val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
|
val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
|
||||||
value.forEachIndexed { index, v ->
|
value.forEachIndexed { index, v ->
|
||||||
|
val colorInt = hexStringToColorInt(v)
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v))
|
0 -> {
|
||||||
1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v))
|
if (darkTheme != null) hookThemeMethod(darkTheme, methodName, colorInt)
|
||||||
|
if (onyxTheme != null) hookThemeMethod(onyxTheme, methodName, colorInt)
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
if (lightTheme != null) hookThemeMethod(lightTheme, methodName, colorInt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's any rawColors value, hook the color getter
|
// If there's any rawColors value, hook the color getter
|
||||||
if (!theme.data.rawColors.isNullOrEmpty()) {
|
if (!theme.data.rawColors.isNullOrEmpty() && themeManager != null) {
|
||||||
val patch = MethodHookBuilder().run {
|
try {
|
||||||
before {
|
val getColorCompat = runCatching {
|
||||||
val arg1 = args[0]
|
|
||||||
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
|
|
||||||
val name = resources.getResourceEntryName(args[1] as Int)
|
|
||||||
|
|
||||||
if (rawColorMap[name] != null) result = rawColorMap[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
build()
|
|
||||||
}
|
|
||||||
|
|
||||||
listOf(
|
|
||||||
runCatching {
|
|
||||||
themeManager.getDeclaredMethod(
|
themeManager.getDeclaredMethod(
|
||||||
"getColorCompat",
|
"getColorCompat",
|
||||||
Resources::class.java,
|
Resources::class.java,
|
||||||
Int::class.javaPrimitiveType,
|
Int::class.javaPrimitiveType,
|
||||||
Resources.Theme::class.java,
|
Resources.Theme::class.java,
|
||||||
)
|
)
|
||||||
}.getOrNull(),
|
}.getOrNull()
|
||||||
runCatching {
|
|
||||||
|
val getColorCompatLegacy = runCatching {
|
||||||
themeManager.getDeclaredMethod(
|
themeManager.getDeclaredMethod(
|
||||||
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
|
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
|
||||||
)
|
)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
).filterNotNull().forEach { it.hook(patch) }
|
|
||||||
|
val patch = MethodHookBuilder().run {
|
||||||
|
before {
|
||||||
|
try {
|
||||||
|
val arg1 = args[0]
|
||||||
|
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
|
||||||
|
val name = resources.getResourceEntryName(args[1] as Int)
|
||||||
|
|
||||||
|
if (rawColorMap[name] != null) result = rawColorMap[name]
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
|
||||||
|
getColorCompat?.hook(patch)
|
||||||
|
getColorCompatLegacy?.hook(patch)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
|
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
|
||||||
private fun hexStringToColorInt(hexString: String): Int {
|
private fun hexStringToColorInt(hexString: String): Int {
|
||||||
return if (hexString.length == 9) {
|
return try {
|
||||||
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
|
if (hexString.length == 9) {
|
||||||
val alpha = hexString.substring(7, 9)
|
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
|
||||||
val rrggbb = hexString.substring(1, 7)
|
val alpha = hexString.substring(7, 9)
|
||||||
"#$alpha$rrggbb".toColorInt()
|
val rrggbb = hexString.substring(1, 7)
|
||||||
} else hexString.toColorInt()
|
"#$alpha$rrggbb".toColorInt()
|
||||||
|
} else hexString.toColorInt()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
|
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue