Delete app/src/main/kotlin/io/github/revenge/xposed directory
This commit is contained in:
parent
8389d19545
commit
2171fc59eb
6 changed files with 0 additions and 1009 deletions
|
|
@ -1,32 +0,0 @@
|
|||
package GoonXposed.xposed
|
||||
|
||||
import android.app.AndroidAppHelper
|
||||
import android.content.Intent
|
||||
import kotlin.system.exitProcess
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class Utils {
|
||||
companion object {
|
||||
val JSON = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun reloadApp() {
|
||||
val application = AndroidAppHelper.currentApplication()
|
||||
val intent =
|
||||
application.packageManager.getLaunchIntentForPackage(application.packageName)
|
||||
application.startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
||||
object Log {
|
||||
fun e(msg: String) = android.util.Log.e(Constants.LOG_TAG, msg)
|
||||
fun e(msg: String, throwable: Throwable) =
|
||||
android.util.Log.e(Constants.LOG_TAG, msg, throwable)
|
||||
fun i(msg: String) = android.util.Log.i(Constants.LOG_TAG, msg)
|
||||
fun i(msg: String, throwable: Throwable) =
|
||||
android.util.Log.i(Constants.LOG_TAG, msg, throwable)
|
||||
fun w(msg: String) = android.util.Log.w(Constants.LOG_TAG, msg)
|
||||
fun w(msg: String, throwable: Throwable) =
|
||||
android.util.Log.w(Constants.LOG_TAG, msg, throwable)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package GoonXposed.xposed.modules
|
||||
|
||||
import android.content.res.Resources
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
|
||||
import GoonXposed.xposed.Module
|
||||
|
||||
/**
|
||||
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
|
||||
*/
|
||||
object FixResourcesModule : Module() {
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
if (packageName != TARGET_PACKAGE) {
|
||||
Resources::class.java.hookMethod(
|
||||
"getIdentifier", String::class.java, String::class.java, String::class.java
|
||||
) {
|
||||
before {
|
||||
if (args[2] == packageName) args[2] = TARGET_PACKAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
package GoonXposed.xposed.modules
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.XModuleResources
|
||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.Constants
|
||||
import GoonXposed.xposed.HookStateHolder
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
* Hooks React Native's script loading methods to load custom scripts and bundles.
|
||||
*
|
||||
* Preload scripts should be placed in the [PRELOADS_DIR] directory inside the module's files directory.
|
||||
*
|
||||
* The main bundle should be placed in the [Constants.CACHE_DIR] directory named [Constants.MAIN_SCRIPT_FILE].
|
||||
* If the bundle file does not exist, it will attempt to load `assets://Shiggy.bundle` from the module's assets.
|
||||
*/
|
||||
object HookScriptLoaderModule : Module() {
|
||||
private lateinit var preloadsDir: File
|
||||
private lateinit var mainScript: File
|
||||
|
||||
private lateinit var modulePath: String
|
||||
private lateinit var resources: XModuleResources
|
||||
|
||||
const val PRELOADS_DIR = "preloads"
|
||||
|
||||
override fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {
|
||||
this@HookScriptLoaderModule.modulePath = startupParam.modulePath
|
||||
}
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
val cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { asDir() }
|
||||
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { asDir() }
|
||||
|
||||
preloadsDir = File(filesDir, PRELOADS_DIR).apply { asDir() }
|
||||
mainScript = File(cacheDir, Constants.MAIN_SCRIPT_FILE).apply { asFile() }
|
||||
|
||||
listOf(
|
||||
"com.facebook.react.runtime.ReactInstance\$loadJSBundle$1",
|
||||
"com.facebook.react.runtime.ReactInstance$1",
|
||||
// TODO: Remove once Discord fully switches to Bridgeless
|
||||
"com.facebook.react.bridge.CatalystInstanceImpl"
|
||||
).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) }
|
||||
}
|
||||
|
||||
private fun hook(instance: Class<*>) = runCatching {
|
||||
val loadScriptFromAssets = instance.method(
|
||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||
)
|
||||
|
||||
val loadScriptFromFile = instance.method(
|
||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||
)
|
||||
|
||||
loadScriptFromAssets.hook {
|
||||
before {
|
||||
Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})")
|
||||
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
|
||||
}
|
||||
}
|
||||
|
||||
loadScriptFromFile.hook {
|
||||
before {
|
||||
Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})")
|
||||
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e("Failed to hook script loading methods in ${instance.name}:", it)
|
||||
}
|
||||
|
||||
private fun HookScope.runCustomScripts(loadScriptFromFile: Method, loadScriptFromAssets: Method) {
|
||||
Log.i("Running custom scripts...")
|
||||
|
||||
runBlocking {
|
||||
val ready = async { HookStateHolder.readyDeferred.join() }
|
||||
val download = async { UpdaterModule.downloadScript().join() }
|
||||
|
||||
awaitAll(ready, download)
|
||||
}
|
||||
|
||||
val loadSynchronously = args[2]
|
||||
val runScriptFile = { file: File ->
|
||||
Log.i("Loading script: ${file.absolutePath}")
|
||||
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
|
||||
)
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
try {
|
||||
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
|
||||
|
||||
if (mainScript.exists()) runScriptFile(mainScript)
|
||||
else {
|
||||
Log.i("Main script does not exist, falling back")
|
||||
|
||||
if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
|
||||
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromAssets,
|
||||
thisObject,
|
||||
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Unable to run scripts:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,601 +0,0 @@
|
|||
package GoonXposed.xposed.modules
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.BuildConfig
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import kotlinx.coroutines.*
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import GoonXposed.xposed.Utils.Companion.reloadApp
|
||||
|
||||
object LogBoxModule : Module() {
|
||||
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
this@LogBoxModule.packageParam = packageParam
|
||||
|
||||
val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost")
|
||||
val getUseDeveloperSupportMethod = dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" }
|
||||
|
||||
getUseDeveloperSupportMethod.hook {
|
||||
before {
|
||||
result = true
|
||||
}
|
||||
}
|
||||
|
||||
listOf(
|
||||
"com.facebook.react.devsupport.BridgeDevSupportManager",
|
||||
"com.facebook.react.devsupport.BridgelessDevSupportManager"
|
||||
).mapNotNull { packageParam.classLoader.safeLoadClass(it) }.forEach {
|
||||
hookDevSupportManager(it)
|
||||
}
|
||||
|
||||
return@with
|
||||
}
|
||||
|
||||
private fun hookDevSupportManager(clazz: Class<*>) {
|
||||
val showDevOptionsDialogMethod = clazz.methods.first { it.name == "showDevOptionsDialog" }
|
||||
|
||||
showDevOptionsDialogMethod.hook {
|
||||
before {
|
||||
try {
|
||||
val context = getContextFromDevSupport(clazz, thisObject)
|
||||
if (context != null) {
|
||||
showRecoveryMenu(context)
|
||||
}
|
||||
result = null
|
||||
} catch (e: Exception) {
|
||||
Log.e("Failed to show recovery menu: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? {
|
||||
return try {
|
||||
val mReactInstanceDevHelperField = XposedHelpers.findField(clazz, "mReactInstanceDevHelper")
|
||||
val helper = mReactInstanceDevHelperField.get(instance)
|
||||
val getCurrentActivityMethod = helper?.javaClass?.methods?.first { it.name == "getCurrentActivity" }
|
||||
getCurrentActivityMethod?.invoke(helper) as? Context
|
||||
} catch (e: Exception) {
|
||||
Log.e("Failed to get context: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun reloadReactNative(context: Context) {
|
||||
try {
|
||||
val reactInstanceManagerClass =
|
||||
packageParam.classLoader.loadClass("com.facebook.react.ReactInstanceManager")
|
||||
val reactApplicationClass = packageParam.classLoader.loadClass("com.facebook.react.ReactApplication")
|
||||
|
||||
val app = context.applicationContext
|
||||
|
||||
if (reactApplicationClass.isInstance(app)) {
|
||||
val getReactNativeHostMethod = reactApplicationClass.methods.first { it.name == "getReactNativeHost" }
|
||||
val reactNativeHost = getReactNativeHostMethod.invoke(app)
|
||||
|
||||
val getReactInstanceManagerMethod =
|
||||
reactNativeHost.javaClass.methods.first { it.name == "getReactInstanceManager" }
|
||||
val reactInstanceManager = getReactInstanceManagerMethod.invoke(reactNativeHost)
|
||||
|
||||
if (reactInstanceManager != null) {
|
||||
val reloadMethod =
|
||||
reactInstanceManagerClass.methods.first { it.name == "recreateReactContextInBackground" }
|
||||
reloadMethod.invoke(reactInstanceManager)
|
||||
} else {
|
||||
showError(context, "Error", "Could not get React Instance Manager")
|
||||
}
|
||||
} else {
|
||||
showError(context, "Error", "Application is not a React Application")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Failed to Reload App: ${e.message}")
|
||||
showError(context, "Failed to reload", "Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRecoveryMenu(context: Context) {
|
||||
val options = arrayOf(
|
||||
if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode",
|
||||
"Refetch Bundle",
|
||||
"Load Custom Bundle",
|
||||
"Reload App",
|
||||
"Clear Cache & Reset"
|
||||
)
|
||||
|
||||
val adapter =
|
||||
object : android.widget.ArrayAdapter<String>(context, android.R.layout.select_dialog_item, options) {
|
||||
override fun getView(
|
||||
position: Int,
|
||||
convertView: android.view.View?,
|
||||
parent: android.view.ViewGroup
|
||||
): android.view.View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
(view as? android.widget.TextView)?.apply {
|
||||
setTextColor(0xFFFFFFFF.toInt())
|
||||
textSize = 14f
|
||||
setPadding(
|
||||
(16 * context.resources.displayMetrics.density).toInt(),
|
||||
(12 * context.resources.displayMetrics.density).toInt(),
|
||||
(16 * context.resources.displayMetrics.density).toInt(),
|
||||
(12 * context.resources.displayMetrics.density).toInt()
|
||||
)
|
||||
setTypeface(typeface, android.graphics.Typeface.NORMAL)
|
||||
}
|
||||
view.setBackgroundColor(0xFF2D2D2D.toInt())
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle("GoonXposed Recovery Menu")
|
||||
.setAdapter(adapter) { _, which ->
|
||||
handleMenuSelection(context, which)
|
||||
}
|
||||
.setNegativeButton("Close", null)
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply {
|
||||
setTextColor(0xFFBB86FC.toInt())
|
||||
setBackgroundColor(0xFF1E1E1E.toInt())
|
||||
textSize = 12f
|
||||
setPadding(
|
||||
(20 * context.resources.displayMetrics.density).toInt(),
|
||||
(8 * context.resources.displayMetrics.density).toInt(),
|
||||
(20 * context.resources.displayMetrics.density).toInt(),
|
||||
(8 * context.resources.displayMetrics.density).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt()))
|
||||
|
||||
val titleId = context.resources.getIdentifier("alertTitle", "id", "android")
|
||||
val titleView = dialog.findViewById<android.widget.TextView>(titleId)
|
||||
titleView?.apply {
|
||||
setTextColor(0xFFBB86FC.toInt())
|
||||
textSize = 16f
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
}
|
||||
|
||||
dialog.listView?.setBackgroundColor(0xFF1E1E1E.toInt())
|
||||
dialog.listView?.divider = android.graphics.drawable.ColorDrawable(0xFF444444.toInt())
|
||||
dialog.listView?.dividerHeight = 1
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun handleMenuSelection(context: Context, index: Int) {
|
||||
when (index) {
|
||||
0 -> toggleSafeMode(context)
|
||||
1 -> showConfirmAction(
|
||||
context, "Refetch Bundle",
|
||||
"This will download the latest bundle from the official server."
|
||||
) { refetchBundle(context) }
|
||||
|
||||
2 -> showCustomBundleDialog(context)
|
||||
3 -> reloadApp()
|
||||
4 -> showConfirmAction(
|
||||
context, "Clear Cache & Reset",
|
||||
"This will clear all cached bundles and reset to default settings."
|
||||
) { clearCacheAndReset(context) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) {
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton("Confirm") { _, _ -> action() }
|
||||
.setNegativeButton("Cancel", null)
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply {
|
||||
setTextColor(0xFFFFFFFF.toInt())
|
||||
setBackgroundColor(0xFF1E1E1E.toInt())
|
||||
textSize = 12f
|
||||
}
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply {
|
||||
setTextColor(0xFFBB86FC.toInt())
|
||||
setBackgroundColor(0xFF1E1E1E.toInt())
|
||||
textSize = 12f
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
}
|
||||
|
||||
dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt()))
|
||||
|
||||
val titleId = context.resources.getIdentifier("alertTitle", "id", "android")
|
||||
val titleView = dialog.findViewById<android.widget.TextView>(titleId)
|
||||
titleView?.apply {
|
||||
setTextColor(0xFFBB86FC.toInt())
|
||||
textSize = 16f
|
||||
}
|
||||
|
||||
val messageId = context.resources.getIdentifier("message", "id", "android")
|
||||
val messageView = dialog.findViewById<android.widget.TextView>(messageId)
|
||||
messageView?.apply {
|
||||
setTextColor(0xFFFFFFFF.toInt())
|
||||
textSize = 14f
|
||||
setPadding(
|
||||
(5 * context.resources.displayMetrics.density).toInt(),
|
||||
paddingTop,
|
||||
(5 * context.resources.displayMetrics.density).toInt(),
|
||||
paddingBottom
|
||||
)
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun showCustomBundleDialog(context: Context) {
|
||||
val filesDir = File(context.filesDir, "pyoncord")
|
||||
val configFile = File(filesDir, "loader.json")
|
||||
var currentUrl: String? = null
|
||||
|
||||
if (configFile.exists()) {
|
||||
try {
|
||||
val json = JSONObject(configFile.readText())
|
||||
val customLoadUrl = json.optJSONObject("customLoadUrl")
|
||||
if (customLoadUrl != null && customLoadUrl.optBoolean("enabled", false)) {
|
||||
currentUrl = customLoadUrl.optString("url", "")
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
val urlInput = EditText(context).apply {
|
||||
hint = "http://localhost:4040/shiggycord.js"
|
||||
setTextColor(0xFFFFFFFF.toInt())
|
||||
setBackgroundColor(0xFF2D2D2D.toInt())
|
||||
setHintTextColor(0xFF888888.toInt())
|
||||
inputType = android.text.InputType.TYPE_TEXT_VARIATION_URI
|
||||
setText(currentUrl ?: "")
|
||||
gravity = android.view.Gravity.START
|
||||
textAlignment = android.view.View.TEXT_ALIGNMENT_VIEW_START
|
||||
textSize = 14f
|
||||
setPadding(
|
||||
(12 * context.resources.displayMetrics.density).toInt(),
|
||||
(10 * context.resources.displayMetrics.density).toInt(),
|
||||
(12 * context.resources.displayMetrics.density).toInt(),
|
||||
(10 * context.resources.displayMetrics.density).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
val container = android.widget.FrameLayout(context)
|
||||
val params = android.widget.FrameLayout.LayoutParams(
|
||||
(context.resources.displayMetrics.widthPixels * 0.85).toInt(),
|
||||
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
params.leftMargin = (15 * context.resources.displayMetrics.density).toInt()
|
||||
params.rightMargin = (15 * context.resources.displayMetrics.density).toInt()
|
||||
urlInput.layoutParams = params
|
||||
container.addView(urlInput)
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle("Custom Bundle URL")
|
||||
.setView(container)
|
||||
.setPositiveButton("Save", null)
|
||||
.setNegativeButton("Cancel", null)
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply {
|
||||
setTextColor(0xFFBB86FC.toInt())
|
||||
setBackgroundColor(0xFF1E1E1E.toInt())
|
||||
textSize = 12f
|
||||
}
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply {
|
||||
setTextColor(0xFFBB86FC.toInt())
|
||||
setBackgroundColor(0xFF1E1E1E.toInt())
|
||||
textSize = 12f
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
}
|
||||
|
||||
dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt()))
|
||||
|
||||
val titleId = context.resources.getIdentifier("alertTitle", "id", "android")
|
||||
val titleView = dialog.findViewById<android.widget.TextView>(titleId)
|
||||
titleView?.apply {
|
||||
setTextColor(0xFFBB86FC.toInt())
|
||||
textSize = 16f
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
|
||||
try {
|
||||
val url = urlInput.text?.toString()?.trim() ?: ""
|
||||
if (url.isNotEmpty()) {
|
||||
setCustomBundleURL(context, url)
|
||||
dialog.dismiss()
|
||||
} else {
|
||||
disableCustomBundle(context)
|
||||
dialog.dismiss()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, "Failed to set custom bundle: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(context: Context, title: String, message: String?) {
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message ?: "An unknown error occurred")
|
||||
.setPositiveButton("Close", null)
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply {
|
||||
setTextColor(0xFFBB86FC.toInt())
|
||||
setBackgroundColor(0xFF1E1E1E.toInt())
|
||||
textSize = 12f
|
||||
}
|
||||
|
||||
dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt()))
|
||||
|
||||
val titleId = context.resources.getIdentifier("alertTitle", "id", "android")
|
||||
val titleView = dialog.findViewById<android.widget.TextView>(titleId)
|
||||
titleView?.setTextColor(0xFFBB86FC.toInt())
|
||||
|
||||
val messageId = context.resources.getIdentifier("message", "id", "android")
|
||||
val messageView = dialog.findViewById<android.widget.TextView>(messageId)
|
||||
messageView?.setTextColor(0xFFFFFFFF.toInt())
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun clearCacheAndReset(context: Context) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val pyoncordDir = getPyoncordDirectory(context)
|
||||
val bundleFile = File(pyoncordDir, "bundle.js")
|
||||
val configFile = File(pyoncordDir, "loader.json")
|
||||
|
||||
bundleFile.delete()
|
||||
|
||||
if (configFile.exists()) {
|
||||
val config = JSONObject(configFile.readText())
|
||||
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
|
||||
customLoadUrl.put("enabled", false)
|
||||
customLoadUrl.put("url", "http://localhost:4040/shiggycord.js")
|
||||
config.put("customLoadUrl", customLoadUrl)
|
||||
configFile.writeText(config.toString())
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Cache cleared successfully", Toast.LENGTH_SHORT).show()
|
||||
reloadApp()
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("Error clearing cache: ${e.message}")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, "Failed to clear cache", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableCustomBundle(context: Context) {
|
||||
try {
|
||||
val configFile = File(getPyoncordDirectory(context), "loader.json")
|
||||
val config = if (configFile.exists()) {
|
||||
JSONObject(configFile.readText())
|
||||
} else {
|
||||
JSONObject().apply {
|
||||
put("loadReactDevTools", false)
|
||||
}
|
||||
}
|
||||
|
||||
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
|
||||
customLoadUrl.put("enabled", false)
|
||||
customLoadUrl.put("url", "http://localhost:4040/shiggycord.js")
|
||||
config.put("customLoadUrl", customLoadUrl)
|
||||
|
||||
configFile.writeText(config.toString())
|
||||
Toast.makeText(context, "Custom bundle disabled", Toast.LENGTH_SHORT).show()
|
||||
reloadApp()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, "Failed to disable custom bundle", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSafeModeEnabled(context: Context): Boolean {
|
||||
return try {
|
||||
val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS")
|
||||
if (!settingsFile.exists()) return false
|
||||
|
||||
val json = JSONObject(settingsFile.readText())
|
||||
json.optJSONObject("safeMode")?.optBoolean("enabled", false) ?: false
|
||||
} catch (e: Exception) {
|
||||
Log.e("Error checking safe mode: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSafeMode(context: Context) {
|
||||
try {
|
||||
val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS")
|
||||
val themeFile = File(context.filesDir, "vd_mmkv/VENDETTA_THEMES")
|
||||
|
||||
settingsFile.parentFile?.mkdirs()
|
||||
|
||||
val settings = if (settingsFile.exists()) {
|
||||
JSONObject(settingsFile.readText())
|
||||
} else {
|
||||
JSONObject()
|
||||
}
|
||||
|
||||
val safeMode = settings.optJSONObject("safeMode") ?: JSONObject()
|
||||
val currentState = safeMode.optBoolean("enabled", false)
|
||||
val newState = !currentState
|
||||
|
||||
safeMode.put("enabled", newState)
|
||||
|
||||
if (newState && themeFile.exists()) {
|
||||
val theme = JSONObject(themeFile.readText())
|
||||
val themeId = theme.optString("id")
|
||||
if (themeId.isNotEmpty()) {
|
||||
safeMode.put("currentThemeId", themeId)
|
||||
themeFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
settings.put("safeMode", safeMode)
|
||||
settingsFile.writeText(settings.toString())
|
||||
|
||||
Toast.makeText(context, "Safe Mode ${if (newState) "Enabled" else "Disabled"}", Toast.LENGTH_SHORT).show()
|
||||
|
||||
reloadApp()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("Error toggling safe mode: ${e.message}")
|
||||
showError(context, "Failed to toggle safe mode", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun refetchBundle(context: Context) {
|
||||
try {
|
||||
val pyoncordDir = getPyoncordDirectory(context)
|
||||
val bundleFile = File(pyoncordDir, "bundle.js")
|
||||
val backupFile = File(pyoncordDir, "bundle.js.backup")
|
||||
|
||||
if (bundleFile.exists()) {
|
||||
backupFile.delete()
|
||||
bundleFile.renameTo(backupFile)
|
||||
Log.e("Bundle moved to backup")
|
||||
}
|
||||
|
||||
reloadApp()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("Error refetching bundle: ${e.message}")
|
||||
showError(context, "Failed to refetch bundle", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetBundle(context: Context) {
|
||||
try {
|
||||
val pyoncordDir = getPyoncordDirectory(context)
|
||||
val bundleFile = File(pyoncordDir, "bundle.js")
|
||||
val configFile = File(pyoncordDir, "loader_config.json")
|
||||
|
||||
bundleFile.delete()
|
||||
|
||||
if (configFile.exists()) {
|
||||
val config = JSONObject(configFile.readText())
|
||||
config.put("customLoadUrlEnabled", false)
|
||||
config.put("customLoadUrl", "http://localhost:4040/ShiggyCord.js")
|
||||
configFile.writeText(config.toString())
|
||||
}
|
||||
|
||||
reloadApp()
|
||||
} catch (e: Exception) {
|
||||
Log.e("Error resetting bundle: ${e.message}")
|
||||
showError(context, "Failed to reset bundle", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateAndLoadBundle(context: Context, urlString: String) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val url = URL(urlString)
|
||||
if (url.protocol != "http" && url.protocol != "https") {
|
||||
withContext(Dispatchers.Main) {
|
||||
showError(context, "Invalid URL", "URL must start with http:// or https://")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 10000
|
||||
connection.readTimeout = 10000
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode != 200) {
|
||||
withContext(Dispatchers.Main) {
|
||||
showError(context, "Error", "Server returned error $responseCode")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val contentType = connection.contentType
|
||||
if (contentType != null && !contentType.contains("javascript") && !contentType.contains("text")) {
|
||||
withContext(Dispatchers.Main) {
|
||||
showError(context, "Invalid Content", "URL must point to a JavaScript file")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
setCustomBundleURL(context, urlString)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
showError(context, "Connection Error", "Could not reach URL: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCustomBundleURL(context: Context, url: String) {
|
||||
try {
|
||||
val pyoncordDir = getPyoncordDirectory(context)
|
||||
val configFile = File(pyoncordDir, "loader.json")
|
||||
|
||||
val config = if (configFile.exists()) {
|
||||
JSONObject(configFile.readText())
|
||||
} else {
|
||||
JSONObject().apply {
|
||||
put("loadReactDevTools", false)
|
||||
}
|
||||
}
|
||||
|
||||
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
|
||||
customLoadUrl.put("enabled", true)
|
||||
customLoadUrl.put("url", url)
|
||||
config.put("customLoadUrl", customLoadUrl)
|
||||
|
||||
configFile.writeText(config.toString())
|
||||
|
||||
File(pyoncordDir, "bundle.js").delete()
|
||||
|
||||
Toast.makeText(context, "Custom bundle set", Toast.LENGTH_SHORT).show()
|
||||
|
||||
reloadApp()
|
||||
} catch (e: Exception) {
|
||||
Log.e("Error setting custom bundle URL: ${e.message}")
|
||||
showError(context, "Failed to save configuration", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPyoncordDirectory(context: Context): File {
|
||||
val dir = File(context.filesDir, "pyoncord")
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs()
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
override fun onContext(context: Context) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
package GoonXposed.xposed.modules
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.BuildConfig
|
||||
import GoonXposed.xposed.Constants
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Companion.JSON
|
||||
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.io.File
|
||||
|
||||
// TODO: Remove this redundant module and merge its functionality into AdditionalBridgeMethodsModule if needed
|
||||
class PayloadGlobalModule(private val modules: List<Module>) : Module() {
|
||||
private companion object {
|
||||
const val GLOBAL_NAME = "__PYON_LOADER__"
|
||||
}
|
||||
|
||||
private fun getPayloadString(): String = JSON.encodeToString(
|
||||
buildJsonObject {
|
||||
put("loaderName", Constants.LOADER_NAME)
|
||||
put("loaderVersion", BuildConfig.VERSION_NAME)
|
||||
@Suppress("DEPRECATION") for (module in modules) module.buildPayload(this)
|
||||
})
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
val catalystInstance = classLoader.safeLoadClass("com.facebook.react.bridge.CatalystInstanceImpl")
|
||||
val scriptLoader = classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance\$loadJSBundle$1")
|
||||
?: classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance$1")
|
||||
|
||||
val setGlobalVariable: (XC_MethodHook.MethodHookParam, String, String) -> Unit = { param, key, json ->
|
||||
runCatching {
|
||||
// Attempt old CatalystInstanceImpl method
|
||||
catalystInstance!!.method(
|
||||
"setGlobalVariable", String::class.java, String::class.java
|
||||
).invoke(param.thisObject, key, json)
|
||||
}.onFailure {
|
||||
// Failed (likely because it's a stub), use a workaround
|
||||
val preloadsDir = File("${appInfo.dataDir}/${Constants.FILES_DIR}", PRELOADS_DIR)
|
||||
File(preloadsDir, "rv_globals_$key.js").apply {
|
||||
writeText("this[${JSON.encodeToString(key)}]=$json")
|
||||
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
scriptLoader!!.method(
|
||||
"loadScriptFromFile",
|
||||
String::class.java,
|
||||
String::class.java,
|
||||
Boolean::class.javaPrimitiveType
|
||||
), param.thisObject, arrayOf(absolutePath, absolutePath, param.args[2])
|
||||
)
|
||||
|
||||
delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val hook = MethodHookBuilder().run {
|
||||
before {
|
||||
setGlobalVariable(param, GLOBAL_NAME, getPayloadString())
|
||||
}
|
||||
|
||||
build()
|
||||
}
|
||||
|
||||
listOf(catalystInstance, scriptLoader).forEach { if (it != null) hook(it, hook) }
|
||||
}
|
||||
|
||||
private fun hook(instance: Class<*>, hook: XC_MethodHook) {
|
||||
instance.method(
|
||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType,
|
||||
).hook(hook)
|
||||
|
||||
instance.method(
|
||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||
).hook(hook)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
package GoonXposed.xposed.modules
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import android.widget.Toast
|
||||
import androidx.core.util.writeBytes
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.Constants
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils
|
||||
import GoonXposed.xposed.Utils.Companion.JSON
|
||||
import GoonXposed.xposed.Utils.Companion.reloadApp
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
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 java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@Serializable
|
||||
data class CustomLoadUrl(
|
||||
val enabled: Boolean = false, val url: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoaderConfig(
|
||||
val customLoadUrl: CustomLoadUrl = CustomLoadUrl()
|
||||
)
|
||||
|
||||
/**
|
||||
* Module that updates the JS bundle by downloading it from a remote URL.
|
||||
*
|
||||
* Shows dialogs when failed allowing retry.
|
||||
*/
|
||||
object UpdaterModule : Module() {
|
||||
private lateinit var config: LoaderConfig
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var lastActivity: WeakReference<Activity>? = null
|
||||
|
||||
private lateinit var cacheDir: File
|
||||
private lateinit var bundle: File
|
||||
private lateinit var etag: File
|
||||
|
||||
private const val TIMEOUT_CACHED = 5000L
|
||||
private const val TIMEOUT = 10000L
|
||||
private const val ETAG_FILE = "etag.txt"
|
||||
private const val CONFIG_FILE = "loader.json"
|
||||
|
||||
private const val DEFAULT_BUNDLE_URL =
|
||||
"https://github.com/OTKUSteyler/GoonManager/releases/latest/download/gooncord.js"
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() }
|
||||
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
|
||||
|
||||
bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE)
|
||||
etag = File(cacheDir, ETAG_FILE)
|
||||
|
||||
val configFile = File(filesDir, CONFIG_FILE)
|
||||
|
||||
config = runCatching {
|
||||
if (configFile.exists()) {
|
||||
JSON.decodeFromString<LoaderConfig>(configFile.readText())
|
||||
} else LoaderConfig()
|
||||
}.getOrDefault(LoaderConfig())
|
||||
}
|
||||
|
||||
fun downloadScript(activity: Activity? = null): Job = scope.launch {
|
||||
try {
|
||||
HttpClient(CIO) {
|
||||
expectSuccess = false
|
||||
install(UserAgent) { agent = Constants.USER_AGENT }
|
||||
install(HttpRedirect) {}
|
||||
}.use { client ->
|
||||
val url = config.customLoadUrl.takeIf { it.enabled }?.url ?: DEFAULT_BUNDLE_URL
|
||||
Log.i("Fetching JS bundle from: $url")
|
||||
|
||||
val response: HttpResponse = client.get(url) {
|
||||
headers {
|
||||
if (etag.exists() && bundle.exists()) {
|
||||
append(HttpHeaders.IfNoneMatch, etag.readText())
|
||||
}
|
||||
}
|
||||
|
||||
// Retries don't need timeout
|
||||
if (activity == null) {
|
||||
timeout {
|
||||
requestTimeoutMillis = if (!bundle.exists()) TIMEOUT else TIMEOUT_CACHED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val bytes: ByteArray = response.body()
|
||||
AtomicFile(bundle).writeBytes(bytes)
|
||||
|
||||
val newTag = response.headers[HttpHeaders.ETag]
|
||||
if (!newTag.isNullOrEmpty()) etag.writeText(newTag) else etag.delete()
|
||||
|
||||
Log.i("Bundle updated (${bytes.size} bytes)")
|
||||
|
||||
// This is a retry, so we show a dialog
|
||||
if (activity != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
AlertDialog.Builder(activity).setTitle("GoonCord Update Successful")
|
||||
.setMessage("A reload is required for changes to take effect.")
|
||||
.setPositiveButton("Reload") { dialog, _ ->
|
||||
reloadApp()
|
||||
dialog.dismiss()
|
||||
}.setCancelable(false).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpStatusCode.NotModified -> {
|
||||
Log.i("Server responded with 304, no changes")
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw ResponseException(response, "Received status: ${response.status}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Failed to download script", e)
|
||||
showErrorDialog(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivity(activity: Activity) {
|
||||
lastActivity = WeakReference(activity)
|
||||
}
|
||||
|
||||
fun showErrorDialog(e: Throwable) {
|
||||
val activity = lastActivity?.get() ?: return
|
||||
}
|
||||
|
||||
fun resetLoaderConfig(context: Context) {
|
||||
val filesDir = File(context.dataDir, Constants.FILES_DIR)
|
||||
val config = File(filesDir, CONFIG_FILE)
|
||||
if (config.exists()) config.delete()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue