From b9b197fd7c901065b447040ed90e166d89f5ad86 Mon Sep 17 00:00:00 2001 From: Amsyar Rasyiq <82711525+amsyarasyiq@users.noreply.github.com> Date: Wed, 24 May 2023 16:30:06 +0000 Subject: [PATCH] Init before onCreate, async bundle downloading For some reason, this actually feels slower.. --- .../main/kotlin/com/pyoncord/xposed/Main.kt | 108 +++++++++++------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/app/src/main/kotlin/com/pyoncord/xposed/Main.kt b/app/src/main/kotlin/com/pyoncord/xposed/Main.kt index 2aa0711..6e39e41 100644 --- a/app/src/main/kotlin/com/pyoncord/xposed/Main.kt +++ b/app/src/main/kotlin/com/pyoncord/xposed/Main.kt @@ -2,11 +2,15 @@ package com.pyoncord.xposed import java.io.File import java.lang.Exception +import java.lang.System import kotlinx.coroutines.* +import android.app.Activity +import android.content.Intent import android.content.res.AssetManager import android.content.res.XModuleResources import android.content.res.Resources import android.util.Log +import android.os.Bundle import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.IXposedHookZygoteInit import de.robv.android.xposed.XC_MethodHook @@ -19,67 +23,92 @@ import io.ktor.client.engine.cio.* import io.ktor.client.statement.* import io.ktor.client.plugins.* import io.ktor.http.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import kotlinx.serialization.* +import kotlinx.serialization.json.* class Main : IXposedHookZygoteInit, IXposedHookLoadPackage { companion object { const val LOG_TAG = "Pyoncord" - const val DEFAULT_ENDPOINT = "https://raw.githubusercontent.com/pyoncord/pyoncord/builds/pyoncord.js" + const val DEFAULT_BUNDLE_ENDPOINT = "https://raw.githubusercontent.com/pyoncord/pyoncord/builds/pyoncord.js" } private lateinit var resources: XModuleResources + private lateinit var appActivity: Activity // Assign module resources in process zygote override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { resources = XModuleResources.createInstance(startupParam.modulePath, null) } - // Hook function responsible for loading Discord's package - override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) { - val catalystInstance = try { - classLoader.loadClass("com.facebook.react.bridge.CatalystInstanceImpl") - } catch (e: ClassNotFoundException) { - // Package is not the target app, return - return@with + override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) = with(lpparam) { + val reactActivity = runCatching { + classLoader.loadClass("com.discord.react_activities.ReactActivity") + }.getOrElse { return@with } // Package is our the target app, return + + XposedBridge.hookMethod(reactActivity.getDeclaredMethod("onCreate", Bundle::class.java), object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + appActivity = param.thisObject as Activity + + init(lpparam) + } + }) + } + + fun restartApp() = with(appActivity.applicationContext) { + val intent = packageManager.getLaunchIntentForPackage(packageName)!! + + appActivity.startActivity(Intent.makeRestartActivityTask(intent.getComponent())) + System.exit(0) + } + + suspend fun checkForUpdate(bundle: File, etag: File): Unit = withContext(Dispatchers.IO) { + val client = HttpClient(CIO) { + install(HttpTimeout) { requestTimeoutMillis = 1000 } + install(UserAgent) { agent = "PyoncordXposed" } } + try { + val response: HttpResponse = client.get(DEFAULT_BUNDLE_ENDPOINT) { + headers { + if (etag.exists() && bundle.exists()) { + append(HttpHeaders.IfNoneMatch, etag.readText()) + } + } + } + + if (response.status == HttpStatusCode.OK) { + Log.i(LOG_TAG, "New bundle found, updating and restarting...") + + bundle.writeBytes(response.body()) + response.headers["Etag"]?.let { etag.writeText(it) } + restartApp() + } + } catch (e: Exception) { + Log.e(LOG_TAG, "An error occured while downloading pyoncord bundle.", e) + } finally { + client.close() + } + } + + fun init(param: XC_LoadPackage.LoadPackageParam) = with(param) { + val catalystInstance = classLoader.loadClass("com.facebook.react.bridge.CatalystInstanceImpl") + val files = File(appInfo.dataDir, "files").also { it.mkdirs() } val pyoncordFd = File(files, "pyoncord").also { it.mkdirs() } val bundle = File(pyoncordFd, "pyoncord.js") - val etag = File(pyoncordFd, "~$.etag") + val etag = File(pyoncordFd, "etag") - val httpJob = GlobalScope.launch(Dispatchers.IO) { - try { - val client = HttpClient(CIO) { - install(HttpTimeout) { requestTimeoutMillis = 1000 } - install(UserAgent) { agent = "PyoncordXposed" } - } - val response: HttpResponse = client.get(DEFAULT_ENDPOINT) { - headers { if (etag.exists() && bundle.exists()) append(HttpHeaders.IfNoneMatch, etag.readText()) } - } - - if (response.status.value == 200) { - bundle.writeBytes(response.body()) - if (response.headers.contains("Etag")) etag.writeText(response.headers["Etag"] as String) - } - - return@launch - } catch (e: Exception) { - Log.e(LOG_TAG, "Failed to download pyoncord.js bundle") - } + MainScope().launch() { + checkForUpdate(bundle, etag) } val loadScriptFromAssets = catalystInstance.getDeclaredMethod("jniLoadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType) val loadScriptFromFile = catalystInstance.getDeclaredMethod("jniLoadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType) // Mirror resource resolver to "com.discord" if the package name is renamed - // I don't know if there's any side effect with this - amsyar@pylix if (packageName != "com.discord") { - val getIdentifier = Resources::class.java.getDeclaredMethod("getIdentifier", String::class.java, String::class.java, String::class.java); + val getIdentifier = Resources::class.java.getDeclaredMethod("getIdentifier", String::class.java, String::class.java, String::class.java) XposedBridge.hookMethod(getIdentifier, object: XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam) { @@ -91,8 +120,6 @@ class Main : IXposedHookZygoteInit, IXposedHookLoadPackage { // TODO: Loader config val hook = object: XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam) = with(param) { - Log.d(LOG_TAG, "Hooking ${method.name} to load Pyoncord") - // Load pre-patches and identity XposedBridge.invokeOriginalMethod(loadScriptFromAssets, thisObject, arrayOf(resources.assets, "assets://js/modules.js", true)) XposedBridge.invokeOriginalMethod(loadScriptFromAssets, thisObject, arrayOf(resources.assets, "assets://js/identity.js", true)) @@ -100,9 +127,12 @@ class Main : IXposedHookZygoteInit, IXposedHookLoadPackage { // Invoke the original method XposedBridge.invokeOriginalMethod(method, thisObject, args) - // Wait for bundle to download. Then, execute our bundle - runBlocking { httpJob.join() } - XposedBridge.invokeOriginalMethod(loadScriptFromFile, thisObject, arrayOf(bundle.absolutePath, bundle.absolutePath, args[2])) + // Execute our bundle + if (bundle.exists()) XposedBridge.invokeOriginalMethod( + loadScriptFromFile, + thisObject, + arrayOf(bundle.absolutePath, bundle.absolutePath, args[2]) + ) // Prevent from invoking the original method setResult(null)