Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
f47d6d1d93
25 changed files with 1800 additions and 330 deletions
106
.github/workflows/build.yml
vendored
106
.github/workflows/build.yml
vendored
|
|
@ -1,65 +1,79 @@
|
|||
name: Build APK
|
||||
on: [push]
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 17
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and push
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
distribution: temurin
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
cache: gradle
|
||||
|
||||
- name: "Grant execution permission"
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Make Gradle executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: "Build APKs"
|
||||
- name: Optional Keystore (if provided)
|
||||
env:
|
||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||
run: |
|
||||
# Retry the Gradle build up to 3 times to mitigate transient cache/service errors
|
||||
ATTEMPT=0
|
||||
until [ $ATTEMPT -ge 3 ]
|
||||
do
|
||||
./gradlew build --no-daemon --stacktrace && break
|
||||
ATTEMPT=$((ATTEMPT+1))
|
||||
echo "Gradle build failed, retrying ($ATTEMPT/3)..."
|
||||
sleep 5
|
||||
done
|
||||
if [ $ATTEMPT -ge 3 ]; then
|
||||
echo "Gradle build failed after 3 attempts"
|
||||
exit 1
|
||||
if [ -n "$KEYSTORE" ]; then
|
||||
echo "$KEYSTORE" | base64 -d > signing-key.jks
|
||||
ls -l signing-key.jks
|
||||
else
|
||||
echo "No keystore provided, skipping..."
|
||||
fi
|
||||
|
||||
- name: "Setup directories"
|
||||
- name: Assemble Debug and Release
|
||||
env:
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
GRADLE_OPTS: -Dorg.gradle.daemon=false
|
||||
run: |
|
||||
mkdir -p /tmp/ShiggyXposedbuild/apks
|
||||
mkdir -p /tmp/ShiggyXposedbuild/tools
|
||||
if [ -f signing-key.jks ]; then
|
||||
./gradlew assembleDebug assembleRelease \
|
||||
-Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/signing-key.jks \
|
||||
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
|
||||
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
|
||||
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
|
||||
else
|
||||
./gradlew assembleDebug assembleRelease
|
||||
fi
|
||||
|
||||
- name: "Move debug APK"
|
||||
run: mv ./app/build/outputs/apk/debug/app-debug.apk /tmp/ShiggyXposedbuild/apks
|
||||
- name: Collect APKs
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp app/build/outputs/apk/debug/*.apk dist/ || true
|
||||
# Prefer signed release if produced
|
||||
if ls app/build/outputs/apk/release/*-signed.apk >/dev/null 2>&1; then
|
||||
cp app/build/outputs/apk/release/*-signed.apk dist/
|
||||
else
|
||||
cp app/build/outputs/apk/release/*.apk dist/
|
||||
fi
|
||||
ls -1 dist
|
||||
|
||||
- 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/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/ShiggyXposedbuild/apks/app-release-aligned-debugSigned.apk /tmp/ShiggyXposedbuild/apks/app-release.apk
|
||||
|
||||
- name: "Upload debug APK"
|
||||
- name: Upload All APKs
|
||||
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/ShiggyXposedbuild/apks/app-release.apk
|
||||
name: apks
|
||||
path: dist/*.apk
|
||||
if-no-files-found: error
|
||||
|
|
|
|||
149
.github/workflows/release.yml
vendored
149
.github/workflows/release.yml
vendored
|
|
@ -1,19 +1,23 @@
|
|||
name: Build Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
versionName:
|
||||
required: true
|
||||
description: This releases version name
|
||||
description: This release's version name
|
||||
default: "1.0.0"
|
||||
versionCode:
|
||||
required: true
|
||||
description: This releases version code
|
||||
description: This release's version code
|
||||
default: "1000"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 17
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -23,106 +27,79 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: 17
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: temurin
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2.0.10
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Set Version
|
||||
uses: chkfung/android-version-actions@v1.1
|
||||
with:
|
||||
gradlePath: app/build.gradle.kts
|
||||
versionCode: ${{github.event.inputs.versionCode}}
|
||||
versionName: ${{github.event.inputs.versionName}}
|
||||
versionCode: ${{ github.event.inputs.versionCode }}
|
||||
versionName: ${{ github.event.inputs.versionName }}
|
||||
|
||||
- name: Bump Version
|
||||
continue-on-error: true
|
||||
- name: Commit Version Bump (if changed)
|
||||
run: |
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
git config user.email "actions@github.com"
|
||||
git config user.name "GitHub Actions"
|
||||
git add app/build.gradle.kts
|
||||
if ! git diff --quiet app/build.gradle.kts; then
|
||||
git commit -m "chore: Bump version to ${{github.event.inputs.versionName}} (${{github.event.inputs.versionCode}})"
|
||||
git push
|
||||
if git diff --cached --quiet; then
|
||||
echo "No version change."
|
||||
else
|
||||
echo "No changes to version file, skipping commit"
|
||||
git commit -m "chore: bump version to ${{ github.event.inputs.versionName }} (${{ github.event.inputs.versionCode }})"
|
||||
git push
|
||||
fi
|
||||
|
||||
- name: Build Signed APK
|
||||
- name: Prepare Keystore
|
||||
run: |
|
||||
# Ensure keystore secret is present
|
||||
if [ -z "${{ secrets.keystore }}" ]; then
|
||||
echo "Keystore secret missing, aborting"
|
||||
exit 1
|
||||
fi
|
||||
echo "${{ secrets.KEYSTORE }}" | base64 -d > signing-key.jks
|
||||
ls -l signing-key.jks
|
||||
|
||||
echo "${{ secrets.keystore }}" | base64 -d > $GITHUB_WORKSPACE/signing-key.jks
|
||||
chmod +x ./gradlew
|
||||
|
||||
# Retry gradle up to 3 times to mitigate transient network/cache service issues
|
||||
ATTEMPT=0
|
||||
until [ $ATTEMPT -ge 3 ]
|
||||
do
|
||||
./gradlew packageReleaseUniversalApk \
|
||||
-Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/signing-key.jks \
|
||||
-Pandroid.injected.signing.store.password=${{ secrets.keystore_password }} \
|
||||
-Pandroid.injected.signing.key.alias=${{ secrets.key_alias }} \
|
||||
-Pandroid.injected.signing.key.password=${{ secrets.key_password }} --no-daemon --stacktrace && break
|
||||
|
||||
ATTEMPT=$((ATTEMPT+1))
|
||||
echo "Gradle build failed, retrying ($ATTEMPT/3)..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ $ATTEMPT -ge 3 ]; then
|
||||
echo "Gradle build failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Release
|
||||
- name: Build Universal APKs
|
||||
env:
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
GRADLE_OPTS: -Dorg.gradle.daemon=false
|
||||
run: |
|
||||
set -e
|
||||
echo "Locating built APK(s) under app/build/outputs..."
|
||||
# Find APK files produced by the build in common output locations
|
||||
apk_paths=$(find app/build/outputs -type f -name "*.apk" 2>/dev/null || true)
|
||||
if [ -z "$apk_paths" ]; then
|
||||
echo "ERROR: No APK files found under app/build/outputs"
|
||||
echo "Diagnostics: top-level listing of app/build/outputs:"
|
||||
ls -la app/build/outputs || true
|
||||
echo "Diagnostics: recursive listing (files only):"
|
||||
find app/build/outputs -type f -maxdepth 6 -print || true
|
||||
echo "Failing the job so you can inspect the build logs and diagnostics above."
|
||||
exit 1
|
||||
fi
|
||||
# Prepare a directory to collect APKs for the release
|
||||
mkdir -p release-apks
|
||||
echo "Copying found APK(s) to release-apks/ for upload:"
|
||||
for apk in $apk_paths; do
|
||||
echo " - $apk"
|
||||
cp "$apk" "release-apks/$(basename "$apk")"
|
||||
done
|
||||
# Keep backward compatibility: if exactly one APK found, copy it to the root as app-release.apk
|
||||
apk_count=$(echo "$apk_paths" | wc -w)
|
||||
if [ "$apk_count" -eq 1 ]; then
|
||||
single_apk=$(echo "$apk_paths")
|
||||
cp "$single_apk" ./app-release.apk
|
||||
echo "Single APK copied to ./app-release.apk"
|
||||
else
|
||||
echo "Multiple APKs found ($apk_count). All copied to release-apks/"
|
||||
fi
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
tag="${{ github.event.inputs.versionCode }}"
|
||||
git tag "$tag"
|
||||
git push origin "$tag"
|
||||
# Create the GitHub release including all APK(s) we collected (debug + release)
|
||||
gh release create "$tag" \
|
||||
--title "${{ github.event.inputs.versionName }}" \
|
||||
--generate-notes \
|
||||
release-apks/*
|
||||
./gradlew packageDebugUniversalApk packageReleaseUniversalApk \
|
||||
-Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/signing-key.jks \
|
||||
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
|
||||
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
|
||||
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
|
||||
|
||||
- name: Collect APKs
|
||||
run: |
|
||||
mkdir dist
|
||||
mv app/build/outputs/apk_from_bundle/debug/app-debug-universal.apk dist/app-debug.apk
|
||||
mv app/build/outputs/apk_from_bundle/release/app-release-universal.apk dist/app-release.apk
|
||||
ls -l dist
|
||||
|
||||
- name: Upload APK Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-apks
|
||||
path: dist/*.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create Tag and Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag=${{ github.event.inputs.versionCode }}
|
||||
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
|
||||
echo "Tag $tag already exists, skipping creation."
|
||||
else
|
||||
git tag "$tag"
|
||||
git push origin "$tag"
|
||||
fi
|
||||
gh release view "$tag" >/dev/null 2>&1 || gh release create "$tag" \
|
||||
--title "${{ github.event.inputs.versionName }}" \
|
||||
--generate-notes \
|
||||
dist/app-debug.apk dist/app-release.apk
|
||||
|
|
|
|||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
|
|
@ -5,15 +5,15 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
namespace = "GoonXposed.xposed"
|
||||
namespace = "ShiggyXposed.xposed"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "GoonXposed.xposed"
|
||||
applicationId = "ShiggyXposed.xposed"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1210
|
||||
versionName = "1.2.1"
|
||||
versionCode = 1220
|
||||
versionName = "1.2.2"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -28,33 +28,24 @@ android {
|
|||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
kotlinOptions { jvmTarget = "17" }
|
||||
buildFeatures { buildConfig = true }
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
maven("https://jitpack.io")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("de.robv.android.xposed:api:82")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
|
||||
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")
|
||||
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.core:core-ktx:1.17.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
|
||||
implementation("io.ktor:ktor-client-core:3.3.1")
|
||||
implementation("io.ktor:ktor-client-cio:3.3.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<manifest
|
||||
package="GoonXposed.xposed"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
|
|
@ -12,7 +13,7 @@
|
|||
<meta-data android:name="xposedmodule" android:value="true" />
|
||||
<meta-data
|
||||
android:name="xposeddescription"
|
||||
android:value="Xposed module to inject GoonCord into discord"
|
||||
android:value="Xposed module to inject ShiggyCord into discord"
|
||||
/>
|
||||
<meta-data android:name="xposedminversion" android:value="82" />
|
||||
<meta-data android:name="xposedscope" android:value="com.discord" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package GoonXposed.xposed
|
||||
package ShiggyXposed.xposed
|
||||
|
||||
class Constants {
|
||||
companion object {
|
||||
|
|
@ -9,10 +9,11 @@ class Constants {
|
|||
const val CACHE_DIR = "cache/pyoncord"
|
||||
const val MAIN_SCRIPT_FILE = "bundle.js"
|
||||
|
||||
const val LOG_TAG = "GoonXposed"
|
||||
|
||||
const val LOADER_NAME = "GoonXposed"
|
||||
const val LOG_TAG = "Shiggy"
|
||||
|
||||
const val USER_AGENT = "GoonXposed"
|
||||
const val LOADER_NAME = "ShiggyXposed"
|
||||
|
||||
const val USER_AGENT = "ShiggyXposed"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package GoonXposed.xposed
|
||||
package ShiggyXposed.xposed
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
|
@ -7,15 +7,15 @@ import android.os.Bundle
|
|||
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import GoonXposed.xposed.modules.*
|
||||
import GoonXposed.xposed.modules.appearance.FontsModule
|
||||
import GoonXposed.xposed.modules.appearance.SysColorsModule
|
||||
import GoonXposed.xposed.modules.appearance.ThemesModule
|
||||
import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
|
||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||
import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule
|
||||
import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
|
||||
import ShiggyXposed.xposed.Utils.Log
|
||||
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 ShiggyXposed.xposed.modules.no_track.BlockCrashReportingModule
|
||||
import ShiggyXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
object HookStateHolder {
|
||||
|
|
@ -35,7 +35,7 @@ object HookStateHolder {
|
|||
|
||||
class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
private var hooked = false
|
||||
private val modules = mutableListOf<Module>(
|
||||
private val modules = mutableListOf(
|
||||
HookScriptLoaderModule,
|
||||
BridgeModule,
|
||||
AdditionalBridgeMethodsModule,
|
||||
|
|
@ -7,25 +7,28 @@ 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 kotlinx.serialization.json.JsonObjectBuilder
|
||||
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,
|
||||
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) {}
|
||||
/**
|
||||
* 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. */
|
||||
/**
|
||||
* Called during Zygote initialization.
|
||||
*/
|
||||
open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
|
||||
|
||||
/**
|
||||
|
|
@ -38,11 +41,10 @@ abstract class Module {
|
|||
open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {}
|
||||
|
||||
/**
|
||||
* Called after [Context] has been attached to a [android.content.ContextWrapper] via
|
||||
* [android.content.ContextWrapper.attachBaseContext].
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
|
|
@ -55,25 +57,22 @@ abstract class Module {
|
|||
*
|
||||
* 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.
|
||||
* 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.
|
||||
* 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()
|
||||
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,
|
||||
packageManager.getApplicationLabel(applicationInfo).toString(),
|
||||
packageName,
|
||||
pInfo.versionName ?: versionCode.toString(),
|
||||
versionCode,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -86,22 +85,21 @@ abstract class Module {
|
|||
if (!this.isFile) this.deleteRecursively()
|
||||
}
|
||||
|
||||
protected fun Class<*>.method(name: String, vararg params: Class<*>?): Method =
|
||||
getDeclaredMethod(name, *params).apply { isAccessible = true }
|
||||
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 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(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())
|
||||
hook(MethodHookBuilder().apply(block).build())
|
||||
|
||||
protected fun Class<*>.hookMethod(
|
||||
name: String,
|
||||
vararg params: Class<*>?,
|
||||
block: MethodHookBuilder.() -> Unit
|
||||
name: String, vararg params: Class<*>?, block: MethodHookBuilder.() -> Unit
|
||||
): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build())
|
||||
|
||||
protected class MethodHookBuilder {
|
||||
|
|
@ -116,36 +114,33 @@ abstract class Module {
|
|||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -159,23 +154,19 @@ abstract class Module {
|
|||
* @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
|
||||
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.
|
||||
* 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 thisObject: Any? get() = param.thisObject
|
||||
|
||||
val args: Array<Any?>
|
||||
get() = param.args
|
||||
val args: Array<Any?> get() = param.args
|
||||
|
||||
var result: Any?
|
||||
get() = param.result
|
||||
58
app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt
Normal file
58
app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package ShiggyXposed.xposed
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.AndroidAppHelper
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import ShiggyXposed.xposed.modules.UpdaterModule
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun showRecoveryAlert(context: Context) {
|
||||
AlertDialog.Builder(context).setTitle("ShiggyCord Recovery Options")
|
||||
.setItems(arrayOf("Reload", "Delete Script", "Reset Loader Config")) { _, which ->
|
||||
when (which) {
|
||||
0 -> {
|
||||
reloadApp()
|
||||
}
|
||||
|
||||
1 -> {
|
||||
val bundleFile = File(
|
||||
context.dataDir, "${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}"
|
||||
)
|
||||
|
||||
if (bundleFile.exists()) bundleFile.delete()
|
||||
|
||||
reloadApp()
|
||||
}
|
||||
|
||||
2 -> {
|
||||
UpdaterModule.resetLoaderConfig(context)
|
||||
reloadApp()
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
object Log {
|
||||
fun e(msg: String) = android.util.Log.e(Constants.LOG_TAG, msg)
|
||||
fun e(msg: String, throwable: Throwable) = android.util.Log.e(Constants.LOG_TAG, msg, throwable)
|
||||
fun i(msg: String) = android.util.Log.i(Constants.LOG_TAG, msg)
|
||||
fun i(msg: String, throwable: Throwable) = android.util.Log.i(Constants.LOG_TAG, msg, throwable)
|
||||
fun w(msg: String) = android.util.Log.w(Constants.LOG_TAG, msg)
|
||||
fun w(msg: String, throwable: Throwable) = android.util.Log.w(Constants.LOG_TAG, msg, throwable)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package GoonXposed.xposed.modules
|
||||
package ShiggyXposed.xposed.modules
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils.Log
|
||||
import ShiggyXposed.xposed.modules.bridge.BridgeModule
|
||||
import java.io.*
|
||||
|
||||
/**
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package ShiggyXposed.xposed.modules
|
||||
|
||||
import android.content.res.Resources
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import ShiggyXposed.xposed.Constants.Companion.TARGET_PACKAGE
|
||||
import ShiggyXposed.xposed.Module
|
||||
|
||||
/**
|
||||
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
|
||||
*/
|
||||
object FixResourcesModule : Module() {
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
if (packageName != TARGET_PACKAGE) {
|
||||
Resources::class.java.hookMethod(
|
||||
"getIdentifier", String::class.java, String::class.java, String::class.java
|
||||
) {
|
||||
before {
|
||||
if (args[2] == packageName) args[2] = TARGET_PACKAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package ShiggyXposed.xposed.modules
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.XModuleResources
|
||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import ShiggyXposed.xposed.Constants
|
||||
import ShiggyXposed.xposed.HookStateHolder
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils.Log
|
||||
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
* Hooks React Native's script loading methods to load custom scripts and bundles.
|
||||
*
|
||||
* Preload scripts should be placed in the [PRELOADS_DIR] directory inside the module's files directory.
|
||||
*
|
||||
* The main bundle should be placed in the [Constants.CACHE_DIR] directory named [Constants.MAIN_SCRIPT_FILE].
|
||||
* If the bundle file does not exist, it will attempt to load `assets://Shiggy.bundle` from the module's assets.
|
||||
*/
|
||||
object HookScriptLoaderModule : Module() {
|
||||
private lateinit var preloadsDir: File
|
||||
private lateinit var mainScript: File
|
||||
|
||||
private lateinit var modulePath: String
|
||||
private lateinit var resources: XModuleResources
|
||||
|
||||
const val PRELOADS_DIR = "preloads"
|
||||
|
||||
override fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {
|
||||
this@HookScriptLoaderModule.modulePath = startupParam.modulePath
|
||||
}
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
val cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { asDir() }
|
||||
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { asDir() }
|
||||
|
||||
preloadsDir = File(filesDir, PRELOADS_DIR).apply { asDir() }
|
||||
mainScript = File(cacheDir, Constants.MAIN_SCRIPT_FILE).apply { asFile() }
|
||||
|
||||
listOf(
|
||||
"com.facebook.react.runtime.ReactInstance\$loadJSBundle$1",
|
||||
"com.facebook.react.runtime.ReactInstance$1",
|
||||
// TODO: Remove once Discord fully switches to Bridgeless
|
||||
"com.facebook.react.bridge.CatalystInstanceImpl"
|
||||
).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) }
|
||||
}
|
||||
|
||||
private fun hook(instance: Class<*>) = runCatching {
|
||||
val loadScriptFromAssets = instance.method(
|
||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||
)
|
||||
|
||||
val loadScriptFromFile = instance.method(
|
||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||
)
|
||||
|
||||
loadScriptFromAssets.hook {
|
||||
before {
|
||||
Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})")
|
||||
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
|
||||
}
|
||||
}
|
||||
|
||||
loadScriptFromFile.hook {
|
||||
before {
|
||||
Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})")
|
||||
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Log.e("Failed to hook script loading methods in ${instance.name}:", it)
|
||||
}
|
||||
|
||||
private fun HookScope.runCustomScripts(loadScriptFromFile: Method, loadScriptFromAssets: Method) {
|
||||
Log.i("Running custom scripts...")
|
||||
|
||||
runBlocking {
|
||||
val ready = async { HookStateHolder.readyDeferred.join() }
|
||||
val download = async { UpdaterModule.downloadScript().join() }
|
||||
|
||||
awaitAll(ready, download)
|
||||
}
|
||||
|
||||
val loadSynchronously = args[2]
|
||||
val runScriptFile = { file: File ->
|
||||
Log.i("Loading script: ${file.absolutePath}")
|
||||
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
|
||||
)
|
||||
|
||||
Unit
|
||||
}
|
||||
|
||||
try {
|
||||
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
|
||||
|
||||
if (mainScript.exists()) runScriptFile(mainScript)
|
||||
else {
|
||||
Log.i("Main script does not exist, falling back")
|
||||
|
||||
if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
|
||||
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
loadScriptFromAssets,
|
||||
thisObject,
|
||||
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Unable to run scripts:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,80 @@
|
|||
package ShiggyXposed.xposed.modules
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import ShiggyXposed.xposed.BuildConfig
|
||||
import ShiggyXposed.xposed.Constants
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils.Companion.JSON
|
||||
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.io.File
|
||||
|
||||
// TODO: Remove this redundant module and merge its functionality into AdditionalBridgeMethodsModule if needed
|
||||
class PayloadGlobalModule(private val modules: List<Module>) : Module() {
|
||||
private companion object {
|
||||
const val GLOBAL_NAME = "__PYON_LOADER__"
|
||||
}
|
||||
|
||||
private fun getPayloadString(): String = JSON.encodeToString(
|
||||
buildJsonObject {
|
||||
put("loaderName", Constants.LOADER_NAME)
|
||||
put("loaderVersion", BuildConfig.VERSION_NAME)
|
||||
@Suppress("DEPRECATION") for (module in modules) module.buildPayload(this)
|
||||
})
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
val catalystInstance = classLoader.safeLoadClass("com.facebook.react.bridge.CatalystInstanceImpl")
|
||||
val scriptLoader = classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance\$loadJSBundle$1")
|
||||
?: classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance$1")
|
||||
|
||||
val setGlobalVariable: (XC_MethodHook.MethodHookParam, String, String) -> Unit = { param, key, json ->
|
||||
runCatching {
|
||||
// Attempt old CatalystInstanceImpl method
|
||||
catalystInstance!!.method(
|
||||
"setGlobalVariable", String::class.java, String::class.java
|
||||
).invoke(param.thisObject, key, json)
|
||||
}.onFailure {
|
||||
// Failed (likely because it's a stub), use a workaround
|
||||
val preloadsDir = File("${appInfo.dataDir}/${Constants.FILES_DIR}", PRELOADS_DIR)
|
||||
File(preloadsDir, "rv_globals_$key.js").apply {
|
||||
writeText("this[${JSON.encodeToString(key)}]=$json")
|
||||
|
||||
XposedBridge.invokeOriginalMethod(
|
||||
scriptLoader!!.method(
|
||||
"loadScriptFromFile",
|
||||
String::class.java,
|
||||
String::class.java,
|
||||
Boolean::class.javaPrimitiveType
|
||||
), param.thisObject, arrayOf(absolutePath, absolutePath, param.args[2])
|
||||
)
|
||||
|
||||
delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val hook = MethodHookBuilder().run {
|
||||
before {
|
||||
setGlobalVariable(param, GLOBAL_NAME, getPayloadString())
|
||||
}
|
||||
|
||||
build()
|
||||
}
|
||||
|
||||
listOf(catalystInstance, scriptLoader).forEach { if (it != null) hook(it, hook) }
|
||||
}
|
||||
|
||||
private fun hook(instance: Class<*>, hook: XC_MethodHook) {
|
||||
instance.method(
|
||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType,
|
||||
).hook(hook)
|
||||
|
||||
instance.method(
|
||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||
).hook(hook)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package GoonXposed.xposed.modules
|
||||
package ShiggyXposed.xposed.modules
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils.Log
|
||||
import ShiggyXposed.xposed.modules.bridge.BridgeModule
|
||||
import java.io.*
|
||||
|
||||
/**
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package ShiggyXposed.xposed.modules
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import android.widget.Toast
|
||||
import androidx.core.util.writeBytes
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import ShiggyXposed.xposed.Constants
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils
|
||||
import ShiggyXposed.xposed.Utils.Companion.JSON
|
||||
import ShiggyXposed.xposed.Utils.Companion.reloadApp
|
||||
import ShiggyXposed.xposed.Utils.Log
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@Serializable
|
||||
data class CustomLoadUrl(
|
||||
val enabled: Boolean = false, val url: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoaderConfig(
|
||||
val customLoadUrl: CustomLoadUrl = CustomLoadUrl()
|
||||
)
|
||||
|
||||
/**
|
||||
* Module that updates the JS bundle by downloading it from a remote URL.
|
||||
*
|
||||
* Shows dialogs when failed allowing retry.
|
||||
*/
|
||||
object UpdaterModule : Module() {
|
||||
private lateinit var config: LoaderConfig
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var lastActivity: WeakReference<Activity>? = null
|
||||
|
||||
private lateinit var cacheDir: File
|
||||
private lateinit var bundle: File
|
||||
private lateinit var etag: File
|
||||
|
||||
private const val TIMEOUT_CACHED = 5000L
|
||||
private const val TIMEOUT = 10000L
|
||||
private const val ETAG_FILE = "etag.txt"
|
||||
private const val CONFIG_FILE = "loader.json"
|
||||
|
||||
private const val DEFAULT_BUNDLE_URL =
|
||||
"https://github.com/kmmiio99o/ShiggyCord/releases/latest/download/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<LoaderConfig>(configFile.readText())
|
||||
} else LoaderConfig()
|
||||
}.getOrDefault(LoaderConfig())
|
||||
}
|
||||
|
||||
fun downloadScript(activity: Activity? = null): Job = scope.launch {
|
||||
try {
|
||||
HttpClient(CIO) {
|
||||
expectSuccess = false
|
||||
install(UserAgent) { agent = Constants.USER_AGENT }
|
||||
install(HttpRedirect) {}
|
||||
}.use { client ->
|
||||
val url = config.customLoadUrl.takeIf { it.enabled }?.url ?: DEFAULT_BUNDLE_URL
|
||||
Log.i("Fetching JS bundle from: $url")
|
||||
|
||||
val response: HttpResponse = client.get(url) {
|
||||
headers {
|
||||
if (etag.exists() && bundle.exists()) {
|
||||
append(HttpHeaders.IfNoneMatch, etag.readText())
|
||||
}
|
||||
}
|
||||
|
||||
// Retries don't need timeout
|
||||
if (activity == null) {
|
||||
timeout {
|
||||
requestTimeoutMillis = if (!bundle.exists()) TIMEOUT else TIMEOUT_CACHED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val bytes: ByteArray = response.body()
|
||||
AtomicFile(bundle).writeBytes(bytes)
|
||||
|
||||
val newTag = response.headers[HttpHeaders.ETag]
|
||||
if (!newTag.isNullOrEmpty()) etag.writeText(newTag) else etag.delete()
|
||||
|
||||
Log.i("Bundle updated (${bytes.size} bytes)")
|
||||
|
||||
// This is a retry, so we show a dialog
|
||||
if (activity != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
AlertDialog.Builder(activity).setTitle("ShiggyCord Update Successful")
|
||||
.setMessage("A reload is required for changes to take effect.")
|
||||
.setPositiveButton("Reload") { dialog, _ ->
|
||||
reloadApp()
|
||||
dialog.dismiss()
|
||||
}.setCancelable(false).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpStatusCode.NotModified -> {
|
||||
Log.i("Server responded with 304, no changes")
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw ResponseException(response, "Received status: ${response.status}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Failed to download script", e)
|
||||
showErrorDialog(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivity(activity: Activity) {
|
||||
lastActivity = WeakReference(activity)
|
||||
}
|
||||
|
||||
fun showErrorDialog(e: Throwable) {
|
||||
val activity = lastActivity?.get() ?: return
|
||||
|
||||
activity.runOnUiThread {
|
||||
}
|
||||
}
|
||||
|
||||
fun resetLoaderConfig(context: Context) {
|
||||
val filesDir = File(context.dataDir, Constants.FILES_DIR)
|
||||
val config = File(filesDir, CONFIG_FILE)
|
||||
if (config.exists()) config.delete()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package GoonXposed.xposed.modules.appearance
|
||||
package ShiggyXposed.xposed.modules.appearance
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import android.graphics.Typeface
|
||||
|
|
@ -9,10 +9,10 @@ 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 GoonXposed.xposed.Constants
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Companion.JSON
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import ShiggyXposed.xposed.Constants
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils.Companion.JSON
|
||||
import ShiggyXposed.xposed.Utils.Log
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package GoonXposed.xposed.modules.appearance
|
||||
package ShiggyXposed.xposed.modules.appearance
|
||||
|
||||
import android.R.color
|
||||
import android.app.AndroidAppHelper
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import GoonXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Module
|
||||
import kotlinx.serialization.json.*
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package GoonXposed.xposed.modules.appearance
|
||||
package ShiggyXposed.xposed.modules.appearance
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import androidx.core.graphics.toColorInt
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.Constants
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Companion.JSON
|
||||
import ShiggyXposed.xposed.Constants
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils.Companion.JSON
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package GoonXposed.xposed.modules.bridge
|
||||
package ShiggyXposed.xposed.modules.bridge
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
|
|
@ -7,8 +7,8 @@ import android.content.ClipboardManager
|
|||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils
|
||||
import java.io.File
|
||||
|
||||
object AdditionalBridgeMethodsModule : Module() {
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package GoonXposed.xposed.modules.bridge
|
||||
package ShiggyXposed.xposed.modules.bridge
|
||||
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.BuildConfig
|
||||
import GoonXposed.xposed.Constants
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import ShiggyXposed.xposed.BuildConfig
|
||||
import ShiggyXposed.xposed.Constants
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils.Log
|
||||
import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package ShiggyXposed.xposed.modules.no_track
|
||||
|
||||
import android.content.Context
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils.Log
|
||||
|
||||
/**
|
||||
* Hooks Discord's crash reporting to disable Sentry initialization.
|
||||
* While Discord doesn't have Sentry auto-initialization on by default, we still hook the content provider just to be safe.
|
||||
*/
|
||||
object BlockCrashReportingModule : Module() {
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
val crashReportingClass = classLoader.safeLoadClass("com.discord.crash_reporting.CrashReporting")
|
||||
crashReportingClass?.apply {
|
||||
|
||||
// This only exists on 30720x and above
|
||||
runCatching {
|
||||
hookMethod("isDisabled") {
|
||||
before {
|
||||
Log.i("Forced CrashReporting.isDisabled() to true")
|
||||
result = true
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
// In older versions, this hook works fine.
|
||||
// Hooking this on 30720x will result in a crash after a few seconds,
|
||||
// since Discord asserts initialization when setting a Sentry tag.
|
||||
hookMethod(
|
||||
"init", Context::class.java, String::class.java
|
||||
) {
|
||||
before {
|
||||
Log.i("Blocked CrashReporting initialization")
|
||||
result = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sentryInitProviderClass = classLoader.safeLoadClass("io.sentry.android.core.SentryInitProvider")
|
||||
sentryInitProviderClass?.hookMethod("onCreate") {
|
||||
before {
|
||||
Log.i("Blocked SentryInitProvider initialization")
|
||||
result = true
|
||||
}
|
||||
}
|
||||
|
||||
return@with
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package GoonXposed.xposed.modules.no_track
|
||||
package ShiggyXposed.xposed.modules.no_track
|
||||
|
||||
import android.content.Context
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils.Log
|
||||
|
||||
/**
|
||||
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package GoonXposed.xposed.modules.no_track
|
||||
|
||||
import android.content.Context
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import GoonXposed.xposed.Module
|
||||
import GoonXposed.xposed.Utils.Log
|
||||
|
||||
/**
|
||||
* Hooks Discord's crash reporting to disable Sentry initialization.
|
||||
* While Discord doesn't have Sentry auto-initialization on by default, we still hook the content provider just to be safe.
|
||||
*/
|
||||
object BlockCrashReportingModule : Module() {
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
val crashReportingClass = classLoader.safeLoadClass("com.discord.crash_reporting.CrashReporting")
|
||||
crashReportingClass?.apply {
|
||||
// Hooking this will result in a crash after a few seconds, since Discord expects initialization to complete when setting a Sentry tag.
|
||||
// So we also hook isDisabled to make sure those calls are no-ops.
|
||||
hookMethod(
|
||||
"init", Context::class.java, String::class.java
|
||||
) {
|
||||
before {
|
||||
Log.i("Blocked CrashReporting initialization")
|
||||
result = null
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
hookMethod("isDisabled") {
|
||||
before {
|
||||
Log.i("Forced CrashReporting.isDisabled() to true")
|
||||
result = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sentryInitProviderClass = classLoader.safeLoadClass("io.sentry.android.core.SentryInitProvider")
|
||||
sentryInitProviderClass?.hookMethod("onCreate") {
|
||||
before {
|
||||
Log.i("Blocked SentryInitProvider initialization")
|
||||
result = true
|
||||
}
|
||||
}
|
||||
|
||||
return@with
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
rootProject.name = "KettuXposed"
|
||||
rootProject.name = "ShiggyXposed"
|
||||
include(":app")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue