merge changes from KettuXposed and bump to 1.2.1

only issue is not working recovery menu, but who cares?
This commit is contained in:
kmmiio99o 2025-11-21 20:58:00 +01:00
parent 5b510a9552
commit 056d6d81d9
14 changed files with 131 additions and 123 deletions

View file

@ -12,8 +12,8 @@ android {
applicationId = "ShiggyXposed.xposed"
minSdk = 24
targetSdk = 36
versionCode = 1200
versionName = "1.2.0"
versionCode = 1210
versionName = "1.2.1"
}
buildTypes {

View file

@ -35,24 +35,24 @@ object HookStateHolder {
class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
private var hooked = false
private val modules = mutableListOf(
HookScriptLoaderModule(),
BridgeModule(),
AdditionalBridgeMethodsModule(),
private val modules = mutableListOf<Module>(
HookScriptLoaderModule,
BridgeModule,
AdditionalBridgeMethodsModule,
PluginsModule(),
UpdaterModule,
FixResourcesModule(),
BlockDeepLinksTrackingModule(),
BlockCrashReportingModule(),
LogBoxModule(),
CacheModule(),
FontsModule(),
ThemesModule(),
SysColorsModule()
FixResourcesModule,
BlockDeepLinksTrackingModule,
BlockCrashReportingModule,
LogBoxModule,
CacheModule,
FontsModule,
ThemesModule,
SysColorsModule
)
init {
modules.add(PayloadGlobalModule(modules))
modules += PayloadGlobalModule(modules)
}
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {

View file

@ -33,30 +33,30 @@ import java.io.*
* 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 = "Shiggy"
object CacheModule : Module() {
private const val CACHE_DIR = "revenge"
private const val MODULES_CACHE_PREFIX = "modules"
private const val ASSETS_CACHE_PREFIX = "assets"
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 ShiggyCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() }
val revengeCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() }
val (_, _, _, versionCode) = getAppInfo()
val modulesCacheFile = File(
ShiggyCacheDir, "$MODULES_CACHE_PREFIX.$versionCode"
revengeCacheDir, "$MODULES_CACHE_PREFIX.$versionCode"
).apply { asFile() }
val assetsCacheFile = File(
ShiggyCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode"
revengeCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode"
).apply { asFile() }
BridgeModule.registerMethod("Shiggy.caches.modules.read") {
BridgeModule.registerMethod("revenge.caches.modules.read") {
if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile)
?.let {
modulesCache = it
@ -64,7 +64,7 @@ class CacheModule : Module() {
}
}
BridgeModule.registerMethod("Shiggy.caches.modules.write") {
BridgeModule.registerMethod("revenge.caches.modules.write") {
val (blacklist, finds) = it
@Suppress("UNCHECKED_CAST")
modulesCache = ModulesCache(
@ -73,7 +73,7 @@ class CacheModule : Module() {
Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})")
}
BridgeModule.registerMethod("Shiggy.caches.assets.read") {
BridgeModule.registerMethod("revenge.caches.assets.read") {
if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile)
?.let {
assetsCache = it
@ -81,7 +81,7 @@ class CacheModule : Module() {
}
}
BridgeModule.registerMethod("Shiggy.caches.assets.write") {
BridgeModule.registerMethod("revenge.caches.assets.write") {
val (data) = it
@Suppress("UNCHECKED_CAST")
assetsCache =

View file

@ -8,7 +8,7 @@ import ShiggyXposed.xposed.Module
/**
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
*/
class FixResourcesModule : Module() {
object FixResourcesModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
if (packageName != TARGET_PACKAGE) {
Resources::class.java.hookMethod(

View file

@ -9,7 +9,7 @@ import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.HookStateHolder
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.Companion.PRELOADS_DIR
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
@ -24,16 +24,14 @@ import java.lang.reflect.Method
* The main bundle should be placed in the [Constants.CACHE_DIR] directory named [Constants.MAIN_SCRIPT_FILE].
* If the bundle file does not exist, it will attempt to load `assets://Shiggy.bundle` from the module's assets.
*/
class HookScriptLoaderModule : Module() {
object HookScriptLoaderModule : Module() {
private lateinit var preloadsDir: File
private lateinit var mainScript: File
private lateinit var modulePath: String
private lateinit var resources: XModuleResources
companion object {
const val PRELOADS_DIR = "preloads"
}
const val PRELOADS_DIR = "preloads"
override fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {
this@HookScriptLoaderModule.modulePath = startupParam.modulePath
@ -47,26 +45,20 @@ class HookScriptLoaderModule : Module() {
mainScript = File(cacheDir, Constants.MAIN_SCRIPT_FILE).apply { asFile() }
listOf(
"com.facebook.react.runtime.ReactInstance\$loadJSBundle$1",
"com.facebook.react.runtime.ReactInstance$1",
// TODO: Remove once Discord fully switches to Bridgeless
"com.facebook.react.bridge.CatalystInstanceImpl"
).mapNotNull { classLoader.safeLoadClass(it) }
.forEach { hook(it) }
).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) }
}
private fun hook(instance: Class<*>) {
private fun hook(instance: Class<*>) = runCatching {
val loadScriptFromAssets = instance.method(
"loadScriptFromAssets",
AssetManager::class.java,
String::class.java,
Boolean::class.javaPrimitiveType
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType
)
val loadScriptFromFile = instance.method(
"loadScriptFromFile",
String::class.java,
String::class.java,
Boolean::class.javaPrimitiveType
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
)
loadScriptFromAssets.hook {
@ -82,6 +74,8 @@ class HookScriptLoaderModule : Module() {
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
}
}
}.onFailure {
Log.e("Failed to hook script loading methods in ${instance.name}:", it)
}
private fun HookScope.runCustomScripts(loadScriptFromFile: Method, loadScriptFromAssets: Method) {
@ -99,25 +93,20 @@ class HookScriptLoaderModule : Module() {
Log.i("Loading script: ${file.absolutePath}")
XposedBridge.invokeOriginalMethod(
loadScriptFromFile,
thisObject,
arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
)
Unit
}
try {
preloadsDir.walk()
.filter { it.isFile }
.forEach(runScriptFile)
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
if (mainScript.exists()) runScriptFile(mainScript)
else {
Log.i("Main script does not exist, falling back")
if (!::resources.isInitialized) resources =
XModuleResources.createInstance(modulePath, null)
if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
XposedBridge.invokeOriginalMethod(
loadScriptFromAssets,

View file

@ -21,7 +21,7 @@ import java.net.HttpURLConnection
import java.net.URL
import ShiggyXposed.xposed.Utils.Companion.reloadApp
class LogBoxModule : Module() {
object LogBoxModule : Module() {
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

View file

@ -8,7 +8,7 @@ import ShiggyXposed.xposed.BuildConfig
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Companion.JSON
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.Companion.PRELOADS_DIR
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.io.File
@ -28,7 +28,8 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val catalystInstance = classLoader.safeLoadClass("com.facebook.react.bridge.CatalystInstanceImpl")
val reactInstance = classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance$1")
val scriptLoader = classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance\$loadJSBundle$1")
?: classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance$1")
val setGlobalVariable: (XC_MethodHook.MethodHookParam, String, String) -> Unit = { param, key, json ->
runCatching {
@ -43,7 +44,7 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
writeText("this[${JSON.encodeToString(key)}]=$json")
XposedBridge.invokeOriginalMethod(
reactInstance!!.method(
scriptLoader!!.method(
"loadScriptFromFile",
String::class.java,
String::class.java,
@ -64,7 +65,7 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
build()
}
listOf(catalystInstance, reactInstance).forEach { if (it != null) hook(it, hook) }
listOf(catalystInstance, scriptLoader).forEach { if (it != null) hook(it, hook) }
}
private fun hook(instance: Class<*>, hook: XC_MethodHook) {

View file

@ -35,12 +35,10 @@ data class FontDefinition(
val main: Map<String, String>,
)
class FontsModule : Module() {
private companion object {
val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic")
val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
const val FONTS_ASSET_PATH = "fonts/"
}
object FontsModule : Module() {
private val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic")
private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
private const val FONTS_ASSET_PATH = "fonts/"
private lateinit var fontsDir: File
private lateinit var fontsDownloadsDir: File

View file

@ -7,13 +7,14 @@ import android.os.Build
import androidx.core.content.ContextCompat
import ShiggyXposed.xposed.Module
import kotlinx.serialization.json.*
import java.lang.ref.WeakReference
class SysColorsModule : Module() {
private lateinit var context: Context
object SysColorsModule : Module() {
private lateinit var context: WeakReference<Context>
private fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
override fun buildPayload(builder: JsonObjectBuilder) {
context = AndroidAppHelper.currentApplication()
context = WeakReference(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)
@ -36,7 +37,7 @@ class SysColorsModule : Module() {
}
private fun convertToColor(id: Int): String {
val clr = if (isSupported()) ContextCompat.getColor(context, id) else 0
val clr = if (isSupported()) ContextCompat.getColor(context.get()!!, id) else 0
return String.format("#%06X", 0xFFFFFF and clr)
}
}

View file

@ -34,15 +34,14 @@ data class Theme(
val id: String, val selected: Boolean, val data: ThemeData
)
class ThemesModule : Module() {
object ThemesModule : Module() {
private lateinit var param: XC_LoadPackage.LoadPackageParam
private var theme: Theme? = null
private val rawColorMap = mutableMapOf<String, Int>()
private companion object {
const val THEME_FILE = "current-theme.json"
}
private const val THEME_FILE = "current-theme.json"
@ExperimentalSerializationApi
override fun buildPayload(builder: JsonObjectBuilder) {
@ -62,7 +61,7 @@ class ThemesModule : Module() {
hookTheme()
}
private fun File.isValidish(): Boolean {
private fun File.isValidThemeFile(): Boolean {
if (!this.exists()) return false
val text = this.readText()
@ -71,7 +70,7 @@ class ThemesModule : Module() {
private fun getTheme(): Theme? {
val themeFile = File(param.appInfo.dataDir, "${Constants.FILES_DIR}/${THEME_FILE}").apply { asFile() }
if (!themeFile.isValidish()) return null
if (!themeFile.isValidThemeFile()) return null
return try {
val themeText = themeFile.readText()

View file

@ -11,7 +11,7 @@ import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils
import java.io.File
class AdditionalBridgeMethodsModule : Module() {
object AdditionalBridgeMethodsModule : Module() {
override fun onContext(context: Context) = with(context) {
BridgeModule.registerMethod("Shiggy.fs.getConstants") {
mapOf(

View file

@ -45,43 +45,28 @@ typealias BridgeMethodArgs = ArrayList<Any>
* }
* ```
*/
class BridgeModule : Module() {
object BridgeModule : Module() {
private lateinit var readableMapGetString: Method
private lateinit var readableMapToHashMap: Method
private lateinit var argumentsMakeNative: Method
companion object {
private const val CALL_DATA_KEY = "Shiggy"
private const val METHOD_NAME_KEY = "method"
private const val METHOD_ARGS_KEY = "args"
private const val CALL_DATA_KEY = "Shiggy"
private const val METHOD_NAME_KEY = "method"
private const val METHOD_ARGS_KEY = "args"
private val methods: MutableMap<String, BridgeMethodCallback> = mutableMapOf("Shiggy.info" to {
mapOf(
"name" to Constants.LOADER_NAME, "version" to BuildConfig.VERSION_CODE
)
}, "Shiggy.test" to {
mapOf(
"string" to "string",
"number" to 7256,
"array" to listOf("testing", 527737, listOf(true)),
"object" to mapOf("nested" to true),
"boolean" to false,
"args" to it,
)
})
private val methods: MutableMap<String, BridgeMethodCallback> = mutableMapOf()
/**
* Registers a bridge method that can be called from JavaScript.
*
* If the method is already registered, it will be overwritten.
*
* @param name The name of the method to register.
* @param callback The callback to invoke when the method is called.
*/
fun registerMethod(name: String, callback: BridgeMethodCallback) {
if (methods.containsKey(name)) Log.w("Bridge method already exists and will be overridden: $name")
methods[name] = callback
}
/**
* Registers a bridge method that can be called from JavaScript.
*
* If the method is already registered, it will be overwritten.
*
* @param name The name of the method to register.
* @param callback The callback to invoke when the method is called.
*/
fun registerMethod(name: String, callback: BridgeMethodCallback) {
if (methods.containsKey(name)) Log.w("Bridge method already exists and will be overridden: $name")
methods[name] = callback
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
@ -112,9 +97,32 @@ class BridgeModule : Module() {
}
}
// Clear methods every package load. Plugins should re-register on each load.
methods.clear()
registerDefaultMethods()
return@with
}
private fun registerDefaultMethods() {
methods["Shiggy.info"] = {
mapOf(
"name" to Constants.LOADER_NAME, "version" to BuildConfig.VERSION_CODE
)
}
methods["Shiggy.test"] = {
mapOf(
"string" to "string",
"number" to 7256,
"array" to listOf("testing", 527737, listOf(true)),
"object" to mapOf("nested" to true),
"boolean" to false,
"args" to it,
)
}
}
private fun Any?.toNativeObject(): Any? = argumentsMakeNative.invoke(
null, when (this) {
Unit -> null

View file

@ -9,26 +9,38 @@ import ShiggyXposed.xposed.Utils.Log
* Hooks Discord's crash reporting to disable Sentry initialization.
* While Discord doesn't have Sentry auto-initialization on by default, we still hook the content provider just to be safe.
*/
class BlockCrashReportingModule : Module() {
object BlockCrashReportingModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val crashReportingClass = classLoader.safeLoadClass("com.discord.crash_reporting.CrashReporting")
crashReportingClass?.hookMethod(
"init", Context::class.java, String::class.java
) {
before {
Log.i("Blocked CrashReporting initialization")
result = null
crashReportingClass?.apply {
// Hooking this will result in a crash after a few seconds, since Discord expects initialization to complete when setting a Sentry tag.
// So we also hook isDisabled to make sure those calls are no-ops.
hookMethod(
"init", Context::class.java, String::class.java
) {
before {
Log.i("Blocked CrashReporting initialization")
result = null
}
}
}
val sentryInitProviderClass = classLoader.safeLoadClass("io.sentry.android.core.SentryInitProvider")
sentryInitProviderClass?.hookMethod("onCreate") {
before {
Log.i("Blocked SentryInitProvider initialization")
result = true
runCatching {
hookMethod("isDisabled") {
before {
Log.i("Forced CrashReporting.isDisabled() to true")
result = true
}
}
}
}
return@with
val sentryInitProviderClass = classLoader.safeLoadClass("io.sentry.android.core.SentryInitProvider")
sentryInitProviderClass?.hookMethod("onCreate") {
before {
Log.i("Blocked SentryInitProvider initialization")
result = true
}
}
return@with
}
}
}

View file

@ -8,7 +8,7 @@ import ShiggyXposed.xposed.Utils.Log
/**
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.
*/
class BlockDeepLinksTrackingModule : Module() {
object BlockDeepLinksTrackingModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val deepLinksClass = classLoader.safeLoadClass("com.discord.deep_link.DeepLinks")
deepLinksClass?.hookMethod(