Update FontsModule.kt
This commit is contained in:
parent
2b9956d97b
commit
82214b6ed0
1 changed files with 119 additions and 103 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue