From ee7e30e67c906655caae857962480d12fed9a085 Mon Sep 17 00:00:00 2001 From: OTKUSteyler Date: Sat, 12 Sep 2026 10:08:55 +0200 Subject: [PATCH] Update FontsModule.kt --- .../xposed/modules/appearance/FontsModule.kt | 198 ++++++++---------- 1 file changed, 90 insertions(+), 108 deletions(-) diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt index dbf90b2..5a5d021 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/appearance/FontsModule.kt @@ -1,5 +1,3 @@ -@file:Suppress("DEPRECATION") - package GoonXposed.xposed.modules.appearance import android.content.res.AssetManager @@ -15,8 +13,6 @@ 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.* @@ -45,92 +41,77 @@ object FontsModule : Module() { private const val FONTS_ASSET_PATH = "fonts/" private lateinit var fontsDir: File - private var fontsDownloadsDir: File? = null + private lateinit var fontsDownloadsDir: File private var fontsAbsPath: String? = null override fun buildPayload(builder: JsonObjectBuilder) { - builder.put("fontPatch", 2) + builder.apply { + put("fontPatch", 2) + } } override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - // ReactFontManager hijack runs regardless of fonts.json presence; it falls back to the - // default Typeface chain if no custom font file is found. - listOf( + XposedHelpers.findAndHookMethod( "com.facebook.react.common.assets.ReactFontManager\$Companion", - "com.facebook.react.views.text.ReactFontManager\$Companion", - ).forEach { clsName -> - XposedHelpers.findClassIfExists(clsName, classLoader)?.let { cls -> - runCatching { - XposedHelpers.findAndHookMethod( - cls, - "createAssetTypeface", - String::class.java, - Int::class.java, - AssetManager::class.java, - object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: MethodHookParam): Any? { - 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) - } - }, - ) - }.onFailure { e -> - Log.e("Failed to hook ReactFontManager: ${e.message}") + 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") + 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 (e: Throwable) { - Log.w("fonts.json malformed: ${e.message}") + } catch (_: Throwable) { return@with } - val setName = fontDef.name ?: return@with - val downloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").asDir() - fontsDir = File(downloadsDir, setName).asDir() - fontsDownloadsDir = downloadsDir + fontsDownloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply { asDir() } + fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { asDir() } fontsAbsPath = fontsDir.absolutePath + "/" - // Prune stale font files for this set. 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 stale font file: $fileName") + Log.i("Deleting font file: $fileName") file.delete() } } } - // Font files are normally downloaded by the JS side; this is a safety net for - // font packs that only ship a fonts.json pointing at remote URLs. + // These files should be downloaded by the JS side, but oh well CoroutineScope(Dispatchers.IO).launch { - fontDef.main.entries.map { (name, url) -> + fontDef.main.keys.map { name -> async { + val url = fontDef.main.getValue(name) try { Log.i("Downloading $name from $url") - val ext = FILE_EXTENSIONS.firstOrNull { url.endsWith(it) } ?: ".ttf" - val file = File(fontsDir, "$name$ext") + val file = + File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}").apply { asFile() } if (file.exists()) return@async - val response: HttpResponse = HttpClient(CIO) { + val client = HttpClient(CIO) { install(UserAgent) { agent = Constants.USER_AGENT } - install(HttpRedirect) {} - }.use { it.get(url) } - - if (response.status == HttpStatusCode.OK) { - file.writeBytes(response.body()) } + + 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 font ($name from $url)", e) + Log.e("Failed to download fonts ($name from $url)", e) } } }.awaitAll() @@ -138,68 +119,69 @@ object FontsModule : Module() { } private fun createAssetTypefaceWithFallbacks( - fontFamilyNames: Array, - style: Int, - assetManager: AssetManager, + fontFamilyNames: Array, style: Int, assetManager: AssetManager ): Typeface? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return null - val fontFamilies: MutableList = ArrayList() - - // Iterate over the list of fontFamilyNames, constructing new FontFamily objects - // for use in the CustomFallbackBuilder below. - for (fontFamilyName in fontFamilyNames) { - try { - for (fileExt in FILE_EXTENSIONS) { - val split = fontFamilyName.split(":") - if (split.size != 2) break - val (customName, refName) = split - val downloads = fontsDownloadsDir ?: break - val file = File(downloads, "$customName/$refName.$fileExt") - if (!file.exists()) continue - val font = Font.Builder(file).build() - fontFamilies.add(FontFamily.Builder(font).build()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Iterate over the list of fontFamilyNames, constructing new FontFamily objects + // for use in the CustomFallbackBuilder below. + for (fontFamilyName in fontFamilyNames) { + try { + for (fileExtension in FILE_EXTENSIONS) { + val (customName, refName) = fontFamilyName.split(":") + val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() } + val font = Font.Builder(file).build() + val family = FontFamily.Builder(font).build() + fontFamilies.add(family) + } + } catch (_: Throwable) { } - } catch (_: Throwable) { - } - for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { - for (fileExt in FILE_EXTENSIONS) { - val fileName = "$fontRootPath$fontFamilyName$fileExt" - try { - val builder = if (fileName[0] == '/') Font.Builder(File(fileName)) - else Font.Builder(assetManager, fileName) - val font = builder.build() - fontFamilies.add(FontFamily.Builder(font).build()) - } catch (_: RuntimeException) { - continue - } catch (_: IOException) { - continue + 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) { + // If the typeface asset does not exist, try another extension. + continue + } catch (_: IOException) { + // If the font asset does not exist, try another extension. + continue + } } } } - } - // If there's some problem constructing fonts, fall back to the default behavior. - if (fontFamilies.isEmpty()) return createAssetTypeface(fontFamilyNames[0], style, assetManager) + // If there's some problem constructing fonts, fall back to the default behavior. + 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]) + val fallbackBuilder = CustomFallbackBuilder(fontFamilies[0]) + for (i in 1 until fontFamilies.size) { + fallbackBuilder.addCustomFallback(fontFamilies[i]) + } + return fallbackBuilder.build() } - return fallbackBuilder.build() + return null } private fun createAssetTypeface( - rawName: 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. - val fontFamilyNames = rawName.split(",").map { it.trim() }.filter { it.isNotEmpty() }.toTypedArray() + var fontFamilyName: String = fontFamilyName + val fontFamilyNames = fontFamilyName.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (i in fontFamilyNames.indices) { + fontFamilyNames[i] = fontFamilyNames[i].trim() + } - var fontFamilyName = rawName // If there are multiple font family names: // For newer versions of Android, construct a Typeface with fallbacks // For older versions of Android, ignore all the fallbacks and just use the first font family @@ -211,16 +193,13 @@ object FontsModule : Module() { } } - val extension = EXTENSIONS.getOrElse(style) { "" } + val extension = EXTENSIONS[style] try { - for (fileExt in FILE_EXTENSIONS) { - val split = fontFamilyName.split(":") - if (split.size != 2) break - val (customName, refName) = split - val downloads = fontsDownloadsDir ?: break - val file = File(downloads, "$customName/$refName.$fileExt") - if (!file.exists()) continue + for (fileExtension in FILE_EXTENSIONS) { + val (customName, refName) = fontFamilyName.split(":") + val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() } + if (!file.exists()) throw Exception() return Typeface.createFromFile(file.absolutePath) } } catch (_: Throwable) { @@ -229,8 +208,11 @@ object FontsModule : Module() { // 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 (fileExt in FILE_EXTENSIONS) { - val fileName = "$fontRootPath$fontFamilyName$extension$fileExt" + 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)