From 2ac8ed15f2e8b14301bae0a0b6f8ec10517aabbc Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 11 Oct 2025 20:07:05 +0200 Subject: [PATCH] replace files to the ones from codeberg --- .github/workflows/build.yml | 74 ++--- .gitignore | 5 +- .idea/.name | 2 +- .idea/AndroidProjectSystem.xml | 6 + .idea/appInsightsSettings.xml | 26 -- .idea/codeStyles/Project.xml | 115 +------- .idea/deploymentTargetDropDown.xml | 10 - .idea/deploymentTargetSelector.xml | 8 - .idea/deviceManager.xml | 13 + .idea/gradle.xml | 1 - .idea/jarRepositories.xml | 30 -- .idea/kotlinc.xml | 6 - .idea/misc.xml | 8 +- .idea/vcs.xml | 2 +- .vscode/settings.json | 3 - Images/kettu.png | Bin 22888 -> 0 bytes Images/pupu_logo.png | Bin 73063 -> 0 bytes README.md | 28 -- app/build.gradle.kts | 51 ++-- app/src/main/AndroidManifest.xml | 28 +- app/src/main/assets/xposed_init | 2 +- .../io/github/pupu/xposed/FontsModule.kt | 258 ----------------- .../io/github/pupu/xposed/LogBoxModule.kt | 117 -------- .../main/kotlin/io/github/pupu/xposed/Main.kt | 230 --------------- .../io/github/pupu/xposed/PupuModule.kt | 9 - .../io/github/pupu/xposed/SysColorsModule.kt | 50 ---- .../io/github/pupu/xposed/ThemeModule.kt | 165 ----------- .../io/github/revenge/xposed/Constants.kt | 18 ++ .../kotlin/io/github/revenge/xposed/Main.kt | 70 +++++ .../kotlin/io/github/revenge/xposed/Module.kt | 192 +++++++++++++ .../kotlin/io/github/revenge/xposed/Utils.kt | 32 +++ .../xposed/modules/FixResourcesModule.kt | 21 ++ .../xposed/modules/HookScriptLoaderModule.kt | 132 +++++++++ .../revenge/xposed/modules/LogBoxModule.kt | 128 +++++++++ .../xposed/modules/PayloadGlobalModule.kt | 108 +++++++ .../revenge/xposed/modules/UpdaterModule.kt | 148 ++++++++++ .../xposed/modules/appearance/FontsModule.kt | 271 ++++++++++++++++++ .../modules/appearance/SysColorsModule.kt | 45 +++ .../xposed/modules/appearance/ThemesModule.kt | 160 +++++++++++ .../bridge/AdditionalBridgeMethodsModule.kt | 52 ++++ .../xposed/modules/bridge/BridgeModule.kt | 202 +++++++++++++ app/src/main/res/values/strings.xml | 4 +- build.gradle.kts | 4 +- gradle/libs.versions.toml | 43 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 45 files changed, 1684 insertions(+), 1195 deletions(-) create mode 100644 .idea/AndroidProjectSystem.xml delete mode 100644 .idea/appInsightsSettings.xml delete mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 .idea/deviceManager.xml delete mode 100644 .idea/jarRepositories.xml delete mode 100644 .idea/kotlinc.xml delete mode 100644 .vscode/settings.json delete mode 100644 Images/kettu.png delete mode 100644 Images/pupu_logo.png delete mode 100644 README.md delete mode 100644 app/src/main/kotlin/io/github/pupu/xposed/FontsModule.kt delete mode 100644 app/src/main/kotlin/io/github/pupu/xposed/LogBoxModule.kt delete mode 100644 app/src/main/kotlin/io/github/pupu/xposed/Main.kt delete mode 100644 app/src/main/kotlin/io/github/pupu/xposed/PupuModule.kt delete mode 100644 app/src/main/kotlin/io/github/pupu/xposed/SysColorsModule.kt delete mode 100644 app/src/main/kotlin/io/github/pupu/xposed/ThemeModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/Constants.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/Main.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/Module.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/Utils.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/FixResourcesModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/PayloadGlobalModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/FontsModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/SysColorsModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/ThemesModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt create mode 100644 app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/BridgeModule.kt delete mode 100644 gradle/libs.versions.toml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8fabe0f..b37574a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,52 +2,52 @@ name: Build APK on: [push] jobs: - build: - name: Build and push - runs-on: ubuntu-latest + build: + name: Build and push + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 + steps: + - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 - with: - distribution: "temurin" - java-version: "17" + - uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" - - name: "Setup Gradle" - uses: gradle/gradle-build-action@v2 + - name: "Setup Gradle" + uses: gradle/gradle-build-action@v2 - - name: "Grant execution permission" - run: chmod +x ./gradlew + - name: "Grant execution permission" + run: chmod +x ./gradlew - - name: "Build APKs" - run: ./gradlew build + - name: "Build APKs" + run: ./gradlew build - - name: "Setup directories" - run: | - mkdir -p /tmp/pupubuild/apks - mkdir -p /tmp/pupubuild/tools + - name: "Setup directories" + run: | + mkdir -p /tmp/ShiggyXposedbuild/apks + mkdir -p /tmp/ShiggyXposedbuild/tools - - name: "Move debug APK" - run: mv ./app/build/outputs/apk/debug/app-debug.apk /tmp/pupubuild/apks + - name: "Move debug APK" + run: mv ./app/build/outputs/apk/debug/app-debug.apk /tmp/ShiggyXposedbuild/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/pupubuild/tools/uber-apk-signer.jar + - 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/ShiggyXposedbuild/tools/uber-apk-signer.jar - - name: "Sign release APK" - run: java -jar /tmp/pupubuild/tools/uber-apk-signer.jar --apks ./app/build/outputs/apk/release/app-release-unsigned.apk --out /tmp/pupubuild/apks/ + - name: "Sign release APK" + run: java -jar /tmp/ShiggyXposedbuild/tools/uber-apk-signer.jar --apks ./app/build/outputs/apk/release/app-release-unsigned.apk --out /tmp/ShiggyXposedbuild/apks/ - - name: "Rename release APK" - run: mv /tmp/pupubuild/apks/app-release-aligned-debugSigned.apk /tmp/pupubuild/apks/app-release.apk + - name: "Rename release APK" + run: mv /tmp/ShiggyXposedbuild/apks/app-release-aligned-debugSigned.apk /tmp/ShiggyXposedbuild/apks/app-release.apk - - name: "Upload debug APK" - uses: actions/upload-artifact@v4 - with: - name: app-debug - path: /tmp/pupubuild/apks/app-debug.apk + - name: "Upload debug APK" + uses: actions/upload-artifact@v4 + with: + name: app-debug + path: /tmp/ShiggyXposedbuild/apks/app-debug.apk - - name: "Upload release APK" - uses: actions/upload-artifact@v4 - with: - name: app-release - path: /tmp/pupubuild/apks/app-release.apk \ No newline at end of file + - name: "Upload release APK" + uses: actions/upload-artifact@v4 + with: + name: app-release + path: /tmp/ShiggyXposedbuild/apks/app-release.apk diff --git a/.gitignore b/.gitignore index 0598b79..162ad5d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,5 @@ build .externalNativeBuild .cxx local.properties -.idea/AndroidProjectSystem.xml -/app/release -/app/build \ No newline at end of file +app/release/release/ +release diff --git a/.idea/.name b/.idea/.name index 29d110d..e551d4f 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -KettuXposed \ No newline at end of file +ShiggyXposed diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml deleted file mode 100644 index 371f2e2..0000000 --- a/.idea/appInsightsSettings.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7643783..0def1f7 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,121 +1,10 @@ + diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 7b3006b..639c779 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -13,7 +13,6 @@ - diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index e34606c..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index c22b6fa..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b2c751a..2557637 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,9 +1,7 @@ - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 0e14d8e..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "disabled" -} \ No newline at end of file diff --git a/Images/kettu.png b/Images/kettu.png deleted file mode 100644 index c047af710aed1b645e0361776c90a4264bf4c548..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22888 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 a^O)sDPiyc5zxWmee9TPX*D9{wfBb($MP+;d diff --git a/README.md b/README.md deleted file mode 100644 index 0451a8f..0000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -
- - Kettu logo - - # KettuXposed - - Easily install Kettu on your rooted device! - - [![Latest release](https://img.shields.io/github/v/release/C0C0B01/KettuXposed?color=3AB8BA&display_name=release&label=Latest&style=for-the-badge)](https://github.com/C0C0B01/KettuXposed/releases/latest) - - --- - -
- - ![Downloads (latest)](https://img.shields.io/github/downloads/C0C0B01/KettuXposed/latest/total?style=for-the-badge&logo=github&label=Downloads%20(Latest)&color=blue) - ![Total downloads](https://img.shields.io/github/downloads/C0C0B01/KettuXposed/total?style=for-the-badge&logo=github&label=Downloads%20(Total)&color=blue) - -
- -
- -## Contributing - -This is an open-source project, you can do so without any programming. - -Here are a few things you can do: - -- [Test and report issues](https://github.com/C0C0B01/KettuXposed/issues/new/choose) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cdebb75..46af0dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,15 +5,15 @@ plugins { } android { - namespace = "cocobo1.pupu.xposed" + namespace = "ShiggyXposed.xposed" compileSdk = 36 defaultConfig { - applicationId = "cocobo1.pupu.xposed" + applicationId = "ShiggyXposed.xposed" minSdk = 22 targetSdk = 36 - versionCode = 1130 - versionName = "1.1.3" + versionCode = 1000 + versionName = "1.0.0" } buildTypes { @@ -29,41 +29,30 @@ android { } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlinOptions { - jvmTarget = "21" - } - buildFeatures { - buildConfig = true + targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { jvmTarget = "17" } + buildFeatures { buildConfig = true } } -repositories { - maven("https://jitpack.io") +repositories { + mavenCentral() + google() + maven("https://jitpack.io") } dependencies { compileOnly("de.robv.android.xposed:api:82") - compileOnly(libs.xposed) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1") - implementation(libs.libunbound) - implementation(libs.kotlinx.serialization.json) - implementation(libs.kotlinx.coroutines.android) - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.cio) - implementation(libs.kotlin.reflect) - - implementation(libs.yuki.api) - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - - testImplementation(libs.junit) - - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) + implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.0") + implementation("io.ktor:ktor-client-core:3.3.0") + implementation("io.ktor:ktor-client-cio:3.3.0") + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f830c6d..0c1bdf6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,25 +1,25 @@ + xmlns:tools="http://schemas.android.com/tools"> + android:name="xposedmodule" + android:value="true"/> + android:name="xposeddescription" + android:value="Xposed module to inject ShiggyCord into discord"/> + android:name="xposedminversion" + android:value="82"/> - \ No newline at end of file + diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init index 572cbc0..36c4f0c 100644 --- a/app/src/main/assets/xposed_init +++ b/app/src/main/assets/xposed_init @@ -1 +1 @@ -cocobo1.pupu.xposed.Main \ No newline at end of file +ShiggyXposed.xposed.Main diff --git a/app/src/main/kotlin/io/github/pupu/xposed/FontsModule.kt b/app/src/main/kotlin/io/github/pupu/xposed/FontsModule.kt deleted file mode 100644 index 0bd107e..0000000 --- a/app/src/main/kotlin/io/github/pupu/xposed/FontsModule.kt +++ /dev/null @@ -1,258 +0,0 @@ -// credits to janisslsm from his PR: https://github.com/vendetta-mod/VendettaXposed/pull/17 -// hooks are modified function from RN codebase - -package cocobo1.pupu.xposed - -import android.content.res.AssetManager -import android.os.Build -import android.graphics.Typeface -import android.graphics.Typeface.CustomFallbackBuilder -import android.graphics.fonts.Font -import android.graphics.fonts.FontFamily -import android.util.Log -import de.robv.android.xposed.XC_MethodReplacement -import de.robv.android.xposed.XposedHelpers -import de.robv.android.xposed.callbacks.XC_LoadPackage -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.* -import java.io.IOException -import java.io.File -import kotlinx.coroutines.* - -import io.ktor.client.* -import io.ktor.client.call.* -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.* - -@Serializable -data class FontDefinition( - val name: String? = null, - val description: String? = null, - val spec: Int? = null, - val main: Map, -) - -class FontsModule: Module() { - private val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic") - private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf") - private val FONTS_ASSET_PATH = "fonts/" - - private lateinit var fontsDir: File - private lateinit var fontsDownloadsDir: File - private var fontsAbsPath: String? = null - - override fun buildJson(builder: JsonObjectBuilder) { - builder.apply { - put("fontPatch", 2) - } - } - - private fun hookClass(classLoader: ClassLoader, className: String) { - XposedHelpers.findAndHookMethod( - className, - classLoader, - "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) - } - }) - } - - override fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) = with (packageParam) { - try { - // Try to hook on versions (280201+) - hookClass(classLoader, "com.facebook.react.views.text.ReactFontManager\$Companion") - } catch (e: Throwable) { - when (e) { - // Hook old class (280200-) - is NoClassDefFoundError, is XposedHelpers.ClassNotFoundError -> hookClass(classLoader, "com.facebook.react.views.text.ReactFontManager") - else -> throw e - } - } - - val fontDefFile = File(appInfo.dataDir, "files/pyoncord/fonts.json") - if (!fontDefFile.exists()) return@with - - val fontDef = try { - val json = Json { ignoreUnknownKeys = true } - json.decodeFromString(fontDefFile.readText()) - } catch (_: Throwable) { return@with } - - fontsDownloadsDir = File(appInfo.dataDir, "files/pyoncord/downloads/fonts").apply { mkdirs() } - fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { mkdirs() } - 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("Kettu", "Deleting font file: $fileName") - file.delete() - } - } - } - - // These files should be downloaded by the JS side, but oh well - CoroutineScope(Dispatchers.IO).launch { - fontDef.main.keys.map { name -> - async { - val url = fontDef.main.getValue(name) - try { - Log.i("Kettu", "Downloading $name from $url") - val file = File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}") - if (file.exists()) return@async - - val client = HttpClient(CIO) { - install(UserAgent) { agent = "KettuXposed" } - } - - val response: HttpResponse = client.get(url) - - if (response.status == HttpStatusCode.OK) { - file.writeBytes(response.body()) - } - - return@async - } catch (e: Throwable) { - Log.e("Kettu", "Failed to download fonts ($name from $url)", e) - } - } - }.awaitAll() - } - - return@with - } - - private fun createAssetTypefaceWithFallbacks( - fontFamilyNames: Array, - style: Int, - assetManager: AssetManager - ): Typeface? { - val fontFamilies: MutableList = ArrayList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // Iterate over the list of fontFamilyNames, constructing new FontFamily objects - // for use in the CustomFallbackBuilder below. - for (fontFamilyName in fontFamilyNames) { - try { - for (fileExtension in FILE_EXTENSIONS) { - val (customName, refName) = fontFamilyName.split(":") - val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension") - val font = Font.Builder(file).build() - val family = FontFamily.Builder(font).build() - fontFamilies.add(family) - } - } catch (e: Throwable) { - // ignore - } - - for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { - for (fileExtension in FILE_EXTENSIONS) { - val fileName = java.lang.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 (e: java.lang.RuntimeException) { - // If the typeface asset does not exist, try another extension. - continue - } catch (e: IOException) { - // If the font asset does not exist, try another extension. - continue - } - } - } - } - - // If there's some problem constructing fonts, fall back to the default behavior. - if (fontFamilies.size == 0) { - 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? { - // This logic attempts to safely check if the frontend code is attempting to use - // fallback fonts, and if it is, to use the fallback typeface creation logic. - var fontFamilyName: String = fontFamilyName_ - val fontFamilyNames = - fontFamilyName.split(",".toRegex()) - .dropLastWhile { it.isEmpty() } - .toTypedArray() - for (i in fontFamilyNames.indices) { - fontFamilyNames[i] = fontFamilyNames[i].trim() - } - - // If there are multiple font family names: - // For newer versions of Android, construct a Typeface with fallbacks - // For older versions of Android, ignore all the fallbacks and just use the first font family - if (fontFamilyNames.size > 1) { - fontFamilyName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return createAssetTypefaceWithFallbacks(fontFamilyNames, style, assetManager) - } else { - fontFamilyNames[0] - } - } - - val extension = EXTENSIONS[style] - - try { - for (fileExtension in FILE_EXTENSIONS) { - val (customName, refName) = fontFamilyName.split(":") - val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension") - if (!file.exists()) throw Exception() - return Typeface.createFromFile(file.absolutePath) - } - } catch (e: Throwable) { - // ignore - } - - // Lastly, after all those checks above, this is the original RN logic for - // getting the typeface. - for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { - for (fileExtension in FILE_EXTENSIONS) { - val fileName = java.lang.StringBuilder() - .append(fontRootPath) - .append(fontFamilyName) - .append(extension) - .append(fileExtension) - .toString() - - return try { - if (fileName[0] == '/') - Typeface.createFromFile(fileName) - else - Typeface.createFromAsset(assetManager, fileName) - } catch (e: java.lang.RuntimeException) { - // If the typeface asset does not exist, try another extension. - continue - } - } - } - - return Typeface.create(fontFamilyName, style) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/pupu/xposed/LogBoxModule.kt b/app/src/main/kotlin/io/github/pupu/xposed/LogBoxModule.kt deleted file mode 100644 index e804f7e..0000000 --- a/app/src/main/kotlin/io/github/pupu/xposed/LogBoxModule.kt +++ /dev/null @@ -1,117 +0,0 @@ -package cocobo1.pupu.xposed - -import android.app.AlertDialog -import android.app.AndroidAppHelper -import android.content.Context -import android.content.Intent -import android.util.Log -import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XposedBridge -import de.robv.android.xposed.XposedHelpers -import de.robv.android.xposed.callbacks.XC_LoadPackage -import java.io.File -import kotlin.system.exitProcess - -class LogBoxModule: Module() { - lateinit var packageParam: XC_LoadPackage.LoadPackageParam - - override fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) = with (packageParam) { - this@LogBoxModule.packageParam = packageParam - - val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") - val bridgeDevSupportManagerClass = classLoader.loadClass("com.facebook.react.devsupport.BridgeDevSupportManager") - - val mReactInstanceDevHelperField = XposedHelpers.findField(bridgeDevSupportManagerClass, "mReactInstanceDevHelper") - - val getUseDeveloperSupportMethod = dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" } - val handleReloadJSMethod = bridgeDevSupportManagerClass.methods.first { it.name == "handleReloadJS" } - val showDevOptionsDialogMethod = bridgeDevSupportManagerClass.methods.first { it.name == "showDevOptionsDialog" } - - var alertDialog: AlertDialog? = null - - // This enables the LogBox and opens dev option on shake - XposedBridge.hookMethod( - getUseDeveloperSupportMethod, - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - param.result = true - } - } - ) - - // Triggered on shake - XposedBridge.hookMethod( - showDevOptionsDialogMethod, - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - try { - if (alertDialog == null) { - val mReactInstanceDevHelper = - mReactInstanceDevHelperField.get(param.thisObject) - val getCurrentActivityMethod = - mReactInstanceDevHelper.javaClass.methods.first { it.name == "getCurrentActivity" } - - val context = - getCurrentActivityMethod.invoke(mReactInstanceDevHelper) as Context - - alertDialog = showRecoveryAlert(context) { - alertDialog = null - } - } - } catch (ex: Exception) { - Log.e("Kettu", "Failed to show dev options dialog: $ex") - alertDialog = null - } - - // Ignore the original dev menu - param.result = null - } - } - ) - - // Replace the method to direct relaunch the app instead of sending reload command to developer server - XposedBridge.hookMethod( - handleReloadJSMethod, - object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - reloadApp() - } - } - ) - - return@with - } - - private fun showRecoveryAlert(context: Context, onClose: () -> Unit): AlertDialog { - return AlertDialog.Builder(context) - .setTitle("Kettu Recovery Options") - .setItems(arrayOf("Reload", "Delete bundle.js")) { _, which -> - when (which) { - 0 -> { - reloadApp() - } - 1 -> { - val bundleFile = File(packageParam.appInfo.dataDir, "cache/pyoncord/bundle.js") - if (bundleFile.exists()) { - bundleFile.delete() - } - - reloadApp() - } - } - - onClose() - } - .setOnDismissListener { - onClose() - } - .show() - } - - private fun reloadApp() { - val application = AndroidAppHelper.currentApplication() - val intent = application.packageManager.getLaunchIntentForPackage(application.packageName) - application.startActivity(Intent.makeRestartActivityTask(intent!!.component)) - exitProcess(0) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/pupu/xposed/Main.kt b/app/src/main/kotlin/io/github/pupu/xposed/Main.kt deleted file mode 100644 index 769f8e7..0000000 --- a/app/src/main/kotlin/io/github/pupu/xposed/Main.kt +++ /dev/null @@ -1,230 +0,0 @@ -package cocobo1.pupu.xposed - -import android.app.Activity -import android.content.res.AssetManager -import android.content.res.Resources -import android.util.Log -import android.os.Bundle -import android.widget.Toast -import de.robv.android.xposed.IXposedHookLoadPackage -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.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 - -@Serializable -data class CustomLoadUrl( - val enabled: Boolean, - val url: String -) - -@Serializable -data class LoaderConfig( - val customLoadUrl: CustomLoadUrl -) - -class Main : IXposedHookLoadPackage { - private val Modules: Array = arrayOf( - ThemeModule(), - SysColorsModule(), - FontsModule(), - LogBoxModule() - ) - - fun buildLoaderJsonString(): String { - val obj = buildJsonObject { - put("loaderName", "KettuXposed") - put("loaderVersion", BuildConfig.VERSION_NAME) - - for (module in Modules) { - module.buildJson(this) - } - } - - return Json.encodeToString(obj) - } - - override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { - val reactActivity = runCatching { - lpparam.classLoader.loadClass("com.discord.react_activities.ReactActivity") - }.getOrElse { return } // Package is not our the target app, return - - var activity: Activity? = null - val onActivityCreateCallback = mutableSetOf<(activity: Activity) -> Unit>() - - XposedBridge.hookMethod(reactActivity.getDeclaredMethod("onCreate", Bundle::class.java), object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - activity = param.thisObject as Activity - onActivityCreateCallback.forEach { cb -> cb(activity!!) } - onActivityCreateCallback.clear() - } - }) - - init(lpparam) { cb -> - if (activity != null) cb(activity!!) - else onActivityCreateCallback.add(cb) - } - } - - private fun init( - param: XC_LoadPackage.LoadPackageParam, - onActivityCreate: ((activity: Activity) -> Unit) -> Unit - ) = with (param) { - val catalystInstanceImpl = classLoader.loadClass("com.facebook.react.bridge.CatalystInstanceImpl") - - for (module in Modules) module.onInit(param) - - val loadScriptFromAssets = catalystInstanceImpl.getDeclaredMethod( - "loadScriptFromAssets", - AssetManager::class.java, - String::class.java, - Boolean::class.javaPrimitiveType - ).apply { isAccessible = true } - - val loadScriptFromFile = catalystInstanceImpl.getDeclaredMethod( - "loadScriptFromFile", - 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() - val json = Json { ignoreUnknownKeys = true } - json.decodeFromString(configFile.readText()) - } catch (_: Exception) { - LoaderConfig( - customLoadUrl = CustomLoadUrl( - enabled = false, - url = "" // Not used - ) - ) - } - - val scope = MainScope() - val httpJob = scope.async(Dispatchers.IO) { - try { - val client = HttpClient(CIO) { - expectSuccess = true - install(HttpTimeout) { - requestTimeoutMillis = if (bundle.exists()) 4000 else 20000 - } - install(UserAgent) { agent = "KettuXposed" } - } - - val url = - if (config.customLoadUrl.enabled) config.customLoadUrl.url - else "https://raw.githubusercontent.com/C0C0B01/Kettu/refs/heads/dist/kettu.min.js" - - Log.e("Kettu", "Fetching JS bundle from $url") - - val response: HttpResponse = client.get(url) { - headers { - if (etag.exists() && bundle.exists()) { - append(HttpHeaders.IfNoneMatch, etag.readText()) - } - } - } - - bundle.writeBytes(response.body()) - if (response.headers["Etag"] != null) { - etag.writeText(response.headers["Etag"]!!) - } - else if (etag.exists()) { - // This is called when server does not return an E-tag, so clear em - etag.delete() - } - - return@async - } catch (e: RedirectResponseException) { - if (e.response.status != HttpStatusCode.NotModified) throw e - Log.e("Kettu", "Server responded with status code 304 - no changes to file") - } catch (e: Throwable) { - onActivityCreate { activity -> - activity.runOnUiThread { - Toast.makeText( - activity.applicationContext, - "Failed to fetch JS bundle, Kettu may not load!", - Toast.LENGTH_SHORT - ).show() - } - } - - Log.e("Kettu", "Failed to download bundle", e) - } - } - - val patch = object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam) { - runBlocking { httpJob.join() } - - XposedBridge.invokeOriginalMethod( - setGlobalVariable, - param.thisObject, - arrayOf("__PYON_LOADER__", buildLoaderJsonString()) - ) - - preloadsDir - .walk() - .filter { it.isFile && it.extension == "js" } - .forEach { file -> - XposedBridge.invokeOriginalMethod( - loadScriptFromFile, - param.thisObject, - arrayOf(file.absolutePath, file.absolutePath, param.args[2]) - ) - } - - XposedBridge.invokeOriginalMethod( - loadScriptFromFile, - 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 - ) - - XposedBridge.hookMethod(getIdentifier, object: XC_MethodHook() { - override fun beforeHookedMethod(mhparam: MethodHookParam) = with(mhparam) { - if (args[2] == param.packageName) args[2] = "com.discord" - } - }) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/pupu/xposed/PupuModule.kt b/app/src/main/kotlin/io/github/pupu/xposed/PupuModule.kt deleted file mode 100644 index 139b7ea..0000000 --- a/app/src/main/kotlin/io/github/pupu/xposed/PupuModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package cocobo1.pupu.xposed - -import de.robv.android.xposed.callbacks.XC_LoadPackage -import kotlinx.serialization.json.JsonObjectBuilder - -abstract class Module { - open fun buildJson(builder: JsonObjectBuilder) {} - open fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) {} -} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/pupu/xposed/SysColorsModule.kt b/app/src/main/kotlin/io/github/pupu/xposed/SysColorsModule.kt deleted file mode 100644 index 4f400a8..0000000 --- a/app/src/main/kotlin/io/github/pupu/xposed/SysColorsModule.kt +++ /dev/null @@ -1,50 +0,0 @@ -package cocobo1.pupu.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, - val neutral2: List, - val accent1: List, - val accent2: List, - val accent3: List -) - -class SysColorsModule : Module() { - private lateinit var context: Context - private 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)) - } - } - } - } - } - - private fun convertToColor(id: Int): String { - val clr = if (isSupported()) ContextCompat.getColor(context, id) else 0 - return String.format("#%06X", 0xFFFFFF and clr) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/pupu/xposed/ThemeModule.kt b/app/src/main/kotlin/io/github/pupu/xposed/ThemeModule.kt deleted file mode 100644 index 3caf491..0000000 --- a/app/src/main/kotlin/io/github/pupu/xposed/ThemeModule.kt +++ /dev/null @@ -1,165 +0,0 @@ -package cocobo1.pupu.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? = null, - val spec: Int, - val semanticColors: Map>? = null, - val rawColors: Map? = null -) -@Serializable -data class Theme( - val id: String, - val selected: Boolean, - val data: ThemeData -) - -class ThemeModule : Module() { - private lateinit var param: XC_LoadPackage.LoadPackageParam - - private var theme: Theme? = null - private val rawColorMap = mutableMapOf() - - @ExperimentalSerializationApi - override fun buildJson(builder: JsonObjectBuilder) { - builder.apply { - put("hasThemeSupport", true) - if (theme != null) - put("storedTheme", Json.encodeToJsonElement(theme!!)) - else - put("storedTheme", null) - } - } - - override fun onInit(packageParam: XC_LoadPackage.LoadPackageParam) { - param = packageParam - theme = getTheme() - hookTheme() - } - - private fun File.isValidish(): Boolean { - if (!this.exists()) return false - - val text = this.readText() - return text.isNotBlank() && text != "{}" && text != "null" - } - - private 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 - - return try { - val themeText = themeFile.readText() - Json { ignoreUnknownKeys = true }.decodeFromString(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.DarkerTheme") - 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) - } - } - - // Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB" - private fun hexStringToColorInt(hexString: String): Int { - return if (hexString.length == 9 ) { - // Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy - val alpha = hexString.substring(7, 9) - val rrggbb = hexString.substring(1, 7) - Color.parseColor("#$alpha$rrggbb") - } else Color.parseColor(hexString) - } - - private 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 - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt b/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt new file mode 100644 index 0000000..86245e2 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/Constants.kt @@ -0,0 +1,18 @@ +package ShiggyXposed.xposed + +class Constants { + companion object { + const val TARGET_PACKAGE = "com.discord" + const val TARGET_ACTIVITY = "$TARGET_PACKAGE.react_activities.ReactActivity" + + const val FILES_DIR = "files/pyoncord" + const val CACHE_DIR = "cache/pyoncord" + const val MAIN_SCRIPT_FILE = "bundle.js" + + const val LOG_TAG = "ShiggyXposed" + + const val LOADER_NAME = "ShiggyXposed" + + const val USER_AGENT = "ShiggyXposed" + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Main.kt b/app/src/main/kotlin/io/github/revenge/xposed/Main.kt new file mode 100644 index 0000000..38118cc --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/Main.kt @@ -0,0 +1,70 @@ +package ShiggyXposed.xposed + +import ShiggyXposed.xposed.modules.* +import ShiggyXposed.xposed.modules.appearance.FontsModule +import ShiggyXposed.xposed.modules.appearance.SysColorsModule +import ShiggyXposed.xposed.modules.appearance.ThemesModule +import ShiggyXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule +import ShiggyXposed.xposed.modules.bridge.BridgeModule +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.os.Bundle +import de.robv.android.xposed.IXposedHookLoadPackage +import de.robv.android.xposed.IXposedHookZygoteInit +import de.robv.android.xposed.callbacks.XC_LoadPackage + +class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit { + private var hooked = false + private val modules = + mutableListOf( + HookScriptLoaderModule(), + BridgeModule(), + AdditionalBridgeMethodsModule(), + UpdaterModule(), + FixResourcesModule(), + LogBoxModule(), + FontsModule(), + ThemesModule(), + SysColorsModule() + ) + + init { + modules.add(PayloadGlobalModule(modules)) + } + + override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { + for (module in modules) module.onInit(startupParam) + } + + override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = + with(param) { + if (hooked) return + + val reactActivity = classLoader.loadClass(Constants.TARGET_ACTIVITY) + + ContextWrapper::class.java.hookMethod("attachBaseContext", Context::class.java) { + after { this@Main.onContext(args[0] as Context) } + } + + reactActivity.hookMethod("onCreate", Bundle::class.java) { + after { this@Main.onActivity(thisObject as Activity) } + } + + this@Main.onLoad(param) + + hooked = true + } + + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) { + for (module in modules) module.onLoad(packageParam) + } + + override fun onContext(context: Context) { + for (module in modules) module.onContext(context) + } + + override fun onActivity(activity: Activity) { + for (module in modules) module.onActivity(activity) + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Module.kt b/app/src/main/kotlin/io/github/revenge/xposed/Module.kt new file mode 100644 index 0000000..8813346 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/Module.kt @@ -0,0 +1,192 @@ +package ShiggyXposed.xposed + +import android.app.Activity +import android.content.Context +import android.os.Build +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 java.io.File +import java.lang.reflect.Method +import kotlinx.serialization.json.JsonObjectBuilder + +data class AppInfo( + val name: String, + val packageName: String, + val version: String, + val versionCode: Long, +) + +abstract class Module { + /** Builds a JSON payload to be injected into the JavaScript context. */ + @Deprecated( + "This will be removed in future versions. Payloads can be replaced via synchronous bridge methods." + ) + open fun buildPayload(builder: JsonObjectBuilder) {} + + /** Called during Zygote initialization. */ + open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {} + + /** + * Called when a package is loaded. + * + * Hooks should typically be set up here. + * + * Bridge methods that do not require [Context] can also be registered here. + */ + open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {} + + /** + * Called after [Context] has been attached to a [android.content.ContextWrapper] via + * [android.content.ContextWrapper.attachBaseContext]. + * + * This may be called multiple times for different [Context]s, e.g. when the app is killed by + * the system. + * + * For bridge methods that require access to [Context], register them here. + */ + open fun onContext(context: Context) {} + + /** + * Called after an [Activity.onCreate] method is executed. + * + * UI code that requires an [Activity] context can be placed here. + * + * Bridge methods must not be registered here, as it can be too late and cause crashes. + * + * If you need to register bridge methods that require an [Activity], do it in [onContext] and + * make it async. + * + * Once an [Activity] is available, execute the queued calls. Then from JS, call the method + * asynchronously. + */ + open fun onActivity(activity: Activity) {} + + protected fun Context.getAppInfo(): AppInfo { + val pInfo = packageManager.getPackageInfo(packageName, 0) + val versionCode = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pInfo.longVersionCode + else @Suppress("DEPRECATION") pInfo.versionCode.toLong() + + return AppInfo( + packageManager.getApplicationLabel(applicationInfo).toString(), + packageName, + pInfo.versionName ?: versionCode.toString(), + versionCode, + ) + } + + protected fun File.asDir() { + if (!this.isDirectory) this.delete() + this.mkdirs() + } + + protected fun File.asFile() { + if (!this.isFile) this.deleteRecursively() + } + + protected fun Class<*>.method(name: String, vararg params: Class<*>?): Method = + getDeclaredMethod(name, *params).apply { isAccessible = true } + + protected fun ClassLoader.safeLoadClass(name: String): Class<*>? = + runCatching { loadClass(name) }.getOrNull() + + protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = + XposedBridge.hookMethod(this, hook) + + protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook = + hook(MethodHookBuilder().apply(block).build()) + + protected fun Class<*>.hookMethod( + name: String, + vararg params: Class<*>?, + block: MethodHookBuilder.() -> Unit + ): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build()) + + protected class MethodHookBuilder { + private var beforeBlock: (HookScope.() -> Unit)? = null + private var afterBlock: (HookScope.() -> Unit)? = null + + fun before(block: HookScope.() -> Unit) { + beforeBlock = block + } + + fun after(block: HookScope.() -> Unit) { + afterBlock = block + } + + fun build(): XC_MethodHook = + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + val b = beforeBlock + if (b != null) { + val scope = + HookScope( + param = param, + proceed = { p -> super.beforeHookedMethod(p) } + ) + scope.b() + } else { + super.beforeHookedMethod(param) + } + } + + override fun afterHookedMethod(param: MethodHookParam) { + val a = afterBlock + if (a != null) { + val scope = + HookScope( + param = param, + proceed = { p -> super.afterHookedMethod(p) } + ) + scope.a() + } else { + super.afterHookedMethod(param) + } + } + } + } + + /** + * Scope object passed to before/after hook blocks. + * + * Provides: + * - Access to the [param] object + * - [proceed] to call the original XC_MethodHook super method + * - Accessors for `thisObject`, `args`, `result`, and `throwable` + * + * @property param The [XC_MethodHook.MethodHookParam] for the current hook. + * @property proceed Function that calls the super method. + */ + protected class HookScope + internal constructor( + val param: XC_MethodHook.MethodHookParam, + private val proceed: (XC_MethodHook.MethodHookParam) -> Unit + ) { + /** + * Continues with the default XC_MethodHook super behavior. Equivalent to calling + * `super.beforeHookedMethod(param)` or `super.afterHookedMethod(param)` depending on the + * phase. + */ + fun proceed() = proceed(param) + + val thisObject: Any? + get() = param.thisObject + + val args: Array + get() = param.args + + var result: Any? + get() = param.result + set(value) { + param.result = value + } + + var throwable: Throwable? + get() = param.throwable + set(value) { + param.throwable = value + } + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt b/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt new file mode 100644 index 0000000..a5c2e68 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt @@ -0,0 +1,32 @@ +package ShiggyXposed.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) + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/FixResourcesModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/FixResourcesModule.kt new file mode 100644 index 0000000..b0bee81 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/FixResourcesModule.kt @@ -0,0 +1,21 @@ +package ShiggyXposed.xposed.modules + +import ShiggyXposed.xposed.Constants.Companion.TARGET_PACKAGE +import ShiggyXposed.xposed.Module +import android.content.res.Resources +import de.robv.android.xposed.callbacks.XC_LoadPackage + +/** Hooks [Resources.getIdentifier] to fix resource package name mismatch. */ +class 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 } } + } + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt new file mode 100644 index 0000000..35346ee --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt @@ -0,0 +1,132 @@ +package ShiggyXposed.xposed.modules + +import ShiggyXposed.xposed.Constants +import ShiggyXposed.xposed.Module +import ShiggyXposed.xposed.Utils.Log +import ShiggyXposed.xposed.modules.HookScriptLoaderModule.Companion.PRELOADS_DIR +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 java.io.File +import java.lang.reflect.Method +import kotlinx.coroutines.runBlocking + +/** + * 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://revenge.bundle` from the module's assets. + */ +class HookScriptLoaderModule : Module() { + private lateinit var preloadsDir: File + private lateinit var mainScript: File + + private lateinit var modulePath: String + private lateinit var resources: XModuleResources + + companion object { + 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$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<*>) { + 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) + } + } + } + + private fun HookScope.runCustomScripts( + loadScriptFromFile: Method, + loadScriptFromAssets: Method + ) { + Log.i("Running custom scripts...") + + // TODO: Is there a better way to do this? + runBlocking { UpdaterModule.job?.join() } + + 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://bundle.js", loadSynchronously) + ) + } + } catch (e: Throwable) { + Log.e("Unable to run scripts:", e) + } + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt new file mode 100644 index 0000000..5ef94c5 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt @@ -0,0 +1,128 @@ +package ShiggyXposed.xposed.modules + +import ShiggyXposed.xposed.Constants +import ShiggyXposed.xposed.Module +import ShiggyXposed.xposed.Utils.Companion.reloadApp +import ShiggyXposed.xposed.Utils.Log +import android.app.AlertDialog +import android.content.Context +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers +import de.robv.android.xposed.callbacks.XC_LoadPackage +import java.io.File + +class LogBoxModule : Module() { + lateinit var packageParam: XC_LoadPackage.LoadPackageParam + lateinit var bridgeDevSupportManagerClass: Class<*> + + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = + with(packageParam) { + this@LogBoxModule.packageParam = packageParam + + try { + val dcdReactNativeHostClass = + classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") + bridgeDevSupportManagerClass = + classLoader.loadClass( + "com.facebook.react.devsupport.BridgeDevSupportManager" + ) + + val getUseDeveloperSupportMethod = + dcdReactNativeHostClass.methods.first { + it.name == "getUseDeveloperSupport" + } + val handleReloadJSMethod = + bridgeDevSupportManagerClass.methods.first { + it.name == "handleReloadJS" + } + val showDevOptionsDialogMethod = + bridgeDevSupportManagerClass.methods.first { + it.name == "showDevOptionsDialog" + } + val mReactInstanceDevHelperField = + XposedHelpers.findField( + bridgeDevSupportManagerClass, + "mReactInstanceDevHelper" + ) + + // This enables the LogBox and opens dev option on shake + XposedBridge.hookMethod( + getUseDeveloperSupportMethod, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + param.result = true + } + } + ) + + // Replace the method to direct relaunch the app instead of sending reload + // command to developer server + XposedBridge.hookMethod( + handleReloadJSMethod, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + reloadApp() + param.result = null + } + } + ) + + // Triggered on shake - Moved from onContext to onLoad + XposedBridge.hookMethod( + showDevOptionsDialogMethod, + object : XC_MethodHook() { + override fun beforeHookedMethod(param: MethodHookParam) { + try { + val mReactInstanceDevHelper = + mReactInstanceDevHelperField.get(param.thisObject) + val getCurrentActivityMethod = + mReactInstanceDevHelper.javaClass.methods.first { + it.name == "getCurrentActivity" + } + val context = + getCurrentActivityMethod.invoke( + mReactInstanceDevHelper + ) as + Context + + showRecoveryAlert(context) + } catch (ex: Exception) { + Log.e("Failed to show dev options dialog: $ex") + } + + // Ignore the original dev menu + param.result = null + } + } + ) + } catch (e: Exception) { + Log.e("Error in LogBoxModule.onLoad: $e") + } + + return@with + } + + private fun showRecoveryAlert(context: Context) { + AlertDialog.Builder(context) + .setTitle("ShiggyXposed Recovery Options") + .setItems(arrayOf("Reload", "Delete bundle.js")) { _, which -> + when (which) { + 0 -> { + reloadApp() + } + 1 -> { + val bundleFile = + File( + packageParam.appInfo.dataDir, + "${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}" + ) + if (bundleFile.exists()) bundleFile.delete() + + reloadApp() + } + } + } + .show() + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/PayloadGlobalModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/PayloadGlobalModule.kt new file mode 100644 index 0000000..17a6de5 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/PayloadGlobalModule.kt @@ -0,0 +1,108 @@ +package ShiggyXposed.xposed.modules + +import ShiggyXposed.xposed.BuildConfig +import ShiggyXposed.xposed.Constants +import ShiggyXposed.xposed.Module +import ShiggyXposed.xposed.Utils.Companion.JSON +import ShiggyXposed.xposed.modules.HookScriptLoaderModule.Companion.PRELOADS_DIR +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 java.io.File +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +// TODO: Remove this redundant module and merge its functionality into AdditionalBridgeMethodsModule +// if needed +class PayloadGlobalModule(private val modules: List) : 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 reactInstance = + 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( + reactInstance!!.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, reactInstance).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) + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt new file mode 100644 index 0000000..a640780 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt @@ -0,0 +1,148 @@ +package ShiggyXposed.xposed.modules + +import ShiggyXposed.xposed.Constants +import ShiggyXposed.xposed.Module +import ShiggyXposed.xposed.Utils.Companion.JSON +import ShiggyXposed.xposed.Utils.Log +import android.app.Activity +import android.util.AtomicFile +import androidx.core.util.writeBytes +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.statement.* +import io.ktor.http.* +import java.io.File +import kotlinx.coroutines.* +import kotlinx.serialization.Serializable + +@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. + */ +class UpdaterModule : Module() { + private lateinit var config: LoaderConfig + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var error: Throwable? = null + + private lateinit var cacheDir: File + private lateinit var bundle: File + private lateinit var etag: File + + companion object { + var job: Job? = null + + private const val TIMEOUT_CACHED = 5000L + private const val TIMEOUT = 50000L + private const val ETAG_FILE = "etag.txt" + private const val CONFIG_FILE = "loader.json" + + private const val DEFAULT_BUNDLE_URL = + "https://github.com/kmmiio99o/ShiggyCord/releases/download/v1.0.0/shiggycord.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(configFile.readText()) + } else LoaderConfig() + } + .getOrDefault(LoaderConfig()) + + downloadScript() + } + + private 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) {} + } + } + 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) + error = e + } + } + } + + override fun onActivity(activity: Activity) { + error ?: return + error = null + downloadScript(activity) + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/FontsModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/FontsModule.kt new file mode 100644 index 0000000..1b62ebb --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/FontsModule.kt @@ -0,0 +1,271 @@ +package ShiggyXposed.xposed.modules.appearance + +import ShiggyXposed.xposed.Constants +import ShiggyXposed.xposed.Module +import ShiggyXposed.xposed.Utils.Companion.JSON +import ShiggyXposed.xposed.Utils.Log +import android.content.res.AssetManager +import android.graphics.Typeface +import android.graphics.Typeface.CustomFallbackBuilder +import android.graphics.fonts.Font +import android.graphics.fonts.FontFamily +import android.os.Build +import de.robv.android.xposed.XC_MethodReplacement +import de.robv.android.xposed.XposedHelpers +import de.robv.android.xposed.callbacks.XC_LoadPackage +import 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 java.io.File +import java.io.IOException +import kotlinx.coroutines.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.put + +@Serializable +data class FontDefinition( + val name: String? = null, + val description: String? = null, + val spec: Int? = null, + val main: Map, +) + +class FontsModule : Module() { + private companion object { + val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic") + val FILE_EXTENSIONS = arrayOf(".ttf", ".otf") + const val FONTS_ASSET_PATH = "fonts/" + } + + private lateinit var fontsDir: File + private lateinit var fontsDownloadsDir: File + private var fontsAbsPath: String? = null + + override fun buildPayload(builder: JsonObjectBuilder) { + builder.apply { put("fontPatch", 2) } + } + + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = + with(packageParam) { + XposedHelpers.findAndHookMethod( + "com.facebook.react.views.text.ReactFontManager\$Companion", + classLoader, + "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 (!fontDefFile.exists()) return@with + + val fontDef = + try { + JSON.decodeFromString(fontDefFile.readText()) + } catch (_: Throwable) { + return@with + } + + fontsDownloadsDir = + File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply { + asDir() + } + fontsDir = File(fontsDownloadsDir, fontDef.name!!).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 + 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) { + install(UserAgent) { + agent = Constants.USER_AGENT + } + } + + val response: HttpResponse = client.get(url) + if (response.status == HttpStatusCode.OK) + file.writeBytes(response.body()) + + return@async + } catch (e: Throwable) { + Log.e("Failed to download fonts ($name from $url)", e) + } + } + } + .awaitAll() + } + } + + private fun createAssetTypefaceWithFallbacks( + fontFamilyNames: Array, + style: Int, + assetManager: AssetManager + ): Typeface? { + val fontFamilies: MutableList = ArrayList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Iterate over the list of fontFamilyNames, constructing new FontFamily objects + // for use in the CustomFallbackBuilder below. + for (fontFamilyName in fontFamilyNames) { + try { + for (fileExtension in FILE_EXTENSIONS) { + val (customName, refName) = fontFamilyName.split(":") + val file = + File(fontsDownloadsDir, "$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) { + // If the typeface asset does not exist, try another extension. + continue + } catch (_: IOException) { + // If the font asset does not exist, try another extension. + continue + } + } + } + } + + // If there's some problem constructing fonts, fall back to the default behavior. + if (fontFamilies.isEmpty()) + return createAssetTypeface(fontFamilyNames[0], style, assetManager) + + val fallbackBuilder = CustomFallbackBuilder(fontFamilies[0]) + for (i in 1 until fontFamilies.size) { + fallbackBuilder.addCustomFallback(fontFamilies[i]) + } + return fallbackBuilder.build() + } + return null + } + + private fun createAssetTypeface( + fontFamilyName: String, + style: Int, + assetManager: AssetManager + ): Typeface? { + // This logic attempts to safely check if the frontend code is attempting to use + // fallback fonts, and if it is, to use the fallback typeface creation logic. + var fontFamilyName: String = fontFamilyName + val fontFamilyNames = + fontFamilyName.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (i in fontFamilyNames.indices) { + fontFamilyNames[i] = fontFamilyNames[i].trim() + } + + // If there are multiple font family names: + // For newer versions of Android, construct a Typeface with fallbacks + // For older versions of Android, ignore all the fallbacks and just use the first font + // family + if (fontFamilyNames.size > 1) { + fontFamilyName = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return createAssetTypefaceWithFallbacks( + fontFamilyNames, + style, + assetManager + ) + } else { + fontFamilyNames[0] + } + } + + val extension = EXTENSIONS[style] + + try { + for (fileExtension in FILE_EXTENSIONS) { + val (customName, refName) = fontFamilyName.split(":") + val file = + File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { + asFile() + } + if (!file.exists()) throw Exception() + return Typeface.createFromFile(file.absolutePath) + } + } catch (_: Throwable) {} + + // Lastly, after all those checks above, this is the original RN logic for + // getting the typeface. + for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) { + for (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) { + // If the typeface asset does not exist, try another extension. + continue + } + } + } + + return Typeface.create(fontFamilyName, style) + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/SysColorsModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/SysColorsModule.kt new file mode 100644 index 0000000..121c86a --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/SysColorsModule.kt @@ -0,0 +1,45 @@ +package ShiggyXposed.xposed.modules.appearance + +import ShiggyXposed.xposed.Module +import android.R.color +import android.app.AndroidAppHelper +import android.content.Context +import android.os.Build +import androidx.core.content.ContextCompat +import kotlinx.serialization.json.* + +class SysColorsModule : Module() { + private lateinit var context: Context + private fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + + override fun buildPayload(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 { + color::class.java.getField(colorName).getInt(null) + } + .getOrElse { 0 } + + add(convertToColor(colorResourceId)) + } + } + } + } + } + + private fun convertToColor(id: Int): String { + val clr = if (isSupported()) ContextCompat.getColor(context, id) else 0 + return String.format("#%06X", 0xFFFFFF and clr) + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/ThemesModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/ThemesModule.kt new file mode 100644 index 0000000..87f372a --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/appearance/ThemesModule.kt @@ -0,0 +1,160 @@ +package ShiggyXposed.xposed.modules.appearance + +import ShiggyXposed.xposed.Constants +import ShiggyXposed.xposed.Module +import ShiggyXposed.xposed.Utils.Companion.JSON +import android.content.Context +import android.content.res.Resources +import androidx.core.graphics.toColorInt +import de.robv.android.xposed.callbacks.XC_LoadPackage +import java.io.File +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.put + +@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? = null, + val spec: Int, + val semanticColors: Map>? = null, + val rawColors: Map? = null +) + +@Serializable data class Theme(val id: String, val selected: Boolean, val data: ThemeData) + +class ThemesModule : Module() { + private lateinit var param: XC_LoadPackage.LoadPackageParam + + private var theme: Theme? = null + private val rawColorMap = mutableMapOf() + + private companion object { + const val THEME_FILE = "current-theme.json" + } + + @ExperimentalSerializationApi + override fun buildPayload(builder: JsonObjectBuilder) { + builder.apply { + put("hasThemeSupport", true) + if (theme != null) put("storedTheme", JSON.encodeToJsonElement(theme!!)) + else put("storedTheme", null) + } + } + + private fun String.fromScreamingSnakeToCamelCase() = + this.split("_").joinToString("") { it -> + it.lowercase().replaceFirstChar { it.uppercase() } + } + + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) { + param = packageParam + theme = getTheme() + hookTheme() + } + + private fun File.isValidish(): Boolean { + if (!this.exists()) return false + + val text = this.readText() + return text.isNotBlank() && text != "{}" && text != "null" + } + + private fun getTheme(): Theme? { + val themeFile = + File(param.appInfo.dataDir, "${Constants.FILES_DIR}/${THEME_FILE}").apply { + asFile() + } + if (!themeFile.isValidish()) return null + + return try { + val themeText = themeFile.readText() + JSON.decodeFromString(themeText) + } catch (_: Exception) { + null + } + } + + fun hookTheme() { + val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") + val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkerTheme") + val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme") + + val theme = this.theme ?: 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.fromScreamingSnakeToCamelCase()}" + 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 = + MethodHookBuilder().run { + before { + val arg1 = args[0] + val resources = + if (arg1 is Context) arg1.resources else (arg1 as Resources) + val name = resources.getResourceEntryName(args[1] as Int) + + if (rawColorMap[name] != null) result = rawColorMap[name] + } + + build() + } + + getColorCompat.hook(patch) + getColorCompatLegacy.hook(patch) + } + } + + // Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB" + private fun hexStringToColorInt(hexString: String): Int { + return if (hexString.length == 9) { + // Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy + val alpha = hexString.substring(7, 9) + val rrggbb = hexString.substring(1, 7) + "#$alpha$rrggbb".toColorInt() + } else hexString.toColorInt() + } + + private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) { + try { + themeClass.getDeclaredMethod(methodName).let { method -> + method.hook { before { result = themeValue } } + } + } catch (_: NoSuchMethodException) {} + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt new file mode 100644 index 0000000..74bcc4b --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt @@ -0,0 +1,52 @@ +package ShiggyXposed.xposed.modules.bridge + +import ShiggyXposed.xposed.Module +import android.app.AlertDialog +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import android.widget.Toast + +class AdditionalBridgeMethodsModule : Module() { + override fun onContext(context: Context) = + with(context) { + BridgeModule.registerMethod("revenge.alertError") { + val (error, version) = it + val app = getAppInfo() + val errorString = "$error" + + val clipboard = + applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as + ClipboardManager + val clip = ClipData.newPlainText("Stack Trace", errorString) + + AlertDialog.Builder(this) + .setTitle("ShiggyXposed Error") + .setMessage( + """ + ShiggyXposed: $version + ${app.name}: ${app.version} (${app.versionCode}) + Device: ${Build.MANUFACTURER} ${Build.MODEL} + + + """.trimIndent() + + errorString + ) + .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() } + .setNeutralButton("Copy") { dialog, _ -> + clipboard.setPrimaryClip(clip) + Toast.makeText( + applicationContext, + "Copied stack trace", + Toast.LENGTH_SHORT + ) + .show() + dialog.dismiss() + } + .show() + + null + } + } +} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/BridgeModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/BridgeModule.kt new file mode 100644 index 0000000..eac6d27 --- /dev/null +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/BridgeModule.kt @@ -0,0 +1,202 @@ +package ShiggyXposed.xposed.modules.bridge + +import ShiggyXposed.xposed.BuildConfig +import ShiggyXposed.xposed.Constants +import ShiggyXposed.xposed.Module +import ShiggyXposed.xposed.Utils.Log +import de.robv.android.xposed.callbacks.XC_LoadPackage +import java.io.File +import java.lang.reflect.Method + +/** + * See for possible return types: + * https://github.com/facebook/react-native/blob/c23e84ae9/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/Arguments.kt#L19 + * + * You may return a [Unit] and the resulting value will be `null`. + */ +typealias BridgeMethodCallback = (args: BridgeMethodArgs) -> Any? + +typealias BridgeMethodArgs = ArrayList + +/** + * A module that exposes a bridge for calling methods from JavaScript. + * + * To call a method, pass an object with the following structure to a hooked method: + * ```js + * { + * revenge: { + * method: "method.name", + * args: [arg1, arg2, ...] + * } + * } + * ``` + * + * For methods that accept additional arguments, only the first object argument is used for the + * bridge call. + * + * The result will be an object with either a `result` or `error` key: + * ```js + * { + * result: ... // The return value of the method + * } + * ``` + * or + * ```js + * { + * error: "Error message" + * } + * ``` + */ +class BridgeModule : Module() { + private lateinit var readableMapGetString: Method + private lateinit var readableMapToHashMap: Method + private lateinit var argumentsMakeNative: Method + + companion object { + private const val CALL_DATA_KEY = "revenge" + private const val METHOD_NAME_KEY = "method" + private const val METHOD_ARGS_KEY = "args" + + private fun File.openFileGuarded() { + if (!this.exists()) throw Error("Path does not exist: $path") + if (!this.isFile) throw Error("Path is not a file: $path") + } + + private val methods: MutableMap = + mutableMapOf( + "revenge.info" to + { + mapOf( + "name" to Constants.LOADER_NAME, + "version" to BuildConfig.VERSION_CODE + ) + }, + "revenge.test" to + { + mapOf( + "string" to "string", + "number" to 7256, + "array" to listOf("testing", 527737, listOf(true)), + "object" to mapOf("nested" to true), + "boolean" to false, + "args" to it, + ) + }, + "revenge.fs.delete" to + { + val (path) = it + File(path as String).run { + if (this.isDirectory) this.deleteRecursively() + else this.delete() + } + }, + "revenge.fs.exists" to + { + val (path) = it + File(path as String).exists() + }, + "revenge.fs.read" to + { it -> + val (path) = it + val file = File(path as String).apply { openFileGuarded() } + + file.bufferedReader().use { it.readText() } + }, + "revenge.fs.write" to + { it -> + val (path, contents) = it + val file = File(path as String).apply { openFileGuarded() } + + file.writeText(contents as String) + } + ) + + /** + * Registers a bridge method that can be called from JavaScript. + * + * If the method is already registered, it will be overwritten. + * + * @param name The name of the method to register. + * @param callback The callback to invoke when the method is called. + */ + fun registerMethod(name: String, callback: BridgeMethodCallback) { + if (methods.containsKey(name)) + Log.w("Bridge method already exists and will be overridden: $name") + methods[name] = callback + } + } + + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = + with(packageParam) { + val arguments = classLoader.loadClass("com.facebook.react.bridge.Arguments") + val readableMap = classLoader.loadClass("com.facebook.react.bridge.ReadableMap") + val promise = classLoader.loadClass("com.facebook.react.bridge.Promise") + + val promiseResolve = promise.method("resolve", Object::class.java) + argumentsMakeNative = arguments.method("makeNativeObject", Object::class.java) + readableMapGetString = readableMap.method("getString", String::class.java) + readableMapToHashMap = readableMap.method("toHashMap") + + classLoader.loadClass("com.horcrux.svg.RNSVGRenderableManager").hookMethod( + "getBBox", + Double::class.javaObjectType, + readableMap + ) { + before { + callBridgeMethod(readableMapToHashMap(args[1]!!))?.let { + result = it.toNativeObject() + } + } + } + + classLoader.loadClass("com.facebook.react.modules.blob.FileReaderModule") + .hookMethod("readAsDataURL", readableMap, promise) { + before { + val (readableMap, promise) = args + callBridgeMethod(readableMapToHashMap(readableMap!!))?.let { + promiseResolve.invoke(promise, it.toNativeObject()) + result = null + } + } + } + + return@with + } + + private fun Any?.toNativeObject(): Any? = + argumentsMakeNative.invoke( + null, + when (this) { + Unit -> null + else -> this + } + ) + + private fun readableMapToHashMap(map: Any): HashMap { + @Suppress("UNCHECKED_CAST") return readableMapToHashMap.invoke(map) as HashMap + } + + private fun callBridgeMethod(hashMap: HashMap): Map? = + try { + val (method, args) = getBridgeCallData(hashMap) ?: return null + val ret = method(args).toNativeObject() + mapOf("result" to ret) + } catch (e: Throwable) { + mapOf("error" to e.toString()) + } + + private fun getBridgeCallData( + hashMap: HashMap + ): Pair? { + @Suppress("UNCHECKED_CAST") val data = hashMap[CALL_DATA_KEY] as HashMap? + data ?: return null + + @Suppress("UNCHECKED_CAST") val name = data[METHOD_NAME_KEY] as String + val method = methods[name] + method ?: throw Error("Method not registered: $name") + + @Suppress("UNCHECKED_CAST") val args = data[METHOD_ARGS_KEY] as BridgeMethodArgs + + return Pair(method, args) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a025b9..a9a427b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - KettuXposed - \ No newline at end of file + ShiggyXposed + diff --git a/build.gradle.kts b/build.gradle.kts index 56b0a88..d421a94 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.9.2") + classpath("com.android.tools.build:gradle:8.11.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10") // NOTE: Do not place your application dependencies here; they belong @@ -23,4 +23,4 @@ allprojects { tasks.register("clean") { delete(rootProject.layout.buildDirectory) -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index cc01b00..0000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,43 +0,0 @@ -[versions] -agp = "8.9.2" -kotlin = "2.1.0" -coreKtx = "1.15.0" -junit = "4.13.2" -junitVersion = "1.2.1" -espressoCore = "3.6.1" -appcompat = "1.7.0" -material = "1.12.0" -xposed = "82" -yukiApi = "1.2.1" -ksp = "2.1.0-1.0.29" -aliuhook = "main-SNAPSHOT" -libunbound = "1.0.0" -kotlinxSerializationJson = "1.8.0" -kotlinxCoroutinesAndroid = "1.10.1" -ktorClientCore = "3.1.1" -ktorClientCio = "3.1.1" -kotlinReflect = "2.1.20-RC" -kotlinSerialization = "2.1.0" - -[libraries] -androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } -aliuhook = { group = "com.aliucord", name = "Aliuhook", version.ref = "aliuhook" } -xposed = { group = "de.robv.android.xposed", name = "api", version.ref = "xposed" } -yuki-api = { group = "com.highcapable.yukihookapi", name = "api", version.ref = "yukiApi" } -libunbound = { group = "dev.rushii", name = "libunbound", version.ref = "libunbound" } -kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } -ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktorClientCore" } -ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktorClientCio" } -kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlinReflect" } - -[plugins] -android-application = { id = "com.android.application", version.ref = "agp" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -kotlin-serialization = { id= "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c8..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME