Begin rewrite
This commit is contained in:
parent
4e06328dcd
commit
1d280d78da
8 changed files with 590 additions and 165 deletions
53
.github/workflows/build.yml
vendored
Normal file
53
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: Build APK
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and push
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: "Setup Gradle"
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: "Grant execution permission"
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: "Build APKs"
|
||||
run: ./gradlew build
|
||||
|
||||
- name: "Setup directories"
|
||||
run: |
|
||||
mkdir -p /tmp/pyonbuild/apks
|
||||
mkdir -p /tmp/pyonbuild/tools
|
||||
|
||||
- name: "Move debug APK"
|
||||
run: mv ./app/build/outputs/apk/debug/app-debug.apk /tmp/pyonbuild/apks
|
||||
|
||||
- name: "Download uber-apk-signer"
|
||||
run: wget -nv "https://github.com/patrickfav/uber-apk-signer/releases/download/v1.2.1/uber-apk-signer-1.2.1.jar" -O /tmp/pyonbuild/tools/uber-apk-signer.jar
|
||||
|
||||
- name: "Sign release APK"
|
||||
run: java -jar /tmp/pyonbuild/tools/uber-apk-signer.jar --apks ./app/build/outputs/apk/release/app-release-unsigned.apk --out /tmp/pyonbuild/apks/
|
||||
|
||||
- name: "Rename release APK"
|
||||
run: mv /tmp/pyonbuild/apks/app-release-aligned-debugSigned.apk /tmp/pyonbuild/apks/app-release.apk
|
||||
|
||||
- name: "Upload debug APK"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: app-debug
|
||||
path: /tmp/pyonbuild/apks/app-debug.apk
|
||||
|
||||
- name: "Upload release APK"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: app-release
|
||||
path: /tmp/pyonbuild/apks/app-release.apk
|
||||
172
LICENSE
Normal file
172
LICENSE
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
Open Software License ("OSL") v. 3.0
|
||||
|
||||
This Open Software License (the "License") applies to any original work of
|
||||
authorship (the "Original Work") whose owner (the "Licensor") has placed the
|
||||
following licensing notice adjacent to the copyright notice for the Original
|
||||
Work:
|
||||
|
||||
Licensed under the Open Software License version 3.0
|
||||
|
||||
1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free,
|
||||
non-exclusive, sublicensable license, for the duration of the copyright, to do
|
||||
the following:
|
||||
|
||||
a) to reproduce the Original Work in copies, either alone or as part of a
|
||||
collective work;
|
||||
|
||||
b) to translate, adapt, alter, transform, modify, or arrange the Original
|
||||
Work, thereby creating derivative works ("Derivative Works") based upon the
|
||||
Original Work;
|
||||
|
||||
c) to distribute or communicate copies of the Original Work and Derivative
|
||||
Works to the public, with the proviso that copies of Original Work or
|
||||
Derivative Works that You distribute or communicate shall be licensed under
|
||||
this Open Software License;
|
||||
|
||||
d) to perform the Original Work publicly; and
|
||||
|
||||
e) to display the Original Work publicly.
|
||||
|
||||
2) Grant of Patent License. Licensor grants You a worldwide, royalty-free,
|
||||
non-exclusive, sublicensable license, under patent claims owned or controlled
|
||||
by the Licensor that are embodied in the Original Work as furnished by the
|
||||
Licensor, for the duration of the patents, to make, use, sell, offer for sale,
|
||||
have made, and import the Original Work and Derivative Works.
|
||||
|
||||
3) Grant of Source Code License. The term "Source Code" means the preferred
|
||||
form of the Original Work for making modifications to it and all available
|
||||
documentation describing how to modify the Original Work. Licensor agrees to
|
||||
provide a machine-readable copy of the Source Code of the Original Work along
|
||||
with each copy of the Original Work that Licensor distributes. Licensor
|
||||
reserves the right to satisfy this obligation by placing a machine-readable
|
||||
copy of the Source Code in an information repository reasonably calculated to
|
||||
permit inexpensive and convenient access by You for as long as Licensor
|
||||
continues to distribute the Original Work.
|
||||
|
||||
4) Exclusions From License Grant. Neither the names of Licensor, nor the names
|
||||
of any contributors to the Original Work, nor any of their trademarks or
|
||||
service marks, may be used to endorse or promote products derived from this
|
||||
Original Work without express prior permission of the Licensor. Except as
|
||||
expressly stated herein, nothing in this License grants any license to
|
||||
Licensor's trademarks, copyrights, patents, trade secrets or any other
|
||||
intellectual property. No patent license is granted to make, use, sell, offer
|
||||
for sale, have made, or import embodiments of any patent claims other than the
|
||||
licensed claims defined in Section 2. No license is granted to the trademarks
|
||||
of Licensor even if such marks are included in the Original Work. Nothing in
|
||||
this License shall be interpreted to prohibit Licensor from licensing under
|
||||
terms different from this License any Original Work that Licensor otherwise
|
||||
would have a right to license.
|
||||
|
||||
5) External Deployment. The term "External Deployment" means the use,
|
||||
distribution, or communication of the Original Work or Derivative Works in any
|
||||
way such that the Original Work or Derivative Works may be used by anyone
|
||||
other than You, whether those works are distributed or communicated to those
|
||||
persons or made available as an application intended for use over a network.
|
||||
As an express condition for the grants of license hereunder, You must treat
|
||||
any External Deployment by You of the Original Work or a Derivative Work as a
|
||||
distribution under section 1(c).
|
||||
|
||||
6) Attribution Rights. You must retain, in the Source Code of any Derivative
|
||||
Works that You create, all copyright, patent, or trademark notices from the
|
||||
Source Code of the Original Work, as well as any notices of licensing and any
|
||||
descriptive text identified therein as an "Attribution Notice." You must cause
|
||||
the Source Code for any Derivative Works that You create to carry a prominent
|
||||
Attribution Notice reasonably calculated to inform recipients that You have
|
||||
modified the Original Work.
|
||||
|
||||
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
|
||||
the copyright in and to the Original Work and the patent rights granted herein
|
||||
by Licensor are owned by the Licensor or are sublicensed to You under the
|
||||
terms of this License with the permission of the contributor(s) of those
|
||||
copyrights and patent rights. Except as expressly stated in the immediately
|
||||
preceding sentence, the Original Work is provided under this License on an "AS
|
||||
IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without
|
||||
limitation, the warranties of non-infringement, merchantability or fitness for
|
||||
a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK
|
||||
IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this
|
||||
License. No license to the Original Work is granted by this License except
|
||||
under this disclaimer.
|
||||
|
||||
8) Limitation of Liability. Under no circumstances and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise, shall the
|
||||
Licensor be liable to anyone for any indirect, special, incidental, or
|
||||
consequential damages of any character arising as a result of this License or
|
||||
the use of the Original Work including, without limitation, damages for loss
|
||||
of goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses. This limitation of liability shall not
|
||||
apply to the extent applicable law prohibits such limitation.
|
||||
|
||||
9) Acceptance and Termination. If, at any time, You expressly assented to this
|
||||
License, that assent indicates your clear and irrevocable acceptance of this
|
||||
License and all of its terms and conditions. If You distribute or communicate
|
||||
copies of the Original Work or a Derivative Work, You must make a reasonable
|
||||
effort under the circumstances to obtain the express assent of recipients to
|
||||
the terms of this License. This License conditions your rights to undertake
|
||||
the activities listed in Section 1, including your right to create Derivative
|
||||
Works based upon the Original Work, and doing so without honoring these terms
|
||||
and conditions is prohibited by copyright law and international treaty.
|
||||
Nothing in this License is intended to affect copyright exceptions and
|
||||
limitations (including "fair use" or "fair dealing"). This License shall
|
||||
terminate immediately and You may no longer exercise any of the rights granted
|
||||
to You by this License upon your failure to honor the conditions in Section
|
||||
1(c).
|
||||
|
||||
10) Termination for Patent Action. This License shall terminate automatically
|
||||
and You may no longer exercise any of the rights granted to You by this
|
||||
License as of the date You commence an action, including a cross-claim or
|
||||
counterclaim, against Licensor or any licensee alleging that the Original Work
|
||||
infringes a patent. This termination provision shall not apply for an action
|
||||
alleging patent infringement by combinations of the Original Work with other
|
||||
software or hardware.
|
||||
|
||||
11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
|
||||
License may be brought only in the courts of a jurisdiction wherein the
|
||||
Licensor resides or in which Licensor conducts its primary business, and under
|
||||
the laws of that jurisdiction excluding its conflict-of-law provisions. The
|
||||
application of the United Nations Convention on Contracts for the
|
||||
International Sale of Goods is expressly excluded. Any use of the Original
|
||||
Work outside the scope of this License or after its termination shall be
|
||||
subject to the requirements and penalties of copyright or patent law in the
|
||||
appropriate jurisdiction. This section shall survive the termination of this
|
||||
License.
|
||||
|
||||
12) Attorneys' Fees. In any action to enforce the terms of this License or
|
||||
seeking damages relating thereto, the prevailing party shall be entitled to
|
||||
recover its costs and expenses, including, without limitation, reasonable
|
||||
attorneys' fees and costs incurred in connection with such action, including
|
||||
any appeal of such action. This section shall survive the termination of this
|
||||
License.
|
||||
|
||||
13) Miscellaneous. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent necessary
|
||||
to make it enforceable.
|
||||
|
||||
14) Definition of "You" in This License. "You" throughout this License,
|
||||
whether in upper or lower case, means an individual or a legal entity
|
||||
exercising rights under, and complying with all of the terms of, this License.
|
||||
For legal entities, "You" includes any entity that controls, is controlled by,
|
||||
or is under common control with you. For purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the direction or
|
||||
management of such entity, whether by contract or otherwise, or (ii) ownership
|
||||
of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
|
||||
ownership of such entity.
|
||||
|
||||
15) Right to Use. You may use the Original Work in all ways not otherwise
|
||||
restricted or conditioned by this License or by law, and Licensor promises not
|
||||
to interfere with or be responsible for such uses by You.
|
||||
|
||||
16) Modification of This License. This License is Copyright © 2005 Lawrence
|
||||
Rosen. Permission is granted to copy, distribute, or communicate this License
|
||||
without modification. Nothing in this License permits You to modify this
|
||||
License as applied to the Original Work or to Derivative Works. However, You
|
||||
may modify the text of this License and copy, distribute or communicate your
|
||||
modified version (the "Modified License") and apply it to other original works
|
||||
of authorship subject to the following conditions: (i) You may not indicate in
|
||||
any way that your Modified License is the "Open Software License" or "OSL" and
|
||||
you may not use those names in the name of your Modified License; (ii) You
|
||||
must replace the notice specified in the first paragraph above with the notice
|
||||
"Licensed under <insert your license name here>" or with a notice of your own
|
||||
that is not confusingly similar to the notice in this License; and (iii) You
|
||||
may not claim that your original works are open source software unless your
|
||||
Modified License has been approved by Open Source Initiative (OSI) and You
|
||||
comply with its license review and certification process.
|
||||
|
|
@ -13,7 +13,7 @@ android {
|
|||
minSdk = 24
|
||||
targetSdk = 33
|
||||
versionCode = 1
|
||||
versionName = "1.0.0-rewrite"
|
||||
versionName = "0.1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -37,6 +37,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
compileOnly("de.robv.android.xposed:api:82")
|
||||
implementation("androidx.core:core:1.10.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
|
||||
implementation("io.ktor:ktor-client-core:2.3.0")
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
let originalObjectCreate = Object.create
|
||||
|
||||
Object.create = function create(proto) {
|
||||
let ret = originalObjectCreate.apply(this, arguments)
|
||||
if (proto === null) {
|
||||
Object.create = originalObjectCreate
|
||||
return modules = ret
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
Object.freeze = Object
|
||||
|
|
@ -1,97 +1,118 @@
|
|||
package com.pyoncord.xposed
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.Context
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Resources
|
||||
import android.content.res.XModuleResources
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.fonts.Font
|
||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
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.engine.cio.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.lang.Exception
|
||||
import java.lang.System
|
||||
import java.util.HashMap
|
||||
|
||||
class Main : IXposedHookZygoteInit, IXposedHookLoadPackage {
|
||||
companion object {
|
||||
const val LOG_TAG = "Pyoncord"
|
||||
const val DEFAULT_BUNDLE_ENDPOINT = "https://raw.githubusercontent.com/pyoncord/pyoncord/builds/pyoncord.js"
|
||||
@Serializable
|
||||
data class CustomLoadUrl(
|
||||
val enabled: Boolean,
|
||||
val url: String
|
||||
)
|
||||
@Serializable
|
||||
data class LoaderConfig(
|
||||
val customLoadUrl: CustomLoadUrl,
|
||||
val loadReactDevTools: Boolean? = null,
|
||||
val loadPyoncord: Boolean? = null
|
||||
)
|
||||
|
||||
val PYONCORD_DIR = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "pyoncord").apply { mkdirs() }
|
||||
class Main : IXposedHookLoadPackage {
|
||||
private val pyonModules: Array<PyonModule> = arrayOf(
|
||||
ThemeModule(),
|
||||
SysColorsModule()
|
||||
)
|
||||
|
||||
val CACHE_DIR = File(PYONCORD_DIR, "cache").apply { mkdir() }
|
||||
val DRAWABLES_DIR = File(PYONCORD_DIR, "drawables").apply { mkdir() }
|
||||
fun buildLoaderJsonString(): String {
|
||||
val obj = buildJsonObject {
|
||||
put("loaderName", "PyoncordXposed")
|
||||
|
||||
val bundle = File(PYONCORD_DIR, "pyoncord.js")
|
||||
val etag = File(PYONCORD_DIR, "etag")
|
||||
|
||||
val loaderPayloadJson = buildJsonObject {
|
||||
put("loader", "PyoncordXposed")
|
||||
for (module in pyonModules) {
|
||||
module.buildJson(this)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return Json.encodeToString(obj)
|
||||
}
|
||||
|
||||
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
|
||||
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with (param) {
|
||||
val catalystInstanceImpl = runCatching {
|
||||
classLoader.loadClass("com.facebook.react.bridge.CatalystInstanceImpl")
|
||||
}.getOrElse { return@with }
|
||||
|
||||
XposedBridge.hookMethod(reactActivity.getDeclaredMethod("onCreate", Bundle::class.java), object : XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
appActivity = param.thisObject as Activity
|
||||
for (module in pyonModules) module.onInit(param)
|
||||
|
||||
init(lpparam)
|
||||
}
|
||||
})
|
||||
val loadScriptFromAssets = catalystInstanceImpl.getDeclaredMethod(
|
||||
"jniLoadScriptFromAssets",
|
||||
AssetManager::class.java,
|
||||
String::class.java,
|
||||
Boolean::class.javaPrimitiveType
|
||||
).apply { isAccessible = true }
|
||||
|
||||
val loadScriptFromFile = catalystInstanceImpl.getDeclaredMethod(
|
||||
"jniLoadScriptFromFile",
|
||||
String::class.java,
|
||||
String::class.java,
|
||||
Boolean::class.javaPrimitiveType
|
||||
).apply { isAccessible = true }
|
||||
|
||||
val setGlobalVariable = catalystInstanceImpl.getDeclaredMethod(
|
||||
"setGlobalVariable",
|
||||
String::class.java,
|
||||
String::class.java
|
||||
).apply { isAccessible = true }
|
||||
|
||||
val cacheDir = File(appInfo.dataDir, "cache/pyoncord").apply { mkdirs() }
|
||||
val filesDir = File(appInfo.dataDir, "files/pyoncord").apply { mkdirs() }
|
||||
|
||||
val preloadsDir = File(filesDir, "preloads").apply { mkdirs() }
|
||||
val bundle = File(cacheDir, "bundle.js")
|
||||
val etag = File(cacheDir, "etag.txt")
|
||||
|
||||
val configFile = File(filesDir, "loader.json")
|
||||
|
||||
val config = try {
|
||||
if (!configFile.exists()) throw Exception()
|
||||
Json { ignoreUnknownKeys = true }.decodeFromString(configFile.readText())
|
||||
} catch (_: Exception) {
|
||||
LoaderConfig(
|
||||
customLoadUrl = CustomLoadUrl(
|
||||
enabled = false,
|
||||
url = "http://localhost:4040/bunny.js"
|
||||
),
|
||||
loadReactDevTools = false,
|
||||
loadPyoncord = false
|
||||
)
|
||||
}
|
||||
|
||||
fun createEvalFile(code: String, identifier: String) = File(CACHE_DIR, "$identifier.js").apply { writeText(code) }
|
||||
|
||||
fun restartApp() = with(appActivity.applicationContext) {
|
||||
val intent = packageManager.getLaunchIntentForPackage(packageName)!!
|
||||
|
||||
appActivity.startActivity(Intent.makeRestartActivityTask(intent.component))
|
||||
System.exit(0)
|
||||
}
|
||||
|
||||
suspend fun checkForUpdate(): Unit = withContext(Dispatchers.IO) {
|
||||
val scope = MainScope()
|
||||
val httpJob = scope.async(Dispatchers.IO) {
|
||||
try {
|
||||
val client = HttpClient(CIO) {
|
||||
install(HttpTimeout) { requestTimeoutMillis = 1000 }
|
||||
install(UserAgent) { agent = "PyoncordXposed" }
|
||||
install(UserAgent) { agent = "PyonXposed" }
|
||||
}
|
||||
|
||||
try {
|
||||
val response: HttpResponse = client.get(DEFAULT_BUNDLE_ENDPOINT) {
|
||||
val url =
|
||||
if (config.customLoadUrl.enabled) config.customLoadUrl.url
|
||||
else if (config.loadPyoncord == true) "https://raw.githubusercontent.com/pyoncord/pyoncord/builds/pyoncord.js"
|
||||
else "https://raw.githubusercontent.com/pyoncord/detta-builds/main/bunny.js"
|
||||
|
||||
val response: HttpResponse = client.get(url) {
|
||||
headers {
|
||||
if (etag.exists() && bundle.exists()) {
|
||||
append(HttpHeaders.IfNoneMatch, etag.readText())
|
||||
|
|
@ -100,92 +121,62 @@ class Main : IXposedHookZygoteInit, IXposedHookLoadPackage {
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
return@async
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "An error occured while downloading pyoncord bundle.", e)
|
||||
} finally {
|
||||
client.close()
|
||||
Log.e("Pyoncord", "Failed to download Pyoncord")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
fun init(param: XC_LoadPackage.LoadPackageParam) = with(param) {
|
||||
val catalystInstance = classLoader.loadClass("com.facebook.react.bridge.CatalystInstanceImpl")
|
||||
|
||||
MainScope().launch() {
|
||||
checkForUpdate()
|
||||
}
|
||||
|
||||
runCatching {
|
||||
// Fight package renaming side effects
|
||||
if (packageName != "com.discord") {
|
||||
val getIdentifier = Resources::class.java.getDeclaredMethod("getIdentifier", String::class.java, String::class.java, String::class.java)
|
||||
|
||||
XposedBridge.hookMethod(getIdentifier, object: XC_MethodHook() {
|
||||
val patch = object : XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
if (param.args[2] == packageName) param.args[2] = "com.discord"
|
||||
}
|
||||
})
|
||||
}
|
||||
runBlocking { httpJob.join() }
|
||||
|
||||
// Custom drawables
|
||||
val uriCache = mutableMapOf<String, Uri>()
|
||||
val resourceDrawableIdHelper = classLoader.loadClass("com.facebook.react.views.imagehelper.ResourceDrawableIdHelper")
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
setGlobalVariable,
|
||||
param.thisObject,
|
||||
arrayOf("__PYON_LOADER__", buildLoaderJsonString())
|
||||
)
|
||||
|
||||
XposedBridge.hookMethod(resourceDrawableIdHelper.getDeclaredMethod("getResourceDrawableUri", Context::class.java, String::class.java), object: XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
val name = param.args[1] as? String ?: return
|
||||
|
||||
val uri = uriCache.getOrPut(name) {
|
||||
File(DRAWABLES_DIR, "$name.png").takeIf { it.exists() }?.let {
|
||||
Uri.fromFile(it)
|
||||
} ?: Uri.EMPTY
|
||||
}
|
||||
|
||||
if (uri != Uri.EMPTY) param.result = uri
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// TODO: Loader config
|
||||
val hook = object: XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) = with(param) {
|
||||
val payloadFile = createEvalFile("PYONCORD_LOADER_PAYLOAD=${Json.encodeToString(loaderPayloadJson)}", "payload")
|
||||
|
||||
// Load pre-patches and payload
|
||||
XposedBridge.invokeOriginalMethod(loadScriptFromAssets, thisObject, arrayOf(resources.assets, "assets://js/preinit.js", true))
|
||||
preloadsDir
|
||||
.walk()
|
||||
.filter { it.isFile && it.extension == "js" }
|
||||
.forEach { file ->
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromFile,
|
||||
thisObject,
|
||||
arrayOf(payloadFile.absolutePath, "pyoncord-payload", true)
|
||||
param.thisObject,
|
||||
arrayOf(file.absolutePath, file.absolutePath, param.args[2])
|
||||
)
|
||||
}
|
||||
|
||||
// Invoke the original method
|
||||
XposedBridge.invokeOriginalMethod(method, thisObject, args)
|
||||
|
||||
// Execute our bundle
|
||||
if (bundle.exists()) XposedBridge.invokeOriginalMethod(
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromFile,
|
||||
thisObject,
|
||||
arrayOf(bundle.absolutePath, bundle.absolutePath, args[2])
|
||||
param.thisObject,
|
||||
arrayOf(bundle.absolutePath, bundle.absolutePath, param.args[2])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
XposedBridge.hookMethod(loadScriptFromAssets, patch)
|
||||
XposedBridge.hookMethod(loadScriptFromFile, patch)
|
||||
|
||||
// Fighting the side effects of changing the package name
|
||||
if (packageName != "com.discord") {
|
||||
val getIdentifier = Resources::class.java.getDeclaredMethod(
|
||||
"getIdentifier",
|
||||
String::class.java,
|
||||
String::class.java,
|
||||
String::class.java
|
||||
)
|
||||
|
||||
// Prevent from invoking the original method
|
||||
setResult(null)
|
||||
XposedBridge.hookMethod(getIdentifier, object: XC_MethodHook() {
|
||||
override fun beforeHookedMethod(mhparam: MethodHookParam) = with(mhparam) {
|
||||
if (args[2] == param.packageName) args[2] = "com.discord"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
XposedBridge.hookMethod(loadScriptFromAssets, hook)
|
||||
XposedBridge.hookMethod(loadScriptFromFile, hook)
|
||||
|
||||
return@with
|
||||
}
|
||||
}
|
||||
9
app/src/main/kotlin/com/pyoncord/xposed/PyonModule.kt
Normal file
9
app/src/main/kotlin/com/pyoncord/xposed/PyonModule.kt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package com.pyoncord.xposed
|
||||
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
|
||||
abstract class PyonModule {
|
||||
open fun buildJson(builder: JsonObjectBuilder) {}
|
||||
open fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) {}
|
||||
}
|
||||
50
app/src/main/kotlin/com/pyoncord/xposed/SysColorsModule.kt
Normal file
50
app/src/main/kotlin/com/pyoncord/xposed/SysColorsModule.kt
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package com.pyoncord.xposed
|
||||
|
||||
import android.app.AndroidAppHelper
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
@Serializable
|
||||
data class SysColors(
|
||||
val neutral1: List<String>,
|
||||
val neutral2: List<String>,
|
||||
val accent1: List<String>,
|
||||
val accent2: List<String>,
|
||||
val accent3: List<String>
|
||||
)
|
||||
|
||||
class SysColorsModule : PyonModule() {
|
||||
private lateinit var context: Context
|
||||
fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
||||
override fun buildJson(builder: JsonObjectBuilder) {
|
||||
context = AndroidAppHelper.currentApplication()
|
||||
val accents = arrayOf("accent1", "accent2", "accent3", "neutral1", "neutral2")
|
||||
val shades = arrayOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
|
||||
|
||||
builder.apply {
|
||||
put("isSysColorsSupported", isSupported())
|
||||
if (isSupported()) putJsonObject("sysColors") {
|
||||
for (accent in accents) putJsonArray(accent) {
|
||||
for (shade in shades) {
|
||||
val colorName = "system_" + accent + "_" + shade
|
||||
|
||||
val colorResourceId = runCatching {
|
||||
android.R.color::class.java.getField(colorName).getInt(null)
|
||||
}.getOrElse { 0 }
|
||||
|
||||
add(convertToColor(colorResourceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun convertToColor(id: Int): String {
|
||||
val clr = if (isSupported()) ContextCompat.getColor(context, id) else 0
|
||||
return String.format("#%06X", 0xFFFFFF and clr)
|
||||
}
|
||||
}
|
||||
162
app/src/main/kotlin/com/pyoncord/xposed/ThemeModule.kt
Normal file
162
app/src/main/kotlin/com/pyoncord/xposed/ThemeModule.kt
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package com.pyoncord.xposed
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.content.res.Resources
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
@Serializable
|
||||
data class Author(
|
||||
val name: String,
|
||||
val id: String? = null
|
||||
)
|
||||
@Serializable
|
||||
data class ThemeData(
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val authors: List<Author>? = null,
|
||||
val spec: Int,
|
||||
val semanticColors: Map<String, List<String>>? = null,
|
||||
val rawColors: Map<String, String>? = null
|
||||
)
|
||||
@Serializable
|
||||
data class Theme(
|
||||
val id: String,
|
||||
val selected: Boolean,
|
||||
val data: ThemeData
|
||||
)
|
||||
|
||||
class ThemeModule : PyonModule() {
|
||||
private lateinit var param: XC_LoadPackage.LoadPackageParam
|
||||
|
||||
private var theme: Theme? = null
|
||||
private val rawColorMap = mutableMapOf<String, Int>()
|
||||
|
||||
@ExperimentalSerializationApi
|
||||
override fun buildJson(builder: JsonObjectBuilder) {
|
||||
builder.apply {
|
||||
put("hasThemeSupport", true)
|
||||
if (theme != null)
|
||||
put("storedTheme", Json.encodeToJsonElement<Theme>(theme!!))
|
||||
else
|
||||
put("storedTheme", null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) {
|
||||
param = packageParam
|
||||
theme = getTheme()
|
||||
hookTheme()
|
||||
}
|
||||
|
||||
fun File.isValidish(): Boolean {
|
||||
if (!this.exists()) return false
|
||||
|
||||
val text = this.readText()
|
||||
return !text.isBlank() && text != "{}" && text != "null"
|
||||
}
|
||||
|
||||
fun getTheme(): Theme? {
|
||||
val filesDir = File(param.appInfo.dataDir, "files").apply { mkdirs() }
|
||||
val pyonDir = File(filesDir, "pyoncord").apply { mkdirs() }
|
||||
val themeFile = File(pyonDir, "current-theme.json")
|
||||
|
||||
val legacyThemeFile = File(filesDir, "vendetta_theme.json")
|
||||
if (legacyThemeFile.isValidish() && !themeFile.isValidish()) {
|
||||
legacyThemeFile.copyTo(themeFile, overwrite = true)
|
||||
}
|
||||
|
||||
if (!themeFile.isValidish()) return null
|
||||
|
||||
val themeText = themeFile.readText()
|
||||
|
||||
return try {
|
||||
Json { ignoreUnknownKeys = true }.decodeFromString<Theme>(themeText)
|
||||
} catch (e: Exception) { null }
|
||||
}
|
||||
|
||||
fun hookTheme() {
|
||||
val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt")
|
||||
val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkTheme")
|
||||
val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme")
|
||||
|
||||
val theme = this.theme
|
||||
if (theme == null) return
|
||||
|
||||
// Apply rawColors
|
||||
theme.data.rawColors?.forEach { (key, value) ->
|
||||
rawColorMap[key.lowercase()] = hexStringToColorInt(value)
|
||||
}
|
||||
|
||||
// Apply semanticColors
|
||||
theme.data.semanticColors?.forEach { (key, value) ->
|
||||
// TEXT_NORMAL -> getTextNormal
|
||||
val methodName = "get${key.split("_").joinToString("") { it.lowercase().replaceFirstChar { it.uppercase() } }}"
|
||||
value.forEachIndexed { index, v ->
|
||||
when (index) {
|
||||
0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v))
|
||||
1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's any rawColors value, hook the color getter
|
||||
if (!theme.data.rawColors.isNullOrEmpty()) {
|
||||
val getColorCompat = themeManager.getDeclaredMethod(
|
||||
"getColorCompat",
|
||||
Resources::class.java,
|
||||
Int::class.javaPrimitiveType,
|
||||
Resources.Theme::class.java,
|
||||
)
|
||||
|
||||
val getColorCompatLegacy = themeManager.getDeclaredMethod(
|
||||
"getColorCompat",
|
||||
Context::class.java,
|
||||
Int::class.javaPrimitiveType
|
||||
)
|
||||
|
||||
val patch = object : XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
val arg1 = param.args[0]
|
||||
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
|
||||
val name = resources.getResourceEntryName(param.args[1] as Int)
|
||||
|
||||
if (rawColorMap[name] != null) param.result = rawColorMap[name]
|
||||
}
|
||||
}
|
||||
|
||||
XposedBridge.hookMethod(getColorCompat, patch)
|
||||
XposedBridge.hookMethod(getColorCompatLegacy, patch)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert 0xRRGGBBAA to 0XAARRGGBB
|
||||
fun hexStringToColorInt(hexString: String): Int {
|
||||
val parsed = Color.parseColor(hexString)
|
||||
return parsed.takeIf { hexString.length == 7 } ?: parsed and 0xFFFFFF or (parsed ushr 24)
|
||||
}
|
||||
|
||||
fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
|
||||
try {
|
||||
themeClass.getDeclaredMethod(methodName).let { method ->
|
||||
// Log.i("Hooking $methodName -> ${themeValue.toString(16)}")
|
||||
XposedBridge.hookMethod(method, object : XC_MethodHook() {
|
||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||
param.result = themeValue
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (ex: NoSuchMethodException) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue