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
|
name: Build APK
|
||||||
on: [push]
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
JAVA_VERSION: 17
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and push
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout
|
||||||
with:
|
uses: actions/checkout@v4
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: actions/setup-java@v3
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: temurin
|
||||||
java-version: "17"
|
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
|
run: chmod +x ./gradlew
|
||||||
|
|
||||||
- name: "Build APKs"
|
- name: Optional Keystore (if provided)
|
||||||
|
env:
|
||||||
|
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||||
run: |
|
run: |
|
||||||
# Retry the Gradle build up to 3 times to mitigate transient cache/service errors
|
if [ -n "$KEYSTORE" ]; then
|
||||||
ATTEMPT=0
|
echo "$KEYSTORE" | base64 -d > signing-key.jks
|
||||||
until [ $ATTEMPT -ge 3 ]
|
ls -l signing-key.jks
|
||||||
do
|
else
|
||||||
./gradlew build --no-daemon --stacktrace && break
|
echo "No keystore provided, skipping..."
|
||||||
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
|
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: |
|
run: |
|
||||||
mkdir -p /tmp/ShiggyXposedbuild/apks
|
if [ -f signing-key.jks ]; then
|
||||||
mkdir -p /tmp/ShiggyXposedbuild/tools
|
./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"
|
- name: Collect APKs
|
||||||
run: mv ./app/build/outputs/apk/debug/app-debug.apk /tmp/ShiggyXposedbuild/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"
|
- name: Upload All APKs
|
||||||
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"
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app-debug
|
name: apks
|
||||||
path: /tmp/ShiggyXposedbuild/apks/app-debug.apk
|
path: dist/*.apk
|
||||||
|
if-no-files-found: error
|
||||||
- name: "Upload release APK"
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: app-release
|
|
||||||
path: /tmp/ShiggyXposedbuild/apks/app-release.apk
|
|
||||||
|
|
|
||||||
141
.github/workflows/release.yml
vendored
141
.github/workflows/release.yml
vendored
|
|
@ -1,19 +1,23 @@
|
||||||
name: Build Release
|
name: Build Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
versionName:
|
versionName:
|
||||||
required: true
|
required: true
|
||||||
description: This releases version name
|
description: This release's version name
|
||||||
default: "1.0.0"
|
default: "1.0.0"
|
||||||
versionCode:
|
versionCode:
|
||||||
required: true
|
required: true
|
||||||
description: This releases version code
|
description: This release's version code
|
||||||
default: "1000"
|
default: "1000"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
JAVA_VERSION: 17
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -23,14 +27,15 @@ jobs:
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup JDK 17
|
- name: Setup JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
java-version: 17
|
distribution: temurin
|
||||||
|
cache: gradle
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v2.0.10
|
uses: android-actions/setup-android@v2
|
||||||
|
|
||||||
- name: Set Version
|
- name: Set Version
|
||||||
uses: chkfung/android-version-actions@v1.1
|
uses: chkfung/android-version-actions@v1.1
|
||||||
|
|
@ -39,90 +44,62 @@ jobs:
|
||||||
versionCode: ${{ github.event.inputs.versionCode }}
|
versionCode: ${{ github.event.inputs.versionCode }}
|
||||||
versionName: ${{ github.event.inputs.versionName }}
|
versionName: ${{ github.event.inputs.versionName }}
|
||||||
|
|
||||||
- name: Bump Version
|
- name: Commit Version Bump (if changed)
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "actions@github.com"
|
git config user.email "actions@github.com"
|
||||||
git config --local user.name "GitHub Actions"
|
git config user.name "GitHub Actions"
|
||||||
git add app/build.gradle.kts
|
git add app/build.gradle.kts
|
||||||
if ! git diff --quiet app/build.gradle.kts; then
|
if git diff --cached --quiet; then
|
||||||
git commit -m "chore: Bump version to ${{github.event.inputs.versionName}} (${{github.event.inputs.versionCode}})"
|
echo "No version change."
|
||||||
|
else
|
||||||
|
git commit -m "chore: bump version to ${{ github.event.inputs.versionName }} (${{ github.event.inputs.versionCode }})"
|
||||||
git push
|
git push
|
||||||
else
|
|
||||||
echo "No changes to version file, skipping commit"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build Signed APK
|
- name: Prepare Keystore
|
||||||
run: |
|
run: |
|
||||||
# Ensure keystore secret is present
|
echo "${{ secrets.KEYSTORE }}" | base64 -d > signing-key.jks
|
||||||
if [ -z "${{ secrets.keystore }}" ]; then
|
ls -l signing-key.jks
|
||||||
echo "Keystore secret missing, aborting"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${{ secrets.keystore }}" | base64 -d > $GITHUB_WORKSPACE/signing-key.jks
|
- name: Build Universal APKs
|
||||||
chmod +x ./gradlew
|
env:
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
# Retry gradle up to 3 times to mitigate transient network/cache service issues
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
ATTEMPT=0
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||||
until [ $ATTEMPT -ge 3 ]
|
GRADLE_OPTS: -Dorg.gradle.daemon=false
|
||||||
do
|
run: |
|
||||||
./gradlew packageReleaseUniversalApk \
|
./gradlew packageDebugUniversalApk packageReleaseUniversalApk \
|
||||||
-Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/signing-key.jks \
|
-Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/signing-key.jks \
|
||||||
-Pandroid.injected.signing.store.password=${{ secrets.keystore_password }} \
|
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
|
||||||
-Pandroid.injected.signing.key.alias=${{ secrets.key_alias }} \
|
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
|
||||||
-Pandroid.injected.signing.key.password=${{ secrets.key_password }} --no-daemon --stacktrace && break
|
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
|
||||||
|
|
||||||
ATTEMPT=$((ATTEMPT+1))
|
- name: Collect APKs
|
||||||
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
|
|
||||||
run: |
|
run: |
|
||||||
set -e
|
mkdir dist
|
||||||
echo "Locating built APK(s) under app/build/outputs..."
|
mv app/build/outputs/apk_from_bundle/debug/app-debug-universal.apk dist/app-debug.apk
|
||||||
# Find APK files produced by the build in common output locations
|
mv app/build/outputs/apk_from_bundle/release/app-release-universal.apk dist/app-release.apk
|
||||||
apk_paths=$(find app/build/outputs -type f -name "*.apk" 2>/dev/null || true)
|
ls -l dist
|
||||||
if [ -z "$apk_paths" ]; then
|
|
||||||
echo "ERROR: No APK files found under app/build/outputs"
|
- name: Upload APK Artifacts
|
||||||
echo "Diagnostics: top-level listing of app/build/outputs:"
|
uses: actions/upload-artifact@v4
|
||||||
ls -la app/build/outputs || true
|
with:
|
||||||
echo "Diagnostics: recursive listing (files only):"
|
name: release-apks
|
||||||
find app/build/outputs -type f -maxdepth 6 -print || true
|
path: dist/*.apk
|
||||||
echo "Failing the job so you can inspect the build logs and diagnostics above."
|
if-no-files-found: error
|
||||||
exit 1
|
|
||||||
fi
|
- name: Create Tag and Release
|
||||||
# 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/*
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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 {
|
android {
|
||||||
namespace = "GoonXposed.xposed"
|
namespace = "ShiggyXposed.xposed"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "GoonXposed.xposed"
|
applicationId = "ShiggyXposed.xposed"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1210
|
versionCode = 1220
|
||||||
versionName = "1.2.1"
|
versionName = "1.2.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
@ -28,33 +28,24 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions { jvmTarget = "17" }
|
kotlinOptions {
|
||||||
buildFeatures { buildConfig = true }
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
google()
|
|
||||||
maven("https://jitpack.io")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("de.robv.android.xposed:api:82")
|
compileOnly("de.robv.android.xposed:api:82")
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
implementation("androidx.core:core-ktx:1.17.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
|
||||||
implementation("io.ktor:ktor-client-core:3.3.0")
|
implementation("io.ktor:ktor-client-core:3.3.1")
|
||||||
implementation("io.ktor:ktor-client-cio:3.3.0")
|
implementation("io.ktor:ktor-client-cio:3.3.1")
|
||||||
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("com.google.android.material:material:1.12.0")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<manifest
|
<manifest
|
||||||
package="GoonXposed.xposed"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
<meta-data android:name="xposedmodule" android:value="true" />
|
<meta-data android:name="xposedmodule" android:value="true" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="xposeddescription"
|
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="xposedminversion" android:value="82" />
|
||||||
<meta-data android:name="xposedscope" android:value="com.discord" />
|
<meta-data android:name="xposedscope" android:value="com.discord" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package GoonXposed.xposed
|
package ShiggyXposed.xposed
|
||||||
|
|
||||||
class Constants {
|
class Constants {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -9,10 +9,11 @@ class Constants {
|
||||||
const val CACHE_DIR = "cache/pyoncord"
|
const val CACHE_DIR = "cache/pyoncord"
|
||||||
const val MAIN_SCRIPT_FILE = "bundle.js"
|
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.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -7,15 +7,15 @@ import android.os.Bundle
|
||||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Utils.Log
|
import ShiggyXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.modules.*
|
import ShiggyXposed.xposed.modules.*
|
||||||
import GoonXposed.xposed.modules.appearance.FontsModule
|
import ShiggyXposed.xposed.modules.appearance.FontsModule
|
||||||
import GoonXposed.xposed.modules.appearance.SysColorsModule
|
import ShiggyXposed.xposed.modules.appearance.SysColorsModule
|
||||||
import GoonXposed.xposed.modules.appearance.ThemesModule
|
import ShiggyXposed.xposed.modules.appearance.ThemesModule
|
||||||
import GoonXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
|
import ShiggyXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule
|
||||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
import ShiggyXposed.xposed.modules.bridge.BridgeModule
|
||||||
import GoonXposed.xposed.modules.no_track.BlockCrashReportingModule
|
import ShiggyXposed.xposed.modules.no_track.BlockCrashReportingModule
|
||||||
import GoonXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
|
import ShiggyXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
|
||||||
object HookStateHolder {
|
object HookStateHolder {
|
||||||
|
|
@ -35,7 +35,7 @@ object HookStateHolder {
|
||||||
|
|
||||||
class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
private var hooked = false
|
private var hooked = false
|
||||||
private val modules = mutableListOf<Module>(
|
private val modules = mutableListOf(
|
||||||
HookScriptLoaderModule,
|
HookScriptLoaderModule,
|
||||||
BridgeModule,
|
BridgeModule,
|
||||||
AdditionalBridgeMethodsModule,
|
AdditionalBridgeMethodsModule,
|
||||||
|
|
@ -7,9 +7,9 @@ import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
import de.robv.android.xposed.XC_MethodHook
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
|
||||||
|
|
||||||
data class AppInfo(
|
data class AppInfo(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
@ -19,13 +19,16 @@ data class AppInfo(
|
||||||
)
|
)
|
||||||
|
|
||||||
abstract class Module {
|
abstract class Module {
|
||||||
/** Builds a JSON payload to be injected into the JavaScript context. */
|
/**
|
||||||
@Deprecated(
|
* Builds a JSON payload to be injected into the JavaScript context.
|
||||||
"This will be removed in future versions. Payloads can be replaced via synchronous bridge methods."
|
*/
|
||||||
)
|
@Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.")
|
||||||
open fun buildPayload(builder: JsonObjectBuilder) {}
|
open fun buildPayload(builder: JsonObjectBuilder) {
|
||||||
|
}
|
||||||
|
|
||||||
/** Called during Zygote initialization. */
|
/**
|
||||||
|
* Called during Zygote initialization.
|
||||||
|
*/
|
||||||
open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
|
open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -38,11 +41,10 @@ abstract class Module {
|
||||||
open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {}
|
open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called after [Context] has been attached to a [android.content.ContextWrapper] via
|
* Called after [Context] has been attached to a [android.content.ContextWrapper]
|
||||||
* [android.content.ContextWrapper.attachBaseContext].
|
* via [android.content.ContextWrapper.attachBaseContext].
|
||||||
*
|
*
|
||||||
* This may be called multiple times for different [Context]s, e.g. when the app is killed by
|
* This may be called multiple times for different [Context]s, e.g. when the app is killed by the system.
|
||||||
* the system.
|
|
||||||
*
|
*
|
||||||
* For bridge methods that require access to [Context], register them here.
|
* For bridge methods that require access to [Context], register them here.
|
||||||
*/
|
*/
|
||||||
|
|
@ -55,18 +57,15 @@ abstract class Module {
|
||||||
*
|
*
|
||||||
* Bridge methods must not be registered here, as it can be too late and cause crashes.
|
* 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
|
* If you need to register bridge methods that require an [Activity], do it in [onContext] and make it async.
|
||||||
* make it async.
|
|
||||||
*
|
*
|
||||||
* Once an [Activity] is available, execute the queued calls. Then from JS, call the method
|
* Once an [Activity] is available, execute the queued calls. Then from JS, call the method asynchronously.
|
||||||
* asynchronously.
|
|
||||||
*/
|
*/
|
||||||
open fun onActivity(activity: Activity) {}
|
open fun onActivity(activity: Activity) {}
|
||||||
|
|
||||||
protected fun Context.getAppInfo(): AppInfo {
|
protected fun Context.getAppInfo(): AppInfo {
|
||||||
val pInfo = packageManager.getPackageInfo(packageName, 0)
|
val pInfo = packageManager.getPackageInfo(packageName, 0)
|
||||||
val versionCode =
|
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pInfo.longVersionCode
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pInfo.longVersionCode
|
|
||||||
else @Suppress("DEPRECATION") pInfo.versionCode.toLong()
|
else @Suppress("DEPRECATION") pInfo.versionCode.toLong()
|
||||||
|
|
||||||
return AppInfo(
|
return AppInfo(
|
||||||
|
|
@ -86,22 +85,21 @@ abstract class Module {
|
||||||
if (!this.isFile) this.deleteRecursively()
|
if (!this.isFile) this.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun Class<*>.method(name: String, vararg params: Class<*>?): Method =
|
protected fun Class<*>.method(
|
||||||
getDeclaredMethod(name, *params).apply { isAccessible = true }
|
name: String, vararg params: Class<*>?
|
||||||
|
): Method = getDeclaredMethod(name, *params).apply {
|
||||||
|
isAccessible = true
|
||||||
|
}
|
||||||
|
|
||||||
protected fun ClassLoader.safeLoadClass(name: String): Class<*>? =
|
protected fun ClassLoader.safeLoadClass(name: String): Class<*>? = runCatching { loadClass(name) }.getOrNull()
|
||||||
runCatching { loadClass(name) }.getOrNull()
|
|
||||||
|
|
||||||
protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook =
|
protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = XposedBridge.hookMethod(this, hook)
|
||||||
XposedBridge.hookMethod(this, hook)
|
|
||||||
|
|
||||||
protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook =
|
protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook =
|
||||||
hook(MethodHookBuilder().apply(block).build())
|
hook(MethodHookBuilder().apply(block).build())
|
||||||
|
|
||||||
protected fun Class<*>.hookMethod(
|
protected fun Class<*>.hookMethod(
|
||||||
name: String,
|
name: String, vararg params: Class<*>?, block: MethodHookBuilder.() -> Unit
|
||||||
vararg params: Class<*>?,
|
|
||||||
block: MethodHookBuilder.() -> Unit
|
|
||||||
): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build())
|
): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build())
|
||||||
|
|
||||||
protected class MethodHookBuilder {
|
protected class MethodHookBuilder {
|
||||||
|
|
@ -116,16 +114,14 @@ abstract class Module {
|
||||||
afterBlock = block
|
afterBlock = block
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(): XC_MethodHook =
|
fun build(): XC_MethodHook = object : XC_MethodHook() {
|
||||||
object : XC_MethodHook() {
|
|
||||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||||
val b = beforeBlock
|
val b = beforeBlock
|
||||||
if (b != null) {
|
if (b != null) {
|
||||||
val scope =
|
val scope = HookScope(
|
||||||
HookScope(
|
param = param, proceed = { p ->
|
||||||
param = param,
|
super.beforeHookedMethod(p)
|
||||||
proceed = { p -> super.beforeHookedMethod(p) }
|
})
|
||||||
)
|
|
||||||
scope.b()
|
scope.b()
|
||||||
} else {
|
} else {
|
||||||
super.beforeHookedMethod(param)
|
super.beforeHookedMethod(param)
|
||||||
|
|
@ -135,11 +131,10 @@ abstract class Module {
|
||||||
override fun afterHookedMethod(param: MethodHookParam) {
|
override fun afterHookedMethod(param: MethodHookParam) {
|
||||||
val a = afterBlock
|
val a = afterBlock
|
||||||
if (a != null) {
|
if (a != null) {
|
||||||
val scope =
|
val scope = HookScope(
|
||||||
HookScope(
|
param = param, proceed = { p ->
|
||||||
param = param,
|
super.afterHookedMethod(p)
|
||||||
proceed = { p -> super.afterHookedMethod(p) }
|
})
|
||||||
)
|
|
||||||
scope.a()
|
scope.a()
|
||||||
} else {
|
} else {
|
||||||
super.afterHookedMethod(param)
|
super.afterHookedMethod(param)
|
||||||
|
|
@ -159,23 +154,19 @@ abstract class Module {
|
||||||
* @property param The [XC_MethodHook.MethodHookParam] for the current hook.
|
* @property param The [XC_MethodHook.MethodHookParam] for the current hook.
|
||||||
* @property proceed Function that calls the super method.
|
* @property proceed Function that calls the super method.
|
||||||
*/
|
*/
|
||||||
protected class HookScope
|
protected class HookScope internal constructor(
|
||||||
internal constructor(
|
val param: XC_MethodHook.MethodHookParam, private val proceed: (XC_MethodHook.MethodHookParam) -> Unit
|
||||||
val param: XC_MethodHook.MethodHookParam,
|
|
||||||
private val proceed: (XC_MethodHook.MethodHookParam) -> Unit
|
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Continues with the default XC_MethodHook super behavior. Equivalent to calling
|
* Continues with the default XC_MethodHook super behavior.
|
||||||
* `super.beforeHookedMethod(param)` or `super.afterHookedMethod(param)` depending on the
|
* Equivalent to calling `super.beforeHookedMethod(param)` or
|
||||||
* phase.
|
* `super.afterHookedMethod(param)` depending on the phase.
|
||||||
*/
|
*/
|
||||||
fun proceed() = proceed(param)
|
fun proceed() = proceed(param)
|
||||||
|
|
||||||
val thisObject: Any?
|
val thisObject: Any? get() = param.thisObject
|
||||||
get() = param.thisObject
|
|
||||||
|
|
||||||
val args: Array<Any?>
|
val args: Array<Any?> get() = param.args
|
||||||
get() = param.args
|
|
||||||
|
|
||||||
var result: Any?
|
var result: Any?
|
||||||
get() = param.result
|
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.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import GoonXposed.xposed.Module
|
import ShiggyXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import ShiggyXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
import ShiggyXposed.xposed.modules.bridge.BridgeModule
|
||||||
import java.io.*
|
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.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import GoonXposed.xposed.Module
|
import ShiggyXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import ShiggyXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
import ShiggyXposed.xposed.modules.bridge.BridgeModule
|
||||||
import java.io.*
|
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.content.res.AssetManager
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
|
|
@ -9,10 +9,10 @@ import android.os.Build
|
||||||
import de.robv.android.xposed.XC_MethodReplacement
|
import de.robv.android.xposed.XC_MethodReplacement
|
||||||
import de.robv.android.xposed.XposedHelpers
|
import de.robv.android.xposed.XposedHelpers
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Constants
|
import ShiggyXposed.xposed.Constants
|
||||||
import GoonXposed.xposed.Module
|
import ShiggyXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Companion.JSON
|
import ShiggyXposed.xposed.Utils.Companion.JSON
|
||||||
import GoonXposed.xposed.Utils.Log
|
import ShiggyXposed.xposed.Utils.Log
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.cio.*
|
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.R.color
|
||||||
import android.app.AndroidAppHelper
|
import android.app.AndroidAppHelper
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import GoonXposed.xposed.Module
|
import ShiggyXposed.xposed.Module
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import java.lang.ref.WeakReference
|
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.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Constants
|
import ShiggyXposed.xposed.Constants
|
||||||
import GoonXposed.xposed.Module
|
import ShiggyXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Companion.JSON
|
import ShiggyXposed.xposed.Utils.Companion.JSON
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
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.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
|
|
@ -7,8 +7,8 @@ import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import GoonXposed.xposed.Module
|
import ShiggyXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils
|
import ShiggyXposed.xposed.Utils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object AdditionalBridgeMethodsModule : Module() {
|
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 de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.BuildConfig
|
import ShiggyXposed.xposed.BuildConfig
|
||||||
import GoonXposed.xposed.Constants
|
import ShiggyXposed.xposed.Constants
|
||||||
import GoonXposed.xposed.Module
|
import ShiggyXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import ShiggyXposed.xposed.Utils.Log
|
||||||
import java.lang.reflect.Method
|
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 android.content.Context
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Module
|
import ShiggyXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import ShiggyXposed.xposed.Utils.Log
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.
|
* 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")
|
include(":app")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue