Update FontsModule.kt

This commit is contained in:
OTKUSteyler 2026-09-11 21:48:46 +02:00 • committed by GitHub
parent 2b9956d97b
commit 82214b6ed0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION")
package GoonXposed.xposed.modules.appearance
import android.content.res.AssetManager
@ -6,6 +8,8 @@ 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_MethodReplacement
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
@ -13,23 +17,17 @@ import GoonXposed.xposed.Utils.Companion.JSON
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 io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.HttpRedirect
import io.ktor.client.plugins.UserAgent
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
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
@ -41,10 +39,19 @@ data class FontDefinition(
val main: Map<String, String>,
)
/**
* Custom font loading + ReactFontManager hijack.
*/
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 var fontsDownloadsDir: File? = null
private var fontsAbsPath: String? = null
override fun buildPayload(builder: JsonObjectBuilder) {
builder.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.
@ -52,43 +59,47 @@ object FontsModule : Module() {
"com.facebook.react.common.assets.ReactFontManager\$Companion",
"com.facebook.react.views.text.ReactFontManager\$Companion",
).forEach { clsName ->
classLoader.safeLoadClass(clsName)?.let { cls ->
XposedHelpers.findClassIfExists(clsName, classLoader)?.let { cls ->
runCatching {
cls.method("createAssetTypeface", String::class.java, Int::class.java, AssetManager::class.java).hook {
before {
val fontFamilyName: String = args[0].toString()
val style: Int = args[1] as Int
val assetManager: AssetManager = args[2] as AssetManager
result = FontsState.createAssetTypeface(fontFamilyName, style, assetManager)
}
}
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}")
}
}
}
}
override fun onContext(context: android.content.Context) {
val dataDir = context.dataDir.absolutePath
val fontDefFile = File(dataDir, "${Constants.FILES_DIR}/fonts.json").asFile()
if (!fontDefFile.exists()) return
val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json")
if (!fontDefFile.exists()) return@with
val fontDef = try {
JSON.decodeFromString<FontDefinition>(fontDefFile.readText())
} catch (e: Throwable) {
Log.w("fonts.json malformed: ${e.message}")
return
return@with
}
val setName = fontDef.name ?: return
val setName = fontDef.name ?: return@with
val downloadsDir = File(dataDir, "${Constants.FILES_DIR}/downloads/fonts").asDir()
val setDir = File(downloadsDir, setName).asDir()
FontsState.fontsDownloadsDir = downloadsDir
FontsState.fontsAbsPath = setDir.absolutePath + "/"
val downloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").asDir()
fontsDir = File(downloadsDir, setName).asDir()
fontsDownloadsDir = downloadsDir
fontsAbsPath = fontsDir.absolutePath + "/"
// Prune stale font files for this set.
setDir.listFiles()?.forEach { file ->
fontsDir.listFiles()?.forEach { file ->
val fileName = file.name
if (!fileName.startsWith(".")) {
val fontName = fileName.split('.')[0]
@ -99,18 +110,22 @@ object FontsModule : Module() {
}
}
// 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.
CoroutineScope(Dispatchers.IO).launch {
fontDef.main.entries.map { (name, url) ->
async {
try {
Log.i("Downloading $name from $url")
val ext = FontsState.FILE_EXTENSIONS.firstOrNull { url.endsWith(it) } ?: ".ttf"
val file = File(setDir, "$name$ext").asFile()
val ext = FILE_EXTENSIONS.firstOrNull { url.endsWith(it) } ?: ".ttf"
val file = File(fontsDir, "$name$ext")
if (file.exists()) return@async
val response: HttpResponse = HttpClient(CIO) {
install(UserAgent) { agent = Constants.USER_AGENT }
install(HttpRedirect) {}
}.use { it.get(url) }
if (response.status == HttpStatusCode.OK) {
file.writeBytes(response.body())
}
@ -121,66 +136,6 @@ object FontsModule : Module() {
}.awaitAll()
}
}
}
/**
* Holds the per-process font-state used by the `createAssetTypeface` hijack.
*/
private object FontsState {
val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic")
val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
const val FONTS_ASSET_PATH = "fonts/"
@Volatile
var fontsDownloadsDir: File? = null
@Volatile
var fontsAbsPath: String? = null
fun createAssetTypeface(
rawName: String,
style: Int,
assetManager: AssetManager,
): Typeface? {
val fontFamilyNames = rawName.split(",").map { it.trim() }.filter { it.isNotEmpty() }.toTypedArray()
var fontFamilyName = rawName
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.getOrElse(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
return Typeface.createFromFile(file.absolutePath)
}
} catch (_: Throwable) {
}
for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) {
for (fileExt in FILE_EXTENSIONS) {
val fileName = "$fontRootPath$fontFamilyName$extension$fileExt"
return try {
if (fileName[0] == '/') Typeface.createFromFile(fileName)
else Typeface.createFromAsset(assetManager, fileName)
} catch (_: RuntimeException) {
continue
}
}
}
return Typeface.create(fontFamilyName, style)
}
private fun createAssetTypefaceWithFallbacks(
fontFamilyNames: Array<String>,
@ -188,7 +143,11 @@ private object FontsState {
assetManager: AssetManager,
): Typeface? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return null
val fontFamilies: MutableList<FontFamily> = 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) {
@ -221,10 +180,67 @@ private object FontsState {
}
}
// 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])
for (i in 1 until fontFamilies.size) {
fallbackBuilder.addCustomFallback(fontFamilies[i])
}
return fallbackBuilder.build()
}
private fun createAssetTypeface(
rawName: 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 = 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
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.getOrElse(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
return Typeface.createFromFile(file.absolutePath)
}
} catch (_: Throwable) {
}
// 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"
return try {
if (fileName[0] == '/') Typeface.createFromFile(fileName)
else Typeface.createFromAsset(assetManager, fileName)
} catch (_: RuntimeException) {
// If the typeface asset does not exist, try another extension.
continue
}
}
}
return Typeface.create(fontFamilyName, style)
}
}