Update FontsModule.kt

This commit is contained in:
OTKUSteyler 2026-09-12 10:08:55 +02:00 • committed by GitHub
parent 57ebd3daed
commit ee7e30e67c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package GoonXposed.xposed.modules.appearance package GoonXposed.xposed.modules.appearance
import android.content.res.AssetManager import android.content.res.AssetManager
@ -15,8 +13,6 @@ 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.*
@ -45,92 +41,77 @@ object FontsModule : Module() {
private const val FONTS_ASSET_PATH = "fonts/" private const val FONTS_ASSET_PATH = "fonts/"
private lateinit var fontsDir: File private lateinit var fontsDir: File
private var fontsDownloadsDir: File? = null private lateinit var fontsDownloadsDir: File
private var fontsAbsPath: String? = null private var fontsAbsPath: String? = null
override fun buildPayload(builder: JsonObjectBuilder) { override fun buildPayload(builder: JsonObjectBuilder) {
builder.put("fontPatch", 2) builder.apply {
put("fontPatch", 2)
}
} }
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
// ReactFontManager hijack runs regardless of fonts.json presence; it falls back to the XposedHelpers.findAndHookMethod(
// default Typeface chain if no custom font file is found.
listOf(
"com.facebook.react.common.assets.ReactFontManager\$Companion", "com.facebook.react.common.assets.ReactFontManager\$Companion",
"com.facebook.react.views.text.ReactFontManager\$Companion", classLoader,
).forEach { clsName -> "createAssetTypeface",
XposedHelpers.findClassIfExists(clsName, classLoader)?.let { cls -> String::class.java,
runCatching { Int::class.java,
XposedHelpers.findAndHookMethod( "android.content.res.AssetManager",
cls, object : XC_MethodReplacement() {
"createAssetTypeface", override fun replaceHookedMethod(param: MethodHookParam): Typeface? {
String::class.java, val fontFamilyName: String = param.args[0].toString()
Int::class.java, val style: Int = param.args[1] as Int
AssetManager::class.java, val assetManager: AssetManager = param.args[2] as AssetManager
object : XC_MethodReplacement() { return createAssetTypeface(fontFamilyName, style, assetManager)
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}")
} }
} })
}
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 if (!fontDefFile.exists()) return@with
val fontDef = try { val fontDef = try {
JSON.decodeFromString<FontDefinition>(fontDefFile.readText()) JSON.decodeFromString<FontDefinition>(fontDefFile.readText())
} catch (e: Throwable) { } catch (_: Throwable) {
Log.w("fonts.json malformed: ${e.message}")
return@with return@with
} }
val setName = fontDef.name ?: return@with
val downloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").asDir() fontsDownloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply { asDir() }
fontsDir = File(downloadsDir, setName).asDir() fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { asDir() }
fontsDownloadsDir = downloadsDir
fontsAbsPath = fontsDir.absolutePath + "/" fontsAbsPath = fontsDir.absolutePath + "/"
// Prune stale font files for this set.
fontsDir.listFiles()?.forEach { file -> fontsDir.listFiles()?.forEach { file ->
val fileName = file.name val fileName = file.name
if (!fileName.startsWith(".")) { if (!fileName.startsWith(".")) {
val fontName = fileName.split('.')[0] val fontName = fileName.split('.')[0]
if (fontDef.main.keys.none { it == fontName }) { if (fontDef.main.keys.none { it == fontName }) {
Log.i("Deleting stale font file: $fileName") Log.i("Deleting font file: $fileName")
file.delete() file.delete()
} }
} }
} }
// Font files are normally downloaded by the JS side; this is a safety net for // These files should be downloaded by the JS side, but oh well
// font packs that only ship a fonts.json pointing at remote URLs.
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
fontDef.main.entries.map { (name, url) -> fontDef.main.keys.map { name ->
async { async {
val url = fontDef.main.getValue(name)
try { try {
Log.i("Downloading $name from $url") Log.i("Downloading $name from $url")
val ext = FILE_EXTENSIONS.firstOrNull { url.endsWith(it) } ?: ".ttf" val file =
val file = File(fontsDir, "$name$ext") File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}").apply { asFile() }
if (file.exists()) return@async if (file.exists()) return@async
val response: HttpResponse = HttpClient(CIO) { val client = HttpClient(CIO) {
install(UserAgent) { agent = Constants.USER_AGENT } 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) { } catch (e: Throwable) {
Log.e("Failed to download font ($name from $url)", e) Log.e("Failed to download fonts ($name from $url)", e)
} }
} }
}.awaitAll() }.awaitAll()
@ -138,68 +119,69 @@ object FontsModule : Module() {
} }
private fun createAssetTypefaceWithFallbacks( private fun createAssetTypefaceWithFallbacks(
fontFamilyNames: Array<String>, fontFamilyNames: Array<String>, style: Int, assetManager: AssetManager
style: Int,
assetManager: AssetManager,
): Typeface? { ): Typeface? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return null
val fontFamilies: MutableList<FontFamily> = ArrayList() val fontFamilies: MutableList<FontFamily> = ArrayList()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Iterate over the list of fontFamilyNames, constructing new FontFamily objects // Iterate over the list of fontFamilyNames, constructing new FontFamily objects
// for use in the CustomFallbackBuilder below. // for use in the CustomFallbackBuilder below.
for (fontFamilyName in fontFamilyNames) { for (fontFamilyName in fontFamilyNames) {
try { try {
for (fileExt in FILE_EXTENSIONS) { for (fileExtension in FILE_EXTENSIONS) {
val split = fontFamilyName.split(":") val (customName, refName) = fontFamilyName.split(":")
if (split.size != 2) break val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
val (customName, refName) = split val font = Font.Builder(file).build()
val downloads = fontsDownloadsDir ?: break val family = FontFamily.Builder(font).build()
val file = File(downloads, "$customName/$refName.$fileExt") fontFamilies.add(family)
if (!file.exists()) continue }
val font = Font.Builder(file).build() } catch (_: Throwable) {
fontFamilies.add(FontFamily.Builder(font).build())
} }
} catch (_: Throwable) {
}
for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) {
for (fileExt in FILE_EXTENSIONS) { for (fileExtension in FILE_EXTENSIONS) {
val fileName = "$fontRootPath$fontFamilyName$fileExt" val fileName =
try { StringBuilder().append(fontRootPath).append(fontFamilyName).append(fileExtension).toString()
val builder = if (fileName[0] == '/') Font.Builder(File(fileName)) try {
else Font.Builder(assetManager, fileName) val builder = if (fileName[0] == '/') Font.Builder(File(fileName)) else Font.Builder(
val font = builder.build() assetManager, fileName
fontFamilies.add(FontFamily.Builder(font).build()) )
} catch (_: RuntimeException) { val font = builder.build()
continue val family = FontFamily.Builder(font).build()
} catch (_: IOException) { fontFamilies.add(family)
continue } 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 there's some problem constructing fonts, fall back to the default behavior.
if (fontFamilies.isEmpty()) return createAssetTypeface(fontFamilyNames[0], style, assetManager) if (fontFamilies.isEmpty()) return createAssetTypeface(fontFamilyNames[0], style, assetManager)
val fallbackBuilder = CustomFallbackBuilder(fontFamilies[0]) val fallbackBuilder = CustomFallbackBuilder(fontFamilies[0])
for (i in 1 until fontFamilies.size) { for (i in 1 until fontFamilies.size) {
fallbackBuilder.addCustomFallback(fontFamilies[i]) fallbackBuilder.addCustomFallback(fontFamilies[i])
}
return fallbackBuilder.build()
} }
return fallbackBuilder.build() return null
} }
private fun createAssetTypeface( private fun createAssetTypeface(
rawName: String, fontFamilyName: String, style: Int, assetManager: AssetManager
style: Int,
assetManager: AssetManager,
): Typeface? { ): Typeface? {
// This logic attempts to safely check if the frontend code is attempting to use // 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. // 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: // If there are multiple font family names:
// For newer versions of Android, construct a Typeface with fallbacks // 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 // 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 { try {
for (fileExt in FILE_EXTENSIONS) { for (fileExtension in FILE_EXTENSIONS) {
val split = fontFamilyName.split(":") val (customName, refName) = fontFamilyName.split(":")
if (split.size != 2) break val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
val (customName, refName) = split if (!file.exists()) throw Exception()
val downloads = fontsDownloadsDir ?: break
val file = File(downloads, "$customName/$refName.$fileExt")
if (!file.exists()) continue
return Typeface.createFromFile(file.absolutePath) return Typeface.createFromFile(file.absolutePath)
} }
} catch (_: Throwable) { } catch (_: Throwable) {
@ -229,8 +208,11 @@ object FontsModule : Module() {
// Lastly, after all those checks above, this is the original RN logic for // Lastly, after all those checks above, this is the original RN logic for
// getting the typeface. // getting the typeface.
for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) {
for (fileExt in FILE_EXTENSIONS) { for (fileExtension in FILE_EXTENSIONS) {
val fileName = "$fontRootPath$fontFamilyName$extension$fileExt" val fileName =
StringBuilder().append(fontRootPath).append(fontFamilyName).append(extension).append(fileExtension)
.toString()
return try { return try {
if (fileName[0] == '/') Typeface.createFromFile(fileName) if (fileName[0] == '/') Typeface.createFromFile(fileName)
else Typeface.createFromAsset(assetManager, fileName) else Typeface.createFromAsset(assetManager, fileName)