From 2d62630e30bae6a3aca9b6899d98c3bf5a5fb50f Mon Sep 17 00:00:00 2001 From: OTKUSteyler Date: Wed, 9 Sep 2026 17:45:20 +0200 Subject: [PATCH] Update Main.kt --- .../kotlin/ShiggyXposed/xposed/xposed/Main.kt | 301 ++++++------------ 1 file changed, 91 insertions(+), 210 deletions(-) diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt index 691afed..cd11e8a 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt @@ -1,228 +1,109 @@ -package GoonXposed.xposed.modules.appearance +package GoonXposed.xposed -import android.content.res.AssetManager -import android.graphics.Typeface -import android.graphics.Typeface.CustomFallbackBuilder -import android.graphics.fonts.Font -import android.graphics.fonts.FontFamily -import android.os.Build -import de.robv.android.xposed.XC_MethodHook.MethodHookParam -import de.robv.android.xposed.XC_MethodReplacement -import de.robv.android.xposed.XposedHelpers +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.os.Bundle +import de.robv.android.xposed.IXposedHookLoadPackage +import de.robv.android.xposed.IXposedHookZygoteInit import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Constants -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Companion.JSON import GoonXposed.xposed.Utils.Log -import GoonXposed.xposed.asDir -import GoonXposed.xposed.asFile -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import kotlinx.coroutines.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObjectBuilder -import kotlinx.serialization.json.put -import java.io.File -import java.io.IOException +import GoonXposed.xposed.modules.* +import GoonXposed.xposed.modules.appearance.FontsModule +import GoonXposed.xposed.modules.appearance.SysColorsModule +import GoonXposed.xposed.modules.appearance.ThemesModule +import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule +import GoonXposed.xposed.modules.bridge.BridgeModule +import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule +import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule +import GoonXposed.xposed.modules.LogBox.* +import kotlinx.coroutines.CompletableDeferred -@Serializable -data class FontDefinition( - val name: String? = null, - val description: String? = null, - val spec: Int? = null, - val main: Map, -) +object HookStateHolder { + /** + * Whether all hooks are completed, and we are ready to load the JS bundle. + */ + val readyDeferred = CompletableDeferred() -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/" + /** + * Whether we have successfully received a [Context] yet. + * Sometimes the app process is recreated and Xposed hooks way too late for us to get [Context] from [ContextWrapper.attachBaseContext]. + * But since Xposed hooks before [Activity.onCreate], we can still get it from there and still initialize properly. + */ + @Volatile + var gotContext = false +} - private lateinit var fontsDir: File - private lateinit var fontsDownloadsDir: File - private var fontsAbsPath: String? = null +class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit { + private var hooked = false + private val modules = mutableListOf( + HookScriptLoaderModule, + BridgeModule, + AdditionalBridgeMethodsModule, + PluginsModule(), + UpdaterModule, + FixResourcesModule, + BlockDeepLinksTrackingModule, + BlockCrashReportingModule, + LogBoxModule, + CacheModule, + PerfPatchesModule, + FontsModule, + ThemesModule, + SysColorsModule + ) - override fun buildPayload(builder: JsonObjectBuilder) { - builder.apply { - put("fontPatch", 2) - } + init { + modules += PayloadGlobalModule(modules) } - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - XposedHelpers.findAndHookMethod( - "com.facebook.react.common.assets.ReactFontManager\$Companion", - classLoader, - "createAssetTypeface", - String::class.java, - Int::class.java, - "android.content.res.AssetManager", - object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: MethodHookParam): Typeface? { - val fontFamilyName: String = param.args[0].toString() - val style: Int = param.args[1] as Int - val assetManager: AssetManager = param.args[2] as AssetManager - return createAssetTypeface(fontFamilyName, style, assetManager) - } - }) - - val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply { asFile() } - if (!fontDefFile.exists()) return@with - - val fontDef = try { - JSON.decodeFromString(fontDefFile.readText()) - } catch (_: Throwable) { - return@with - } - - val fontName = fontDef.name ?: return@with - fontsDownloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply { asDir() } - fontsDir = File(fontsDownloadsDir, fontName).apply { asDir() } - 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) }}").apply { asFile() } - 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() - } + override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { + for (module in modules) module.onInit(startupParam) } - private fun splitFontName(fontFamilyName: String): Pair { - val parts = fontFamilyName.split(":") - return if (parts.size >= 2) parts[0] to parts[1] else "" to fontFamilyName + override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) { + if (hooked) return + + val reactActivity = classLoader.loadClass(Constants.TARGET_ACTIVITY) + + ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) { + after { + val ctx = args[0] as Context + HookStateHolder.gotContext = true + Log.i("Received Context") + this@Main.onContext(ctx) + } + } + + reactActivity.hookMethod("onCreate", Bundle::class.java) { + after { + val act = thisObject as Activity + Log.i("Received Activity") + + if (!HookStateHolder.gotContext) { + Log.w("Activity created before we got Context, process may have been recreated!") + this@Main.onContext(act.applicationContext) + } + + this@Main.onActivity(act) + HookStateHolder.readyDeferred.complete(Unit) + } + } + + this@Main.onLoad(param) + + hooked = true } - private fun fontFileFor(customName: String, refName: String, fileExtension: String): File = - if (customName.isEmpty()) File(fontsDownloadsDir, "$refName.$fileExtension") - else File(fontsDownloadsDir, "$customName/$refName.$fileExtension") - - private fun createAssetTypefaceWithFallbacks( - fontFamilyNames: Array, style: Int, assetManager: AssetManager - ): Typeface? { - val fontFamilies: MutableList = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - for (fontFamilyName in fontFamilyNames) { - try { - for (fileExtension in FILE_EXTENSIONS) { - val (customName, refName) = splitFontName(fontFamilyName) - val file = fontFileFor(customName, refName, fileExtension).apply { asFile() } - val font = Font.Builder(file).build() - val family = FontFamily.Builder(font).build() - fontFamilies.add(family) - } - } catch (_: Throwable) { - } - - for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { - for (fileExtension in FILE_EXTENSIONS) { - val fileName = - StringBuilder().append(fontRootPath).append(fontFamilyName).append(fileExtension).toString() - try { - val builder = if (fileName[0] == '/') Font.Builder(File(fileName)) else Font.Builder( - assetManager, fileName - ) - val font = builder.build() - val family = FontFamily.Builder(font).build() - fontFamilies.add(family) - } catch (_: RuntimeException) { - continue - } catch (_: IOException) { - continue - } - } - } - } - - 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]) - } - return fallbackBuilder.build() - } - return null + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) { + for (module in modules) module.onLoad(packageParam) } - private fun createAssetTypeface( - fontFamilyName: String, style: Int, assetManager: AssetManager - ): Typeface? { - var fontFamilyName: String = fontFamilyName - val fontFamilyNames = fontFamilyName.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - for (i in fontFamilyNames.indices) { - fontFamilyNames[i] = fontFamilyNames[i].trim() - } + override fun onContext(context: Context) { + for (module in modules) module.onContext(context) + } - if (fontFamilyNames.size > 1) { - fontFamilyName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return createAssetTypefaceWithFallbacks(fontFamilyNames, style, assetManager) - } else { - fontFamilyNames[0] - } - } - - val extension = EXTENSIONS[style] - - try { - for (fileExtension in FILE_EXTENSIONS) { - val (customName, refName) = splitFontName(fontFamilyName) - val file = fontFileFor(customName, refName, fileExtension).apply { asFile() } - if (!file.exists()) throw Exception() - return Typeface.createFromFile(file.absolutePath) - } - } catch (_: Throwable) { - } - - for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { - for (fileExtension in FILE_EXTENSIONS) { - val fileName = - StringBuilder().append(fontRootPath).append(fontFamilyName).append(extension).append(fileExtension) - .toString() - - return try { - if (fileName[0] == '/') Typeface.createFromFile(fileName) - else Typeface.createFromAsset(assetManager, fileName) - } catch (_: RuntimeException) { - continue - } - } - } - - return Typeface.create(fontFamilyName, style) + override fun onActivity(activity: Activity) { + for (module in modules) module.onActivity(activity) } }