Update Main.kt
This commit is contained in:
parent
750a4cd5b0
commit
2d62630e30
1 changed files with 91 additions and 210 deletions
|
|
@ -1,228 +1,109 @@
|
||||||
package GoonXposed.xposed.modules.appearance
|
package GoonXposed.xposed
|
||||||
|
|
||||||
import android.content.res.AssetManager
|
import android.app.Activity
|
||||||
import android.graphics.Typeface
|
import android.content.Context
|
||||||
import android.graphics.Typeface.CustomFallbackBuilder
|
import android.content.ContextWrapper
|
||||||
import android.graphics.fonts.Font
|
import android.os.Bundle
|
||||||
import android.graphics.fonts.FontFamily
|
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||||
import android.os.Build
|
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.XC_MethodHook.MethodHookParam
|
|
||||||
import de.robv.android.xposed.XC_MethodReplacement
|
|
||||||
import de.robv.android.xposed.XposedHelpers
|
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
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.Utils.Log
|
||||||
import GoonXposed.xposed.asDir
|
import GoonXposed.xposed.modules.*
|
||||||
import GoonXposed.xposed.asFile
|
import GoonXposed.xposed.modules.appearance.FontsModule
|
||||||
import io.ktor.client.*
|
import GoonXposed.xposed.modules.appearance.SysColorsModule
|
||||||
import io.ktor.client.call.*
|
import GoonXposed.xposed.modules.appearance.ThemesModule
|
||||||
import io.ktor.client.engine.cio.*
|
import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
|
||||||
import io.ktor.client.plugins.*
|
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||||
import io.ktor.client.request.*
|
import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule
|
||||||
import io.ktor.client.statement.*
|
import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
|
||||||
import io.ktor.http.*
|
import GoonXposed.xposed.modules.LogBox.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
@Serializable
|
object HookStateHolder {
|
||||||
data class FontDefinition(
|
/**
|
||||||
val name: String? = null,
|
* Whether all hooks are completed, and we are ready to load the JS bundle.
|
||||||
val description: String? = null,
|
*/
|
||||||
val spec: Int? = null,
|
val readyDeferred = CompletableDeferred<Unit>()
|
||||||
val main: Map<String, String>,
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
object FontsModule : Module() {
|
init {
|
||||||
private val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic")
|
modules += PayloadGlobalModule(modules)
|
||||||
private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
|
}
|
||||||
private const val FONTS_ASSET_PATH = "fonts/"
|
|
||||||
|
|
||||||
private lateinit var fontsDir: File
|
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
|
||||||
private lateinit var fontsDownloadsDir: File
|
for (module in modules) module.onInit(startupParam)
|
||||||
private var fontsAbsPath: String? = null
|
}
|
||||||
|
|
||||||
override fun buildPayload(builder: JsonObjectBuilder) {
|
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) {
|
||||||
builder.apply {
|
if (hooked) return
|
||||||
put("fontPatch", 2)
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
reactActivity.hookMethod("onCreate", Bundle::class.java) {
|
||||||
XposedHelpers.findAndHookMethod(
|
after {
|
||||||
"com.facebook.react.common.assets.ReactFontManager\$Companion",
|
val act = thisObject as Activity
|
||||||
classLoader,
|
Log.i("Received Activity")
|
||||||
"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 (!HookStateHolder.gotContext) {
|
||||||
if (!fontDefFile.exists()) return@with
|
Log.w("Activity created before we got Context, process may have been recreated!")
|
||||||
|
this@Main.onContext(act.applicationContext)
|
||||||
val fontDef = try {
|
|
||||||
JSON.decodeFromString<FontDefinition>(fontDefFile.readText())
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
return@with
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val fontName = fontDef.name ?: return@with
|
this@Main.onActivity(act)
|
||||||
fontsDownloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply { asDir() }
|
HookStateHolder.readyDeferred.complete(Unit)
|
||||||
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
|
this@Main.onLoad(param)
|
||||||
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) {
|
hooked = true
|
||||||
install(UserAgent) { agent = Constants.USER_AGENT }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val response: HttpResponse = client.get(url)
|
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
|
||||||
if (response.status == HttpStatusCode.OK) file.writeBytes(response.body())
|
for (module in modules) module.onLoad(packageParam)
|
||||||
|
|
||||||
return@async
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e("Failed to download fonts ($name from $url)", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.awaitAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun splitFontName(fontFamilyName: String): Pair<String, String> {
|
override fun onContext(context: Context) {
|
||||||
val parts = fontFamilyName.split(":")
|
for (module in modules) module.onContext(context)
|
||||||
return if (parts.size >= 2) parts[0] to parts[1] else "" to fontFamilyName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fontFileFor(customName: String, refName: String, fileExtension: String): File =
|
override fun onActivity(activity: Activity) {
|
||||||
if (customName.isEmpty()) File(fontsDownloadsDir, "$refName.$fileExtension")
|
for (module in modules) module.onActivity(activity)
|
||||||
else File(fontsDownloadsDir, "$customName/$refName.$fileExtension")
|
|
||||||
|
|
||||||
private fun createAssetTypefaceWithFallbacks(
|
|
||||||
fontFamilyNames: Array<String>, style: Int, assetManager: AssetManager
|
|
||||||
): Typeface? {
|
|
||||||
val fontFamilies: MutableList<FontFamily> = 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue