refactor: module startup loading logic
EXPERIMENTAL: WIP: use unified entry point
This commit is contained in:
@@ -251,12 +251,20 @@ kotlin {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// loader
|
||||
compileOnly(projects.loader.hookapi)
|
||||
runtimeOnly(projects.loader.sbl)
|
||||
implementation(projects.loader.startup)
|
||||
// TODO: 2024-07-21 remove libs.xposed.api once refactor done
|
||||
compileOnly(libs.xposed.api)
|
||||
// ksp
|
||||
ksp(projects.libs.ksp)
|
||||
// host stub
|
||||
compileOnly(projects.libs.stub)
|
||||
// libraries
|
||||
implementation(projects.libs.mmkv)
|
||||
implementation(projects.libs.dexkit)
|
||||
implementation(projects.libs.xView)
|
||||
ksp(projects.libs.ksp)
|
||||
compileOnly(libs.xposed)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.browser)
|
||||
|
||||
@@ -1 +1 @@
|
||||
io.github.qauxv.startup.HookEntry
|
||||
io.github.qauxv.loader.sbl.xp51.Xp51HookEntry
|
||||
|
||||
@@ -56,7 +56,7 @@ import io.github.qauxv.fragment.AboutFragment;
|
||||
import io.github.qauxv.fragment.CheckAbiVariantFragment;
|
||||
import io.github.qauxv.fragment.CheckAbiVariantModel;
|
||||
import io.github.qauxv.lifecycle.JumpActivityEntryHook;
|
||||
import io.github.qauxv.startup.HookEntry;
|
||||
import io.github.qauxv.util.PackageConstants;
|
||||
import io.github.qauxv.util.SyncUtils;
|
||||
import io.github.qauxv.util.Toasts;
|
||||
import io.github.qauxv.util.UiThread;
|
||||
@@ -165,11 +165,11 @@ public class ConfigV2Activity extends AppCompatTransferActivity {
|
||||
String pkg = null;
|
||||
var id = view.getId();
|
||||
if (id == R.id.mainRelativeLayoutButtonOpenQQ) {
|
||||
pkg = HookEntry.PACKAGE_NAME_QQ;
|
||||
pkg = PackageConstants.PACKAGE_NAME_QQ;
|
||||
} else if (id == R.id.mainRelativeLayoutButtonOpenTIM) {
|
||||
pkg = HookEntry.PACKAGE_NAME_TIM;
|
||||
pkg = PackageConstants.PACKAGE_NAME_TIM;
|
||||
} else if (id == R.id.mainRelativeLayoutButtonOpenQQLite) {
|
||||
pkg = HookEntry.PACKAGE_NAME_QQ_LITE;
|
||||
pkg = PackageConstants.PACKAGE_NAME_QQ_LITE;
|
||||
}
|
||||
if (pkg != null) {
|
||||
Intent intent = new Intent();
|
||||
@@ -205,19 +205,19 @@ public class ConfigV2Activity extends AppCompatTransferActivity {
|
||||
String pkg = null;
|
||||
switch (which) {
|
||||
case 0: {
|
||||
pkg = HookEntry.PACKAGE_NAME_QQ;
|
||||
pkg = PackageConstants.PACKAGE_NAME_QQ;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
pkg = HookEntry.PACKAGE_NAME_TIM;
|
||||
pkg = PackageConstants.PACKAGE_NAME_TIM;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pkg = HookEntry.PACKAGE_NAME_QQ_LITE;
|
||||
pkg = PackageConstants.PACKAGE_NAME_QQ_LITE;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pkg = HookEntry.PACKAGE_NAME_QQ_HD;
|
||||
pkg = PackageConstants.PACKAGE_NAME_QQ_HD;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
package io.github.qauxv.config;
|
||||
|
||||
import android.os.Environment;
|
||||
import io.github.qauxv.startup.HookEntry;
|
||||
import io.github.qauxv.util.PackageConstants;
|
||||
import io.github.qauxv.util.HostInfo;
|
||||
import io.github.qauxv.util.Log;
|
||||
import java.io.File;
|
||||
@@ -44,7 +44,7 @@ public class SafeModeManager {
|
||||
}
|
||||
INSTANCE.mSafeModeEnableFile = new File(
|
||||
Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" +
|
||||
HookEntry.sCurrentPackageName + "/" + SAFE_MODE_FILE_NAME
|
||||
HostInfo.getHostInfo().getPackageName() + "/" + SAFE_MODE_FILE_NAME
|
||||
);
|
||||
return INSTANCE;
|
||||
}
|
||||
@@ -73,10 +73,6 @@ public class SafeModeManager {
|
||||
if (!isAvailable()) {
|
||||
return false;
|
||||
}
|
||||
if (HookEntry.sCurrentPackageName == null || HookEntry.sCurrentPackageName.isBlank()) {
|
||||
Log.e("Failed to enable or disable safe mode, sCurrentPackageName is null or blank");
|
||||
return false;
|
||||
}
|
||||
if (isEnable) {
|
||||
try {
|
||||
boolean isCreated = mSafeModeEnableFile.createNewFile();
|
||||
|
||||
@@ -72,7 +72,7 @@ import io.github.qauxv.dsl.item.CategoryItem
|
||||
import io.github.qauxv.dsl.item.DslTMsgListItemInflatable
|
||||
import io.github.qauxv.dsl.item.TextSwitchItem
|
||||
import io.github.qauxv.lifecycle.ActProxyMgr
|
||||
import io.github.qauxv.startup.HookEntry
|
||||
import io.github.qauxv.poststartup.StartupInfo
|
||||
import io.github.qauxv.startup.HybridClassLoader
|
||||
import io.github.qauxv.tlb.ConfigTable.cacheMap
|
||||
import io.github.qauxv.ui.CustomDialog
|
||||
@@ -176,8 +176,7 @@ class TroubleshootFragment : BaseRootLayoutFragment() {
|
||||
", UID: " + android.os.Process.myUid() +
|
||||
", " + (if (android.os.Process.is64Bit()) "64 bit" else "32 bit") + "\n" +
|
||||
"Xposed API version: " + XposedBridge.getXposedVersion() + "\n" +
|
||||
HybridClassLoader.getXposedBridgeClassName() + "\n" +
|
||||
"module: " + HookEntry.getModulePath() + "\n" +
|
||||
"module: " + StartupInfo.getModulePath() + "\n" +
|
||||
"ctx.dataDir: " + hostInfo.application.dataDir
|
||||
description(statusInfo, isTextSelectable = true)
|
||||
description(generateDebugInfo(), isTextSelectable = true)
|
||||
|
||||
@@ -53,7 +53,8 @@ import androidx.annotation.RequiresApi;
|
||||
import cc.ioctl.util.HostInfo;
|
||||
import io.github.qauxv.R;
|
||||
import io.github.qauxv.core.MainHook;
|
||||
import io.github.qauxv.startup.HookEntry;
|
||||
import io.github.qauxv.poststartup.StartupInfo;
|
||||
import io.github.qauxv.util.PackageConstants;
|
||||
import io.github.qauxv.ui.WindowIsTranslucent;
|
||||
import io.github.qauxv.util.Initiator;
|
||||
import io.github.qauxv.util.Log;
|
||||
@@ -113,7 +114,7 @@ public class Parasitics {
|
||||
return;
|
||||
} catch (Resources.NotFoundException ignored) {
|
||||
}
|
||||
String sModulePath = HookEntry.getModulePath();
|
||||
String sModulePath = StartupInfo.getModulePath();
|
||||
if (sModulePath == null) {
|
||||
throw new RuntimeException("get module path failed, loader=" + MainHook.class.getClassLoader());
|
||||
}
|
||||
|
||||
152
app/src/main/java/io/github/qauxv/poststartup/StartupAgent.java
Normal file
152
app/src/main/java/io/github/qauxv/poststartup/StartupAgent.java
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2024 QAuxiliary developers
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is an opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the General Public License
|
||||
* along with this software.
|
||||
* If not, see
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
|
||||
package io.github.qauxv.poststartup;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import io.github.qauxv.loader.hookapi.IHookBridge;
|
||||
import io.github.qauxv.loader.hookapi.ILoaderInfo;
|
||||
import io.github.qauxv.util.IoUtils;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass;
|
||||
|
||||
@Keep
|
||||
public class StartupAgent {
|
||||
|
||||
private static boolean sInitialized = false;
|
||||
|
||||
private StartupAgent() {
|
||||
throw new AssertionError("No instance for you!");
|
||||
}
|
||||
|
||||
@Keep
|
||||
public static void startup(
|
||||
@NonNull String modulePath,
|
||||
@NonNull ApplicationInfo appInfo,
|
||||
@NonNull ILoaderInfo loaderInfo,
|
||||
@NonNull ClassLoader hostClassLoader,
|
||||
@Nullable IHookBridge hookBridge
|
||||
) {
|
||||
if (sInitialized) {
|
||||
return;
|
||||
}
|
||||
sInitialized = true;
|
||||
if (io.github.qauxv.R.string.res_inject_success >>> 24 == 0x7f) {
|
||||
throw new AssertionError("package id must NOT be 0x7f, reject loading...");
|
||||
}
|
||||
if ("true".equals(System.getProperty(StartupAgent.class.getName()))) {
|
||||
android.util.Log.e("QAuxv", "Error: QAuxiliary reloaded??");
|
||||
// I don't know... What happened?
|
||||
return;
|
||||
}
|
||||
StartupInfo.modulePath = modulePath;
|
||||
StartupInfo.loaderInfo = loaderInfo;
|
||||
StartupInfo.hookBridge = hookBridge;
|
||||
// bypass hidden api
|
||||
ensureHiddenApiAccess();
|
||||
// we want context
|
||||
Application baseApp = getApplicationByActivityThread();
|
||||
if (baseApp == null) {
|
||||
if (hookBridge == null) {
|
||||
throw new UnsupportedOperationException("neither base application nor hook bridge found");
|
||||
}
|
||||
StartupHook.getInstance().initializeBeforeAppCreate(hostClassLoader);
|
||||
} else {
|
||||
Context ctx = getBaseApplicationImpl(hostClassLoader);
|
||||
if (ctx == null) {
|
||||
throw new AssertionError("getBaseApplicationImpl() == null but getApplicationByActivityThread() != null");
|
||||
}
|
||||
StartupHook.getInstance().initializeAfterAppCreate(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
public static Context getBaseApplicationImpl(@NonNull ClassLoader classLoader) {
|
||||
Context app;
|
||||
try {
|
||||
Class<?> clz = classLoader.loadClass("com.tencent.common.app.BaseApplicationImpl");
|
||||
Field fsApp = null;
|
||||
for (Field f : clz.getDeclaredFields()) {
|
||||
if (f.getType() == clz) {
|
||||
fsApp = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fsApp == null) {
|
||||
throw new UnsupportedOperationException("field BaseApplicationImpl.sApplication not found");
|
||||
}
|
||||
app = (Context) fsApp.get(null);
|
||||
return app;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
android.util.Log.e("QAuxv", "getBaseApplicationImpl: failed", e);
|
||||
throw IoUtils.unsafeThrow(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Application getApplicationByActivityThread() {
|
||||
try {
|
||||
Class<?> kActivityThread = Class.forName("android.app.ActivityThread");
|
||||
Method mGetApplication = kActivityThread.getDeclaredMethod("currentApplication");
|
||||
return (Application) mGetApplication.invoke(null);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
android.util.Log.e("QAuxv", "getApplicationByActivityThread: failed", e);
|
||||
throw IoUtils.unsafeThrow(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureHiddenApiAccess() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !isHiddenApiAccessible()) {
|
||||
android.util.Log.w("QAuxv", "Hidden API access not accessible, SDK_INT is " + Build.VERSION.SDK_INT);
|
||||
HiddenApiBypass.setHiddenApiExemptions("L");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint({"BlockedPrivateApi", "PrivateApi"})
|
||||
public static boolean isHiddenApiAccessible() {
|
||||
Class<?> kContextImpl;
|
||||
try {
|
||||
kContextImpl = Class.forName("android.app.ContextImpl");
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
Field mActivityToken = null;
|
||||
Field mToken = null;
|
||||
try {
|
||||
mActivityToken = kContextImpl.getDeclaredField("mActivityToken");
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
}
|
||||
try {
|
||||
mToken = kContextImpl.getDeclaredField("mToken");
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
}
|
||||
return mActivityToken != null || mToken != null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +1,36 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2022 qwq233@qwq2333.top
|
||||
* Copyright (C) 2019-2024 QAuxiliary developers
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is non-free but opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Affero General Public License
|
||||
* This software is an opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version and our eula as published
|
||||
* version 3 of the License, or any later version as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Affero General Public License for more details.
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* and eula along with this software. If not, see
|
||||
* <https://www.gnu.org/licenses/>
|
||||
* You should have received a copy of the General Public License
|
||||
* along with this software.
|
||||
* If not, see
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
package io.github.qauxv.startup;
|
||||
package io.github.qauxv.poststartup;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
import androidx.annotation.NonNull;
|
||||
import io.github.qauxv.startup.HybridClassLoader;
|
||||
import io.github.qauxv.util.IoUtils;
|
||||
import io.github.qauxv.util.xpcompat.XC_MethodHook;
|
||||
import io.github.qauxv.util.xpcompat.XposedBridge;
|
||||
import io.github.qauxv.util.xpcompat.XposedHelpers;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -38,9 +40,11 @@ import java.lang.reflect.Modifier;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Startup hook for QQ/TIM They should act differently according to the process they belong to. I don't want to cope
|
||||
* with them any more, enjoy it as long as possible. DO NOT INVOKE ANY METHOD THAT MAY GET IN TOUCH WITH KOTLIN HERE. DO
|
||||
* NOT MODIFY ANY CODE HERE UNLESS NECESSARY.
|
||||
* Startup hook for QQ/TIM They should act differently according to the process they belong to.
|
||||
* <p>
|
||||
* I don't want to cope with them anymore, enjoy it as long as possible.
|
||||
* <p>
|
||||
* DO NOT MODIFY ANY CODE HERE UNLESS NECESSARY.
|
||||
*
|
||||
* @author cinit
|
||||
*/
|
||||
@@ -48,7 +52,6 @@ public class StartupHook {
|
||||
|
||||
private static StartupHook sInstance;
|
||||
private static boolean sSecondStageInit = false;
|
||||
private boolean mFirstStageInit = false;
|
||||
|
||||
private StartupHook() {
|
||||
}
|
||||
@@ -65,44 +68,12 @@ public class StartupHook {
|
||||
if (sSecondStageInit) {
|
||||
return;
|
||||
}
|
||||
ClassLoader classLoader = ctx.getClassLoader();
|
||||
if (classLoader == null) {
|
||||
throw new AssertionError("ERROR: classLoader == null");
|
||||
}
|
||||
if ("true".equals(System.getProperty(StartupHook.class.getName()))) {
|
||||
XposedBridge.log("Err:QAuxiliary reloaded??");
|
||||
//I don't know... What happened?
|
||||
return;
|
||||
}
|
||||
System.setProperty(StartupHook.class.getName(), "true");
|
||||
injectClassLoader(classLoader);
|
||||
HybridClassLoader.setHostClassLoader(ctx.getClassLoader());
|
||||
StartupRoutine.execPostStartupInit(ctx, step, lpwReserved, bReserved);
|
||||
sSecondStageInit = true;
|
||||
deleteDirIfNecessaryNoThrow(ctx);
|
||||
}
|
||||
|
||||
@SuppressWarnings("JavaReflectionMemberAccess")
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
private static void injectClassLoader(ClassLoader classLoader) {
|
||||
if (classLoader == null) {
|
||||
throw new NullPointerException("classLoader == null");
|
||||
}
|
||||
try {
|
||||
Field fParent = ClassLoader.class.getDeclaredField("parent");
|
||||
fParent.setAccessible(true);
|
||||
ClassLoader mine = StartupHook.class.getClassLoader();
|
||||
ClassLoader curr = (ClassLoader) fParent.get(mine);
|
||||
if (curr == null) {
|
||||
curr = XposedBridge.class.getClassLoader();
|
||||
}
|
||||
if (!curr.getClass().getName().equals(HybridClassLoader.class.getName())) {
|
||||
fParent.set(mine, new HybridClassLoader(curr, classLoader));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log_e(e);
|
||||
}
|
||||
}
|
||||
|
||||
static void deleteDirIfNecessaryNoThrow(Context ctx) {
|
||||
try {
|
||||
deleteFile(new File(ctx.getDataDir(), "app_qqprotect"));
|
||||
@@ -147,58 +118,27 @@ public class StartupHook {
|
||||
String msg = Log.getStackTraceString(th);
|
||||
Log.e("QAuxv", msg);
|
||||
try {
|
||||
XposedBridge.log(th);
|
||||
} catch (NoClassDefFoundError e) {
|
||||
StartupInfo.getLoaderInfo().log(th);
|
||||
} catch (NoClassDefFoundError | NullPointerException e) {
|
||||
Log.e("Xposed", msg);
|
||||
Log.e("EdXposed-Bridge", msg);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkClassLoaderIsolation() {
|
||||
Class<?> stub;
|
||||
try {
|
||||
stub = Class.forName("com.tencent.common.app.BaseApplicationImpl");
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.d("QAuxv", "checkClassLoaderIsolation success");
|
||||
return;
|
||||
}
|
||||
Log.e("QAuxv", "checkClassLoaderIsolation failure!");
|
||||
Log.e("QAuxv", "HostApp: " + stub.getClassLoader());
|
||||
Log.e("QAuxv", "Module: " + StartupHook.class.getClassLoader());
|
||||
Log.e("QAuxv", "Module.parent: " + StartupHook.class.getClassLoader().getParent());
|
||||
Log.e("QAuxv", "XposedBridge: " + XposedBridge.class.getClassLoader());
|
||||
Log.e("QAuxv", "SystemClassLoader: " + ClassLoader.getSystemClassLoader());
|
||||
Log.e("QAuxv", "Context.class: " + Context.class.getClassLoader());
|
||||
public void initializeAfterAppCreate(@NonNull Context ctx) {
|
||||
execStartupInit(ctx, null, null, false);
|
||||
applyTargetDpiIfNecessary(ctx);
|
||||
deleteDirIfNecessaryNoThrow(ctx);
|
||||
}
|
||||
|
||||
public void initialize(ClassLoader rtLoader) throws Throwable {
|
||||
if (mFirstStageInit) {
|
||||
return;
|
||||
}
|
||||
public void initializeBeforeAppCreate(@NonNull ClassLoader rtLoader) {
|
||||
try {
|
||||
XC_MethodHook startup = new XC_MethodHook(51) {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
|
||||
try {
|
||||
Context app;
|
||||
Class<?> clz = param.thisObject.getClass().getClassLoader()
|
||||
.loadClass("com.tencent.common.app.BaseApplicationImpl");
|
||||
Field fsApp = null;
|
||||
for (Field f : clz.getDeclaredFields()) {
|
||||
if (f.getType() == clz) {
|
||||
fsApp = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fsApp == null) {
|
||||
throw new NoSuchFieldException("field BaseApplicationImpl.sApplication not found");
|
||||
}
|
||||
app = (Context) fsApp.get(null);
|
||||
ClassLoader cl = param.thisObject.getClass().getClassLoader();
|
||||
Context app = StartupAgent.getBaseApplicationImpl(cl);
|
||||
execStartupInit(app, param.thisObject, null, false);
|
||||
} catch (Throwable e) {
|
||||
log_e(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
Class<?> loadDex = findLoadDexTaskClass(rtLoader);
|
||||
@@ -218,7 +158,6 @@ public class StartupHook {
|
||||
}
|
||||
}
|
||||
XposedBridge.hookMethod(m, startup);
|
||||
mFirstStageInit = true;
|
||||
} catch (Throwable e) {
|
||||
if ((e + "").contains("com.bug.zqq")) {
|
||||
return;
|
||||
@@ -227,7 +166,7 @@ public class StartupHook {
|
||||
return;
|
||||
}
|
||||
log_e(e);
|
||||
throw e;
|
||||
throw IoUtils.unsafeThrow(e);
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(rtLoader.loadClass("com.tencent.mobileqq.qfix.QFixApplication"),
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2024 QAuxiliary developers
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is an opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the General Public License
|
||||
* along with this software.
|
||||
* If not, see
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
|
||||
package io.github.qauxv.poststartup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import io.github.qauxv.loader.hookapi.IHookBridge;
|
||||
import io.github.qauxv.loader.hookapi.ILoaderInfo;
|
||||
|
||||
public class StartupInfo {
|
||||
|
||||
private StartupInfo() {
|
||||
throw new AssertionError("No instance for you!");
|
||||
}
|
||||
|
||||
/* package */ static String modulePath;
|
||||
|
||||
/* package */ static ILoaderInfo loaderInfo;
|
||||
|
||||
/* package */ static IHookBridge hookBridge;
|
||||
|
||||
@NonNull
|
||||
public static String getModulePath() {
|
||||
return modulePath;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ILoaderInfo getLoaderInfo() {
|
||||
return loaderInfo;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static IHookBridge getHookBridge() {
|
||||
return hookBridge;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static IHookBridge requireHookBridge() {
|
||||
if (hookBridge == null) {
|
||||
throw new IllegalStateException("HookBridge is not initialized");
|
||||
}
|
||||
return hookBridge;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2022 qwq233@qwq2333.top
|
||||
* Copyright (C) 2019-2024 QAuxiliary developers
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is non-free but opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Affero General Public License
|
||||
* This software is an opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version and our eula as published
|
||||
* version 3 of the License, or any later version as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Affero General Public License for more details.
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* and eula along with this software. If not, see
|
||||
* <https://www.gnu.org/licenses/>
|
||||
* You should have received a copy of the General Public License
|
||||
* along with this software.
|
||||
* If not, see
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
package io.github.qauxv.startup;
|
||||
package io.github.qauxv.poststartup;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
@@ -32,7 +31,6 @@ import io.github.qauxv.util.HostInfo;
|
||||
import io.github.qauxv.util.Initiator;
|
||||
import io.github.qauxv.util.Natives;
|
||||
import java.lang.reflect.Field;
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass;
|
||||
|
||||
public class StartupRoutine {
|
||||
|
||||
@@ -51,10 +49,9 @@ public class StartupRoutine {
|
||||
* @param bReserved false, not used
|
||||
*/
|
||||
public static void execPostStartupInit(Context ctx, Object step, String lpwReserved, boolean bReserved) {
|
||||
ensureHiddenApiAccess();
|
||||
// init all kotlin utils here
|
||||
EzXHelperInit.INSTANCE.initZygote(HookEntry.getInitZygoteStartupParam());
|
||||
EzXHelperInit.INSTANCE.initHandleLoadPackage(HookEntry.getLoadPackageParam());
|
||||
EzXHelperInit.INSTANCE.setHostPackageName(ctx.getPackageName());
|
||||
EzXHelperInit.INSTANCE.setEzClassLoader(ctx.getClassLoader());
|
||||
// resource injection is done somewhere else, do not init it here
|
||||
EzXHelperInit.INSTANCE.initAppContext(ctx, false, false);
|
||||
EzXHelperInit.INSTANCE.setLogTag("QAuxv");
|
||||
@@ -92,31 +89,4 @@ public class StartupRoutine {
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureHiddenApiAccess() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !isHiddenApiAccessible()) {
|
||||
android.util.Log.w("QAuxv", "Hidden API access not accessible, SDK_INT is " + Build.VERSION.SDK_INT);
|
||||
HiddenApiBypass.setHiddenApiExemptions("L");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint({"BlockedPrivateApi", "PrivateApi"})
|
||||
public static boolean isHiddenApiAccessible() {
|
||||
Class<?> kContextImpl;
|
||||
try {
|
||||
kContextImpl = Class.forName("android.app.ContextImpl");
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
Field mActivityToken = null;
|
||||
Field mToken = null;
|
||||
try {
|
||||
mActivityToken = kContextImpl.getDeclaredField("mActivityToken");
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
}
|
||||
try {
|
||||
mToken = kContextImpl.getDeclaredField("mToken");
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
}
|
||||
return mActivityToken != null || mToken != null;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2022 qwq233@qwq2333.top
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is non-free but opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Affero General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version and our eula as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* and eula along with this software. If not, see
|
||||
* <https://www.gnu.org/licenses/>
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
package io.github.qauxv.startup;
|
||||
|
||||
import android.content.Context;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* NOTICE: Do NOT use any androidx annotations here.
|
||||
*/
|
||||
public class HybridClassLoader extends ClassLoader {
|
||||
|
||||
private static String sObfuscatedPackageName = null;
|
||||
private static String sProbeLsposedNativeApiClassName = "Lorg/lsposed/lspd/nativebridge/NativeAPI;";
|
||||
private static final ClassLoader sBootClassLoader = Context.class.getClassLoader();
|
||||
private final ClassLoader clPreload;
|
||||
private final ClassLoader clBase;
|
||||
|
||||
public HybridClassLoader(ClassLoader x, ClassLoader ctx) {
|
||||
clPreload = x;
|
||||
clBase = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把宿主和模块共有的 package 扔这里.
|
||||
*
|
||||
* @param name NonNull, class name
|
||||
* @return true if conflicting
|
||||
*/
|
||||
public static boolean isConflictingClass(String name) {
|
||||
return name.startsWith("androidx.") || name.startsWith("android.support.")
|
||||
|| name.startsWith("kotlin.") || name.startsWith("kotlinx.")
|
||||
|| name.startsWith("com.tencent.mmkv.")
|
||||
|| name.startsWith("com.android.tools.r8.")
|
||||
|| name.startsWith("com.google.android.")
|
||||
|| name.startsWith("com.google.gson.")
|
||||
|| name.startsWith("com.google.common.")
|
||||
|| name.startsWith("com.google.protobuf.")
|
||||
|| name.startsWith("com.microsoft.appcenter.")
|
||||
|| name.startsWith("org.intellij.lang.annotations.")
|
||||
|| name.startsWith("org.jetbrains.annotations.")
|
||||
|| name.startsWith("com.bumptech.glide.")
|
||||
|| name.startsWith("com.google.errorprone.annotations.")
|
||||
|| name.startsWith("_COROUTINE.");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
try {
|
||||
return sBootClassLoader.loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
if (name != null && isConflictingClass(name)) {
|
||||
//Nevertheless, this will not interfere with the host application,
|
||||
//classes in host application SHOULD find with their own ClassLoader, eg Class.forName()
|
||||
//use shipped androidx and kotlin lib.
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
// The ClassLoader for some apk-modifying frameworks are terrible, XposedBridge.class.getClassLoader()
|
||||
// is the sane as Context.getClassLoader(), which mess up with 3rd lib, can cause the ART to crash.
|
||||
if (clPreload != null) {
|
||||
try {
|
||||
return clPreload.loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
if (clBase != null) {
|
||||
try {
|
||||
return clBase.loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getResource(String name) {
|
||||
URL ret = clPreload.getResource(name);
|
||||
if (ret != null) {
|
||||
return ret;
|
||||
}
|
||||
return clBase.getResource(name);
|
||||
}
|
||||
|
||||
public static void setObfuscatedXposedApiPackage(String packageName) {
|
||||
sObfuscatedPackageName = packageName;
|
||||
}
|
||||
|
||||
public static String getObfuscatedXposedApiPackage() {
|
||||
return sObfuscatedPackageName;
|
||||
}
|
||||
|
||||
public static String getObfuscatedLsposedNativeApiClassName() {
|
||||
return sProbeLsposedNativeApiClassName.replace('.', '/').substring(1, sProbeLsposedNativeApiClassName.length() - 1);
|
||||
}
|
||||
|
||||
public static String getXposedBridgeClassName() {
|
||||
if (sObfuscatedPackageName == null) {
|
||||
return "de.robv.android.xposed.XposedBridge";
|
||||
} else {
|
||||
var sb = new StringBuilder(sObfuscatedPackageName);
|
||||
sb.append(".XposedBridge");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2024 QAuxiliary developers
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is an opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the General Public License
|
||||
* along with this software.
|
||||
* If not, see
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
package io.github.qauxv.util;
|
||||
|
||||
public class LspObfuscationHelper {
|
||||
|
||||
private static String sObfuscatedPackageName = null;
|
||||
private static String sProbeLsposedNativeApiClassName = "Lorg/lsposed/lspd/nativebridge/NativeAPI;";
|
||||
|
||||
public static void setObfuscatedXposedApiPackage(String packageName) {
|
||||
sObfuscatedPackageName = packageName;
|
||||
}
|
||||
|
||||
public static String getObfuscatedXposedApiPackage() {
|
||||
return sObfuscatedPackageName;
|
||||
}
|
||||
|
||||
public static String getObfuscatedLsposedNativeApiClassName() {
|
||||
return sProbeLsposedNativeApiClassName.replace('.', '/').substring(1, sProbeLsposedNativeApiClassName.length() - 1);
|
||||
}
|
||||
|
||||
public static String getXposedBridgeClassName() {
|
||||
if (sObfuscatedPackageName == null) {
|
||||
return "de.robv.android.xposed.XposedBridge";
|
||||
} else {
|
||||
var sb = new StringBuilder(sObfuscatedPackageName);
|
||||
sb.append(".XposedBridge");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,8 +30,7 @@ import android.system.Os;
|
||||
import android.system.StructUtsname;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
import io.github.qauxv.BuildConfig;
|
||||
import io.github.qauxv.startup.HookEntry;
|
||||
import io.github.qauxv.startup.HybridClassLoader;
|
||||
import io.github.qauxv.poststartup.StartupInfo;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOError;
|
||||
@@ -198,10 +197,10 @@ public class Natives {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Class<?> xp = Class.forName(HybridClassLoader.getXposedBridgeClassName());
|
||||
Class<?> xp = Class.forName(LspObfuscationHelper.getXposedBridgeClassName());
|
||||
try {
|
||||
xp.getClassLoader()
|
||||
.loadClass(HybridClassLoader.getObfuscatedLsposedNativeApiClassName())
|
||||
.loadClass(LspObfuscationHelper.getObfuscatedLsposedNativeApiClassName())
|
||||
.getMethod("recordNativeEntrypoint", String.class)
|
||||
.invoke(null, soTailingName);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
@@ -222,10 +221,10 @@ public class Natives {
|
||||
} catch (UnsatisfiedLinkError ignored) {
|
||||
}
|
||||
try {
|
||||
Class.forName(HybridClassLoader.getXposedBridgeClassName());
|
||||
Class.forName(LspObfuscationHelper.getXposedBridgeClassName());
|
||||
// in host process
|
||||
List<String> abis = getAbiForLibrary();
|
||||
String modulePath = HookEntry.getModulePath();
|
||||
String modulePath = StartupInfo.getModulePath();
|
||||
loadNativeLibraryInHost(ctx, modulePath, abis);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// not in host process, ignore
|
||||
|
||||
38
app/src/main/java/io/github/qauxv/util/PackageConstants.java
Normal file
38
app/src/main/java/io/github/qauxv/util/PackageConstants.java
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2024 QAuxiliary developers
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is an opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the General Public License
|
||||
* along with this software.
|
||||
* If not, see
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
|
||||
package io.github.qauxv.util;
|
||||
|
||||
public class PackageConstants {
|
||||
|
||||
private PackageConstants() {
|
||||
throw new AssertionError("No instance for you!");
|
||||
}
|
||||
|
||||
public static final String PACKAGE_NAME_QQ = "com.tencent.mobileqq";
|
||||
public static final String PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi";
|
||||
public static final String PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite";
|
||||
public static final String PACKAGE_NAME_QQ_HD = "com.tencent.minihd.qq";
|
||||
public static final String PACKAGE_NAME_TIM = "com.tencent.tim";
|
||||
public static final String PACKAGE_NAME_SELF = "io.github.qauxv";
|
||||
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import android.content.pm.PackageManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import cc.ioctl.util.HostInfo;
|
||||
import io.github.qauxv.startup.HookEntry;
|
||||
import io.github.qauxv.poststartup.StartupInfo;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -89,7 +89,7 @@ public class AbiUtils {
|
||||
}
|
||||
String apkPath;
|
||||
if (HostInfo.isInHostProcess()) {
|
||||
apkPath = HookEntry.getModulePath();
|
||||
apkPath = StartupInfo.getModulePath();
|
||||
} else {
|
||||
// self process
|
||||
apkPath = HostInfo.getApplication().getPackageCodePath();
|
||||
|
||||
@@ -22,17 +22,20 @@
|
||||
|
||||
package io.github.qauxv.util.hookstatus;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* Called in handleLoadPackage, NO KOTLIN, NO ANDROIDX
|
||||
**/
|
||||
@Keep
|
||||
public class HookStatusInit {
|
||||
|
||||
private HookStatusInit() {
|
||||
}
|
||||
|
||||
@Keep
|
||||
public static void init(ClassLoader classLoader) throws Throwable {
|
||||
Class<?> kHookStatusImpl = classLoader.loadClass("io.github.qauxv.util.hookstatus.HookStatusImpl");
|
||||
Field f = kHookStatusImpl.getDeclaredField("sZygoteHookMode");
|
||||
|
||||
272
app/src/main/java/io/github/qauxv/util/xpcompat/ArrayUtils.java
Normal file
272
app/src/main/java/io/github/qauxv/util/xpcompat/ArrayUtils.java
Normal file
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.github.qauxv.util.xpcompat;
|
||||
|
||||
/**
|
||||
* <p>Operations on arrays, primitive arrays (like {@code int[]}) and
|
||||
* primitive wrapper arrays (like {@code Integer[]}).</p>
|
||||
*
|
||||
* <p>This class tries to handle {@code null} input gracefully.
|
||||
* An exception will not be thrown for a {@code null} array input.
|
||||
* <p>
|
||||
* However, an Object array that contains a {@code null} element may throw an exception. Each method documents its behaviour.</p>
|
||||
*
|
||||
* <p>#ThreadSafe#</p>
|
||||
*
|
||||
* @version $Id: ArrayUtils.java 1154216 2011-08-05 13:57:16Z mbenson $
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ArrayUtils {
|
||||
|
||||
/**
|
||||
* An empty immutable {@code Object} array.
|
||||
*/
|
||||
public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
|
||||
/**
|
||||
* An empty immutable {@code Class} array.
|
||||
*/
|
||||
public static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[0];
|
||||
/**
|
||||
* An empty immutable {@code String} array.
|
||||
*/
|
||||
public static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||
/**
|
||||
* An empty immutable {@code long} array.
|
||||
*/
|
||||
public static final long[] EMPTY_LONG_ARRAY = new long[0];
|
||||
/**
|
||||
* An empty immutable {@code Long} array.
|
||||
*/
|
||||
public static final Long[] EMPTY_LONG_OBJECT_ARRAY = new Long[0];
|
||||
/**
|
||||
* An empty immutable {@code int} array.
|
||||
*/
|
||||
public static final int[] EMPTY_INT_ARRAY = new int[0];
|
||||
/**
|
||||
* An empty immutable {@code Integer} array.
|
||||
*/
|
||||
public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0];
|
||||
/**
|
||||
* An empty immutable {@code short} array.
|
||||
*/
|
||||
public static final short[] EMPTY_SHORT_ARRAY = new short[0];
|
||||
/**
|
||||
* An empty immutable {@code Short} array.
|
||||
*/
|
||||
public static final Short[] EMPTY_SHORT_OBJECT_ARRAY = new Short[0];
|
||||
/**
|
||||
* An empty immutable {@code byte} array.
|
||||
*/
|
||||
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||
/**
|
||||
* An empty immutable {@code Byte} array.
|
||||
*/
|
||||
public static final Byte[] EMPTY_BYTE_OBJECT_ARRAY = new Byte[0];
|
||||
/**
|
||||
* An empty immutable {@code double} array.
|
||||
*/
|
||||
public static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
|
||||
/**
|
||||
* An empty immutable {@code Double} array.
|
||||
*/
|
||||
public static final Double[] EMPTY_DOUBLE_OBJECT_ARRAY = new Double[0];
|
||||
/**
|
||||
* An empty immutable {@code float} array.
|
||||
*/
|
||||
public static final float[] EMPTY_FLOAT_ARRAY = new float[0];
|
||||
/**
|
||||
* An empty immutable {@code Float} array.
|
||||
*/
|
||||
public static final Float[] EMPTY_FLOAT_OBJECT_ARRAY = new Float[0];
|
||||
/**
|
||||
* An empty immutable {@code boolean} array.
|
||||
*/
|
||||
public static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
|
||||
/**
|
||||
* An empty immutable {@code Boolean} array.
|
||||
*/
|
||||
public static final Boolean[] EMPTY_BOOLEAN_OBJECT_ARRAY = new Boolean[0];
|
||||
/**
|
||||
* An empty immutable {@code char} array.
|
||||
*/
|
||||
public static final char[] EMPTY_CHAR_ARRAY = new char[0];
|
||||
/**
|
||||
* An empty immutable {@code Character} array.
|
||||
*/
|
||||
public static final Character[] EMPTY_CHARACTER_OBJECT_ARRAY = new Character[0];
|
||||
|
||||
// Is same length
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* <p>Checks whether two arrays are the same length, treating
|
||||
* {@code null} arrays as length {@code 0}.
|
||||
*
|
||||
* <p>Any multi-dimensional aspects of the arrays are ignored.</p>
|
||||
*
|
||||
* @param array1 the first array, may be {@code null}
|
||||
* @param array2 the second array, may be {@code null}
|
||||
* @return {@code true} if length of arrays matches, treating {@code null} as an empty array
|
||||
*/
|
||||
public static boolean isSameLength(Object[] array1, Object[] array2) {
|
||||
if ((array1 == null && array2 != null && array2.length > 0) ||
|
||||
(array2 == null && array1 != null && array1.length > 0) ||
|
||||
(array1 != null && array2 != null && array1.length != array2.length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether two arrays are the same length, treating
|
||||
* {@code null} arrays as length {@code 0}.</p>
|
||||
*
|
||||
* @param array1 the first array, may be {@code null}
|
||||
* @param array2 the second array, may be {@code null}
|
||||
* @return {@code true} if length of arrays matches, treating {@code null} as an empty array
|
||||
*/
|
||||
public static boolean isSameLength(long[] array1, long[] array2) {
|
||||
if ((array1 == null && array2 != null && array2.length > 0) ||
|
||||
(array2 == null && array1 != null && array1.length > 0) ||
|
||||
(array1 != null && array2 != null && array1.length != array2.length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether two arrays are the same length, treating
|
||||
* {@code null} arrays as length {@code 0}.</p>
|
||||
*
|
||||
* @param array1 the first array, may be {@code null}
|
||||
* @param array2 the second array, may be {@code null}
|
||||
* @return {@code true} if length of arrays matches, treating {@code null} as an empty array
|
||||
*/
|
||||
public static boolean isSameLength(int[] array1, int[] array2) {
|
||||
if ((array1 == null && array2 != null && array2.length > 0) ||
|
||||
(array2 == null && array1 != null && array1.length > 0) ||
|
||||
(array1 != null && array2 != null && array1.length != array2.length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether two arrays are the same length, treating
|
||||
* {@code null} arrays as length {@code 0}.</p>
|
||||
*
|
||||
* @param array1 the first array, may be {@code null}
|
||||
* @param array2 the second array, may be {@code null}
|
||||
* @return {@code true} if length of arrays matches, treating {@code null} as an empty array
|
||||
*/
|
||||
public static boolean isSameLength(short[] array1, short[] array2) {
|
||||
if ((array1 == null && array2 != null && array2.length > 0) ||
|
||||
(array2 == null && array1 != null && array1.length > 0) ||
|
||||
(array1 != null && array2 != null && array1.length != array2.length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether two arrays are the same length, treating
|
||||
* {@code null} arrays as length {@code 0}.</p>
|
||||
*
|
||||
* @param array1 the first array, may be {@code null}
|
||||
* @param array2 the second array, may be {@code null}
|
||||
* @return {@code true} if length of arrays matches, treating {@code null} as an empty array
|
||||
*/
|
||||
public static boolean isSameLength(char[] array1, char[] array2) {
|
||||
if ((array1 == null && array2 != null && array2.length > 0) ||
|
||||
(array2 == null && array1 != null && array1.length > 0) ||
|
||||
(array1 != null && array2 != null && array1.length != array2.length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether two arrays are the same length, treating
|
||||
* {@code null} arrays as length {@code 0}.</p>
|
||||
*
|
||||
* @param array1 the first array, may be {@code null}
|
||||
* @param array2 the second array, may be {@code null}
|
||||
* @return {@code true} if length of arrays matches, treating {@code null} as an empty array
|
||||
*/
|
||||
public static boolean isSameLength(byte[] array1, byte[] array2) {
|
||||
if ((array1 == null && array2 != null && array2.length > 0) ||
|
||||
(array2 == null && array1 != null && array1.length > 0) ||
|
||||
(array1 != null && array2 != null && array1.length != array2.length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether two arrays are the same length, treating
|
||||
* {@code null} arrays as length {@code 0}.</p>
|
||||
*
|
||||
* @param array1 the first array, may be {@code null}
|
||||
* @param array2 the second array, may be {@code null}
|
||||
* @return {@code true} if length of arrays matches, treating {@code null} as an empty array
|
||||
*/
|
||||
public static boolean isSameLength(double[] array1, double[] array2) {
|
||||
if ((array1 == null && array2 != null && array2.length > 0) ||
|
||||
(array2 == null && array1 != null && array1.length > 0) ||
|
||||
(array1 != null && array2 != null && array1.length != array2.length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether two arrays are the same length, treating
|
||||
* {@code null} arrays as length {@code 0}.</p>
|
||||
*
|
||||
* @param array1 the first array, may be {@code null}
|
||||
* @param array2 the second array, may be {@code null}
|
||||
* @return {@code true} if length of arrays matches, treating {@code null} as an empty array
|
||||
*/
|
||||
public static boolean isSameLength(float[] array1, float[] array2) {
|
||||
if ((array1 == null && array2 != null && array2.length > 0) ||
|
||||
(array2 == null && array1 != null && array1.length > 0) ||
|
||||
(array1 != null && array2 != null && array1.length != array2.length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether two arrays are the same length, treating
|
||||
* {@code null} arrays as length {@code 0}.</p>
|
||||
*
|
||||
* @param array1 the first array, may be {@code null}
|
||||
* @param array2 the second array, may be {@code null}
|
||||
* @return {@code true} if length of arrays matches, treating {@code null} as an empty array
|
||||
*/
|
||||
public static boolean isSameLength(boolean[] array1, boolean[] array2) {
|
||||
if ((array1 == null && array2 != null && array2.length > 0) ||
|
||||
(array2 == null && array1 != null && array1.length > 0) ||
|
||||
(array1 != null && array2 != null && array1.length != array2.length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
345
app/src/main/java/io/github/qauxv/util/xpcompat/ClassUtils.java
Normal file
345
app/src/main/java/io/github/qauxv/util/xpcompat/ClassUtils.java
Normal file
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.github.qauxv.util.xpcompat;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
// org.apache.commons.lang3
|
||||
|
||||
/**
|
||||
* <p>Operates on classes without using reflection.</p>
|
||||
*
|
||||
* <p>This class handles invalid {@code null} inputs as best it can.
|
||||
* Each method documents its behaviour in more detail.</p>
|
||||
*
|
||||
* <p>The notion of a {@code canonical name} includes the human
|
||||
* readable name for the type, for example {@code int[]}. The non-canonical method variants work with the JVM names, such as {@code [I}. </p>
|
||||
*
|
||||
* @version $Id: ClassUtils.java 1199894 2011-11-09 17:53:59Z ggregory $
|
||||
* @since 2.0
|
||||
*/
|
||||
/*package*/
|
||||
class ClassUtils {
|
||||
|
||||
/**
|
||||
* Maps primitive {@code Class}es to their corresponding wrapper {@code Class}.
|
||||
*/
|
||||
private static final Map<Class<?>, Class<?>> primitiveWrapperMap = new HashMap<Class<?>, Class<?>>();
|
||||
|
||||
static {
|
||||
primitiveWrapperMap.put(Boolean.TYPE, Boolean.class);
|
||||
primitiveWrapperMap.put(Byte.TYPE, Byte.class);
|
||||
primitiveWrapperMap.put(Character.TYPE, Character.class);
|
||||
primitiveWrapperMap.put(Short.TYPE, Short.class);
|
||||
primitiveWrapperMap.put(Integer.TYPE, Integer.class);
|
||||
primitiveWrapperMap.put(Long.TYPE, Long.class);
|
||||
primitiveWrapperMap.put(Double.TYPE, Double.class);
|
||||
primitiveWrapperMap.put(Float.TYPE, Float.class);
|
||||
primitiveWrapperMap.put(Void.TYPE, Void.TYPE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps wrapper {@code Class}es to their corresponding primitive types.
|
||||
*/
|
||||
private static final Map<Class<?>, Class<?>> wrapperPrimitiveMap = new HashMap<Class<?>, Class<?>>();
|
||||
|
||||
static {
|
||||
for (Class<?> primitiveClass : primitiveWrapperMap.keySet()) {
|
||||
Class<?> wrapperClass = primitiveWrapperMap.get(primitiveClass);
|
||||
if (!primitiveClass.equals(wrapperClass)) {
|
||||
wrapperPrimitiveMap.put(wrapperClass, primitiveClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is assignable
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* <p>Checks if an array of Classes can be assigned to another array of Classes.</p>
|
||||
*
|
||||
* <p>This method calls {@link #isAssignable(Class, Class) isAssignable} for each
|
||||
* Class pair in the input arrays. It can be used to check if a set of arguments (the first parameter) are suitably compatible with a set of method
|
||||
* parameter types (the second parameter).</p>
|
||||
*
|
||||
* <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, this
|
||||
* method takes into account widenings of primitive classes and {@code null}s.</p>
|
||||
*
|
||||
* <p>Primitive widenings allow an int to be assigned to a {@code long},
|
||||
* {@code float} or {@code double}. This method returns the correct result for these cases.</p>
|
||||
*
|
||||
* <p>{@code Null} may be assigned to any reference type. This method will
|
||||
* return {@code true} if {@code null} is passed in and the toClass is non-primitive.</p>
|
||||
*
|
||||
* <p>Specifically, this method tests whether the type represented by the
|
||||
* specified {@code Class} parameter can be converted to the type represented by this {@code Class} object via an identity conversion widening primitive or
|
||||
* widening reference conversion. See
|
||||
* <em><a href="http://java.sun.com/docs/books/jls/">The Java Language Specification</a></em>,
|
||||
* sections 5.1.1, 5.1.2 and 5.1.4 for details.</p>
|
||||
*
|
||||
* <p><strong>Since Lang 3.0,</strong> this method will default behavior for
|
||||
* calculating assignability between primitive and wrapper types <em>corresponding to the running Java version</em>; i.e. autoboxing will be the default
|
||||
* behavior in VMs running Java versions >= 1.5.</p>
|
||||
*
|
||||
* @param classArray the array of Classes to check, may be {@code null}
|
||||
* @param toClassArray the array of Classes to try to assign into, may be {@code null}
|
||||
* @return {@code true} if assignment possible
|
||||
*/
|
||||
public static boolean isAssignable(Class<?>[] classArray, Class<?>... toClassArray) {
|
||||
return isAssignable(classArray, toClassArray, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks if an array of Classes can be assigned to another array of Classes.</p>
|
||||
*
|
||||
* <p>This method calls {@link #isAssignable(Class, Class) isAssignable} for each
|
||||
* Class pair in the input arrays. It can be used to check if a set of arguments (the first parameter) are suitably compatible with a set of method
|
||||
* parameter types (the second parameter).</p>
|
||||
*
|
||||
* <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, this
|
||||
* method takes into account widenings of primitive classes and {@code null}s.</p>
|
||||
*
|
||||
* <p>Primitive widenings allow an int to be assigned to a {@code long},
|
||||
* {@code float} or {@code double}. This method returns the correct result for these cases.</p>
|
||||
*
|
||||
* <p>{@code Null} may be assigned to any reference type. This method will
|
||||
* return {@code true} if {@code null} is passed in and the toClass is non-primitive.</p>
|
||||
*
|
||||
* <p>Specifically, this method tests whether the type represented by the
|
||||
* specified {@code Class} parameter can be converted to the type represented by this {@code Class} object via an identity conversion widening primitive or
|
||||
* widening reference conversion. See
|
||||
* <em><a href="http://java.sun.com/docs/books/jls/">The Java Language Specification</a></em>,
|
||||
* sections 5.1.1, 5.1.2 and 5.1.4 for details.</p>
|
||||
*
|
||||
* @param classArray the array of Classes to check, may be {@code null}
|
||||
* @param toClassArray the array of Classes to try to assign into, may be {@code null}
|
||||
* @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
|
||||
* @return {@code true} if assignment possible
|
||||
*/
|
||||
public static boolean isAssignable(Class<?>[] classArray, Class<?>[] toClassArray, boolean autoboxing) {
|
||||
if (ArrayUtils.isSameLength(classArray, toClassArray) == false) {
|
||||
return false;
|
||||
}
|
||||
if (classArray == null) {
|
||||
classArray = ArrayUtils.EMPTY_CLASS_ARRAY;
|
||||
}
|
||||
if (toClassArray == null) {
|
||||
toClassArray = ArrayUtils.EMPTY_CLASS_ARRAY;
|
||||
}
|
||||
for (int i = 0; i < classArray.length; i++) {
|
||||
if (isAssignable(classArray[i], toClassArray[i], autoboxing) == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>Checks if one {@code Class} can be assigned to a variable of
|
||||
* another {@code Class}.</p>
|
||||
*
|
||||
* <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method,
|
||||
* this method takes into account widenings of primitive classes and {@code null}s.</p>
|
||||
*
|
||||
* <p>Primitive widenings allow an int to be assigned to a long, float or
|
||||
* double. This method returns the correct result for these cases.</p>
|
||||
*
|
||||
* <p>{@code Null} may be assigned to any reference type. This method
|
||||
* will return {@code true} if {@code null} is passed in and the toClass is non-primitive.</p>
|
||||
*
|
||||
* <p>Specifically, this method tests whether the type represented by the
|
||||
* specified {@code Class} parameter can be converted to the type represented by this {@code Class} object via an identity conversion widening primitive or
|
||||
* widening reference conversion. See
|
||||
* <em><a href="http://java.sun.com/docs/books/jls/">The Java Language Specification</a></em>,
|
||||
* sections 5.1.1, 5.1.2 and 5.1.4 for details.</p>
|
||||
*
|
||||
* <p><strong>Since Lang 3.0,</strong> this method will default behavior for
|
||||
* calculating assignability between primitive and wrapper types <em>corresponding to the running Java version</em>; i.e. autoboxing will be the default
|
||||
* behavior in VMs running Java versions >= 1.5.</p>
|
||||
*
|
||||
* @param cls the Class to check, may be null
|
||||
* @param toClass the Class to try to assign into, returns false if null
|
||||
* @return {@code true} if assignment possible
|
||||
*/
|
||||
public static boolean isAssignable(Class<?> cls, Class<?> toClass) {
|
||||
return isAssignable(cls, toClass, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks if one {@code Class} can be assigned to a variable of
|
||||
* another {@code Class}.</p>
|
||||
*
|
||||
* <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method,
|
||||
* this method takes into account widenings of primitive classes and {@code null}s.</p>
|
||||
*
|
||||
* <p>Primitive widenings allow an int to be assigned to a long, float or
|
||||
* double. This method returns the correct result for these cases.</p>
|
||||
*
|
||||
* <p>{@code Null} may be assigned to any reference type. This method
|
||||
* will return {@code true} if {@code null} is passed in and the toClass is non-primitive.</p>
|
||||
*
|
||||
* <p>Specifically, this method tests whether the type represented by the
|
||||
* specified {@code Class} parameter can be converted to the type represented by this {@code Class} object via an identity conversion widening primitive or
|
||||
* widening reference conversion. See
|
||||
* <em><a href="http://java.sun.com/docs/books/jls/">The Java Language Specification</a></em>,
|
||||
* sections 5.1.1, 5.1.2 and 5.1.4 for details.</p>
|
||||
*
|
||||
* @param cls the Class to check, may be null
|
||||
* @param toClass the Class to try to assign into, returns false if null
|
||||
* @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
|
||||
* @return {@code true} if assignment possible
|
||||
*/
|
||||
public static boolean isAssignable(Class<?> cls, Class<?> toClass, boolean autoboxing) {
|
||||
if (toClass == null) {
|
||||
return false;
|
||||
}
|
||||
// have to check for null, as isAssignableFrom doesn't
|
||||
if (cls == null) {
|
||||
return !toClass.isPrimitive();
|
||||
}
|
||||
//autoboxing:
|
||||
if (autoboxing) {
|
||||
if (cls.isPrimitive() && !toClass.isPrimitive()) {
|
||||
cls = primitiveToWrapper(cls);
|
||||
if (cls == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (toClass.isPrimitive() && !cls.isPrimitive()) {
|
||||
cls = wrapperToPrimitive(cls);
|
||||
if (cls == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cls.equals(toClass)) {
|
||||
return true;
|
||||
}
|
||||
if (cls.isPrimitive()) {
|
||||
if (toClass.isPrimitive() == false) {
|
||||
return false;
|
||||
}
|
||||
if (Integer.TYPE.equals(cls)) {
|
||||
return Long.TYPE.equals(toClass)
|
||||
|| Float.TYPE.equals(toClass)
|
||||
|| Double.TYPE.equals(toClass);
|
||||
}
|
||||
if (Long.TYPE.equals(cls)) {
|
||||
return Float.TYPE.equals(toClass)
|
||||
|| Double.TYPE.equals(toClass);
|
||||
}
|
||||
if (Boolean.TYPE.equals(cls)) {
|
||||
return false;
|
||||
}
|
||||
if (Double.TYPE.equals(cls)) {
|
||||
return false;
|
||||
}
|
||||
if (Float.TYPE.equals(cls)) {
|
||||
return Double.TYPE.equals(toClass);
|
||||
}
|
||||
if (Character.TYPE.equals(cls)) {
|
||||
return Integer.TYPE.equals(toClass)
|
||||
|| Long.TYPE.equals(toClass)
|
||||
|| Float.TYPE.equals(toClass)
|
||||
|| Double.TYPE.equals(toClass);
|
||||
}
|
||||
if (Short.TYPE.equals(cls)) {
|
||||
return Integer.TYPE.equals(toClass)
|
||||
|| Long.TYPE.equals(toClass)
|
||||
|| Float.TYPE.equals(toClass)
|
||||
|| Double.TYPE.equals(toClass);
|
||||
}
|
||||
if (Byte.TYPE.equals(cls)) {
|
||||
return Short.TYPE.equals(toClass)
|
||||
|| Integer.TYPE.equals(toClass)
|
||||
|| Long.TYPE.equals(toClass)
|
||||
|| Float.TYPE.equals(toClass)
|
||||
|| Double.TYPE.equals(toClass);
|
||||
}
|
||||
// should never get here
|
||||
return false;
|
||||
}
|
||||
return toClass.isAssignableFrom(cls);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Converts the specified primitive Class object to its corresponding
|
||||
* wrapper Class object.</p>
|
||||
*
|
||||
* <p>NOTE: From v2.2, this method handles {@code Void.TYPE},
|
||||
* returning {@code Void.TYPE}.</p>
|
||||
*
|
||||
* @param cls the class to convert, may be null
|
||||
* @return the wrapper class for {@code cls} or {@code cls} if {@code cls} is not a primitive. {@code null} if null input.
|
||||
* @since 2.1
|
||||
*/
|
||||
public static Class<?> primitiveToWrapper(Class<?> cls) {
|
||||
Class<?> convertedClass = cls;
|
||||
if (cls != null && cls.isPrimitive()) {
|
||||
convertedClass = primitiveWrapperMap.get(cls);
|
||||
}
|
||||
return convertedClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Converts the specified array of primitive Class objects to an array of
|
||||
* its corresponding wrapper Class objects.</p>
|
||||
*
|
||||
* @param classes the class array to convert, may be null or empty
|
||||
* @return an array which contains for each given class, the wrapper class or the original class if class is not a primitive. {@code null} if null input.
|
||||
* Empty array if an empty array passed in.
|
||||
* @since 2.1
|
||||
*/
|
||||
public static Class<?>[] primitivesToWrappers(Class<?>... classes) {
|
||||
if (classes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (classes.length == 0) {
|
||||
return classes;
|
||||
}
|
||||
|
||||
Class<?>[] convertedClasses = new Class[classes.length];
|
||||
for (int i = 0; i < classes.length; i++) {
|
||||
convertedClasses[i] = primitiveToWrapper(classes[i]);
|
||||
}
|
||||
return convertedClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Converts the specified wrapper class to its corresponding primitive
|
||||
* class.</p>
|
||||
*
|
||||
* <p>This method is the counter part of {@code primitiveToWrapper()}.
|
||||
* If the passed in class is a wrapper class for a primitive type, this primitive type will be returned (e.g. {@code Integer.TYPE} for
|
||||
* {@code Integer.class}). For other classes, or if the parameter is
|
||||
* <b>null</b>, the return value is <b>null</b>.</p>
|
||||
*
|
||||
* @param cls the class to convert, may be <b>null</b>
|
||||
* @return the corresponding primitive type if {@code cls} is a wrapper class, <b>null</b> otherwise
|
||||
* @see #primitiveToWrapper(Class)
|
||||
* @since 2.4
|
||||
*/
|
||||
public static Class<?> wrapperToPrimitive(Class<?> cls) {
|
||||
return wrapperPrimitiveMap.get(cls);
|
||||
}
|
||||
|
||||
}
|
||||
134
app/src/main/java/io/github/qauxv/util/xpcompat/MemberUtils.java
Normal file
134
app/src/main/java/io/github/qauxv/util/xpcompat/MemberUtils.java
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.github.qauxv.util.xpcompat;
|
||||
|
||||
// org.apache.commons.lang3
|
||||
|
||||
/**
|
||||
* Contains common code for working with Methods/Constructors, extracted and refactored from <code>MethodUtils</code> when it was imported from Commons
|
||||
* BeanUtils.
|
||||
*
|
||||
* @version $Id: MemberUtils.java 1143537 2011-07-06 19:30:22Z joehni $
|
||||
* @since 2.5
|
||||
*/
|
||||
/*package*/
|
||||
class MemberUtils {
|
||||
|
||||
/**
|
||||
* Array of primitive number types ordered by "promotability"
|
||||
*/
|
||||
private static final Class<?>[] ORDERED_PRIMITIVE_TYPES = {Byte.TYPE, Short.TYPE,
|
||||
Character.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE};
|
||||
|
||||
/**
|
||||
* Compares the relative fitness of two sets of parameter types in terms of matching a third set of runtime parameter types, such that a list ordered by the
|
||||
* results of the comparison would return the best match first (least).
|
||||
*
|
||||
* @param left the "left" parameter set
|
||||
* @param right the "right" parameter set
|
||||
* @param actual the runtime parameter types to match against
|
||||
* <code>left</code>/<code>right</code>
|
||||
* @return int consistent with <code>compare</code> semantics
|
||||
*/
|
||||
public static int compareParameterTypes(Class<?>[] left, Class<?>[] right, Class<?>[] actual) {
|
||||
float leftCost = getTotalTransformationCost(actual, left);
|
||||
float rightCost = getTotalTransformationCost(actual, right);
|
||||
return leftCost < rightCost ? -1 : rightCost < leftCost ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of the object transformation cost for each class in the source argument list.
|
||||
*
|
||||
* @param srcArgs The source arguments
|
||||
* @param destArgs The destination arguments
|
||||
* @return The total transformation cost
|
||||
*/
|
||||
private static float getTotalTransformationCost(Class<?>[] srcArgs, Class<?>[] destArgs) {
|
||||
float totalCost = 0.0f;
|
||||
for (int i = 0; i < srcArgs.length; i++) {
|
||||
Class<?> srcClass, destClass;
|
||||
srcClass = srcArgs[i];
|
||||
destClass = destArgs[i];
|
||||
totalCost += getObjectTransformationCost(srcClass, destClass);
|
||||
}
|
||||
return totalCost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of steps required needed to turn the source class into the destination class. This represents the number of steps in the object hierarchy
|
||||
* graph.
|
||||
*
|
||||
* @param srcClass The source class
|
||||
* @param destClass The destination class
|
||||
* @return The cost of transforming an object
|
||||
*/
|
||||
private static float getObjectTransformationCost(Class<?> srcClass, Class<?> destClass) {
|
||||
if (destClass.isPrimitive()) {
|
||||
return getPrimitivePromotionCost(srcClass, destClass);
|
||||
}
|
||||
float cost = 0.0f;
|
||||
while (srcClass != null && !destClass.equals(srcClass)) {
|
||||
if (destClass.isInterface() && ClassUtils.isAssignable(srcClass, destClass)) {
|
||||
// slight penalty for interface match.
|
||||
// we still want an exact match to override an interface match,
|
||||
// but
|
||||
// an interface match should override anything where we have to
|
||||
// get a superclass.
|
||||
cost += 0.25f;
|
||||
break;
|
||||
}
|
||||
cost++;
|
||||
srcClass = srcClass.getSuperclass();
|
||||
}
|
||||
/*
|
||||
* If the destination class is null, we've travelled all the way up to
|
||||
* an Object match. We'll penalize this by adding 1.5 to the cost.
|
||||
*/
|
||||
if (srcClass == null) {
|
||||
cost += 1.5f;
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of steps required to promote a primitive number to another type.
|
||||
*
|
||||
* @param srcClass the (primitive) source class
|
||||
* @param destClass the (primitive) destination class
|
||||
* @return The cost of promoting the primitive
|
||||
*/
|
||||
private static float getPrimitivePromotionCost(final Class<?> srcClass, final Class<?> destClass) {
|
||||
float cost = 0.0f;
|
||||
Class<?> cls = srcClass;
|
||||
if (!cls.isPrimitive()) {
|
||||
// slight unwrapping penalty
|
||||
cost += 0.1f;
|
||||
cls = ClassUtils.wrapperToPrimitive(cls);
|
||||
}
|
||||
for (int i = 0; cls != destClass && i < ORDERED_PRIMITIVE_TYPES.length; i++) {
|
||||
if (cls == ORDERED_PRIMITIVE_TYPES[i]) {
|
||||
cost += 0.1f;
|
||||
if (i < ORDERED_PRIMITIVE_TYPES.length - 1) {
|
||||
cls = ORDERED_PRIMITIVE_TYPES[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package io.github.qauxv.util.xpcompat;
|
||||
|
||||
import io.github.qauxv.loader.hookapi.IHookBridge;
|
||||
import java.lang.reflect.Member;
|
||||
|
||||
/**
|
||||
* Callback class for method hooks.
|
||||
*
|
||||
* <p>Usually, anonymous subclasses of this class are created which override
|
||||
* {@link #beforeHookedMethod} and/or {@link #afterHookedMethod}.
|
||||
*/
|
||||
public abstract class XC_MethodHook {
|
||||
|
||||
private final int priority;
|
||||
|
||||
/**
|
||||
* Creates a new callback with default priority.
|
||||
*/
|
||||
public XC_MethodHook() {
|
||||
priority = IHookBridge.PRIORITY_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new callback with a specific priority.
|
||||
*
|
||||
* <p class="note">Note that {@link #afterHookedMethod} will be called in reversed order, i.e.
|
||||
* the callback with the highest priority will be called last. This way, the callback has the final control over the return value.
|
||||
* {@link #beforeHookedMethod} is called as usual, i.e. highest priority first.
|
||||
*
|
||||
* @param priority The priority for this callback.
|
||||
*/
|
||||
public XC_MethodHook(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the invocation of the method.
|
||||
*
|
||||
* <p>You can use {@link XC_MethodHook.MethodHookParam#setResult} and
|
||||
* {@link XC_MethodHook.MethodHookParam#setThrowable} to prevent the original method from being called.
|
||||
*
|
||||
* <p>Note that implementations shouldn't call {@code super(param)}, it's not necessary.
|
||||
*
|
||||
* @param param Information about the method call.
|
||||
* @throws Throwable Everything the callback throws is caught and logged.
|
||||
*/
|
||||
protected void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the invocation of the method.
|
||||
*
|
||||
* <p>You can use {@link XC_MethodHook.MethodHookParam#setResult} and
|
||||
* {@link XC_MethodHook.MethodHookParam#setThrowable} to modify the return value of the original method.
|
||||
*
|
||||
* <p>Note that implementations shouldn't call {@code super(param)}, it's not necessary.
|
||||
*
|
||||
* @param param Information about the method call.
|
||||
* @throws Throwable Everything the callback throws is caught and logged.
|
||||
*/
|
||||
protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps information about the method call and allows to influence it.
|
||||
*/
|
||||
public static abstract class MethodHookParam {
|
||||
|
||||
protected MethodHookParam() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The hooked method/constructor.
|
||||
*/
|
||||
public Member method;
|
||||
|
||||
/**
|
||||
* The {@code this} reference for an instance method, or {@code null} for static methods.
|
||||
*/
|
||||
public Object thisObject;
|
||||
|
||||
/**
|
||||
* Arguments to the method call.
|
||||
*/
|
||||
public Object[] args;
|
||||
|
||||
/**
|
||||
* Returns the result of the method call.
|
||||
*/
|
||||
public abstract Object getResult();
|
||||
|
||||
/**
|
||||
* Modify the result of the method call.
|
||||
*
|
||||
* <p>If called from {@link #beforeHookedMethod}, it prevents the call to the original method.
|
||||
*/
|
||||
public abstract void setResult(Object result);
|
||||
|
||||
/**
|
||||
* Returns the {@link Throwable} thrown by the method, or {@code null}.
|
||||
*/
|
||||
public abstract Throwable getThrowable();
|
||||
|
||||
/**
|
||||
* Returns true if an exception was thrown by the method.
|
||||
*/
|
||||
public abstract boolean hasThrowable();
|
||||
|
||||
/**
|
||||
* Modify the exception thrown of the method call.
|
||||
*
|
||||
* <p>If called from {@link #beforeHookedMethod}, it prevents the call to the original method.
|
||||
*/
|
||||
public abstract void setThrowable(Throwable throwable);
|
||||
|
||||
/**
|
||||
* Returns the result of the method call, or throws the Throwable caused by it.
|
||||
*/
|
||||
public abstract Object getResultOrThrowable() throws Throwable;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object with which the method/constructor can be unhooked.
|
||||
*/
|
||||
public static class Unhook {
|
||||
|
||||
private final IHookBridge.MemberUnhookHandle unhookHandle;
|
||||
private final XC_MethodHook callback;
|
||||
|
||||
/*package*/ Unhook(IHookBridge.MemberUnhookHandle unhookHandle, XC_MethodHook callback) {
|
||||
this.unhookHandle = unhookHandle;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the method/constructor that has been hooked.
|
||||
*/
|
||||
public Member getHookedMethod() {
|
||||
return unhookHandle.getMember();
|
||||
}
|
||||
|
||||
public XC_MethodHook getCallback() {
|
||||
return callback;
|
||||
}
|
||||
|
||||
public void unhook() {
|
||||
unhookHandle.unhook();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package io.github.qauxv.util.xpcompat;
|
||||
|
||||
import io.github.qauxv.loader.hookapi.IHookBridge;
|
||||
|
||||
public abstract class XC_MethodReplacement extends XC_MethodHook {
|
||||
/**
|
||||
* Creates a new callback with default priority.
|
||||
*/
|
||||
public XC_MethodReplacement() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new callback with a specific priority.
|
||||
*
|
||||
* @param priority See Xposed callback priorities.
|
||||
*/
|
||||
public XC_MethodReplacement(int priority) {
|
||||
super(priority);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void beforeHookedMethod(MethodHookParam param) throws Throwable {
|
||||
try {
|
||||
Object result = replaceHookedMethod(param);
|
||||
param.setResult(result);
|
||||
} catch (Throwable t) {
|
||||
param.setThrowable(t);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("EmptyMethod")
|
||||
protected final void afterHookedMethod(MethodHookParam param) throws Throwable {}
|
||||
|
||||
/**
|
||||
* Shortcut for replacing a method completely. Whatever is returned/thrown here is taken
|
||||
* instead of the result of the original method (which will not be called).
|
||||
*
|
||||
* <p>Note that implementations shouldn't call {@code super(param)}, it's not necessary.
|
||||
*
|
||||
* @param param Information about the method call.
|
||||
* @throws Throwable Anything that is thrown by the callback will be passed on to the original caller.
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
protected abstract Object replaceHookedMethod(MethodHookParam param) throws Throwable;
|
||||
|
||||
/**
|
||||
* Predefined callback that skips the method without replacements.
|
||||
*/
|
||||
public static final XC_MethodReplacement DO_NOTHING = new XC_MethodReplacement(IHookBridge.PRIORITY_HIGHEST *2) {
|
||||
@Override
|
||||
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a callback which always returns a specific value.
|
||||
*
|
||||
* @param result The value that should be returned to callers of the hooked method.
|
||||
*/
|
||||
public static XC_MethodReplacement returnConstant(final Object result) {
|
||||
return returnConstant(IHookBridge.PRIORITY_DEFAULT, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #returnConstant(Object)}, but allows to specify a priority for the callback.
|
||||
*
|
||||
* @param priority See Xposed callback priorities.
|
||||
* @param result The value that should be returned to callers of the hooked method.
|
||||
*/
|
||||
public static XC_MethodReplacement returnConstant(int priority, final Object result) {
|
||||
return new XC_MethodReplacement(priority) {
|
||||
@Override
|
||||
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package io.github.qauxv.util.xpcompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import io.github.qauxv.loader.hookapi.IHookBridge;
|
||||
import io.github.qauxv.poststartup.StartupInfo;
|
||||
import java.lang.reflect.Member;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The XposedBridge compatibility layer.
|
||||
*/
|
||||
public class XposedBridge {
|
||||
|
||||
private XposedBridge() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks all methods with a certain name that were declared in the specified class. Inherited methods and constructors are not considered. For constructors,
|
||||
* use {@link #hookAllConstructors} instead.
|
||||
*
|
||||
* @param hookClass The class to check for declared methods.
|
||||
* @param methodName The name of the method(s) to hook.
|
||||
* @param callback The callback to be executed when the hooked methods are called.
|
||||
* @return A set containing one object for each found method which can be used to unhook it.
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static Set<XC_MethodHook.Unhook> hookAllMethods(Class<?> hookClass, String methodName, XC_MethodHook callback) {
|
||||
Set<XC_MethodHook.Unhook> unhooks = new HashSet<XC_MethodHook.Unhook>();
|
||||
for (Member method : hookClass.getDeclaredMethods()) {
|
||||
if (method.getName().equals(methodName)) {
|
||||
unhooks.add(hookMethod(method, callback));
|
||||
}
|
||||
}
|
||||
return unhooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook all constructors of the specified class.
|
||||
*
|
||||
* @param hookClass The class to check for constructors.
|
||||
* @param callback The callback to be executed when the hooked constructors are called.
|
||||
* @return A set containing one object for each found constructor which can be used to unhook it.
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public static Set<XC_MethodHook.Unhook> hookAllConstructors(Class<?> hookClass, XC_MethodHook callback) {
|
||||
Set<XC_MethodHook.Unhook> unhooks = new HashSet<XC_MethodHook.Unhook>();
|
||||
for (Member constructor : hookClass.getDeclaredConstructors()) {
|
||||
unhooks.add(hookMethod(constructor, callback));
|
||||
}
|
||||
return unhooks;
|
||||
}
|
||||
|
||||
private static IHookBridge requireHookBridge() {
|
||||
IHookBridge hookBridge = StartupInfo.getHookBridge();
|
||||
if (hookBridge == null) {
|
||||
throw new IllegalStateException("Hook bridge not available");
|
||||
}
|
||||
return hookBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook any method (or constructor) with the specified callback. See below for some wrappers that make it easier to find a method/constructor in one step.
|
||||
*
|
||||
* @param hookMethod The method to be hooked.
|
||||
* @param callback The callback to be executed when the hooked method is called.
|
||||
* @return An object that can be used to remove the hook.
|
||||
* @see XposedHelpers#findAndHookMethod(String, ClassLoader, String, Object...)
|
||||
* @see XposedHelpers#findAndHookMethod(Class, String, Object...)
|
||||
* @see #hookAllMethods
|
||||
* @see XposedHelpers#findAndHookConstructor(String, ClassLoader, Object...)
|
||||
* @see XposedHelpers#findAndHookConstructor(Class, Object...)
|
||||
* @see #hookAllConstructors
|
||||
*/
|
||||
public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
|
||||
if (hookMethod == null) {
|
||||
throw new IllegalArgumentException("hookMethod must not be null");
|
||||
}
|
||||
if (callback == null) {
|
||||
throw new IllegalArgumentException("callback must not be null");
|
||||
}
|
||||
int priority = callback.getPriority();
|
||||
IHookBridge hookBridge = requireHookBridge();
|
||||
IHookBridge.IMemberHookCallback wrappedCallback = new WrappedHookCallback(callback);
|
||||
IHookBridge.MemberUnhookHandle unhookHandle = hookBridge.hookMethod(hookMethod, wrappedCallback, priority);
|
||||
return new XC_MethodHook.Unhook(unhookHandle, callback);
|
||||
}
|
||||
|
||||
private static class WrappedHookParam extends XC_MethodHook.MethodHookParam {
|
||||
|
||||
private WrappedHookParam() {
|
||||
}
|
||||
|
||||
private IHookBridge.IMemberHookParam param;
|
||||
|
||||
@Override
|
||||
public Object getResult() {
|
||||
return param.getResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResult(Object result) {
|
||||
param.setResult(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable getThrowable() {
|
||||
return param.getThrowable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasThrowable() {
|
||||
return param.getThrowable() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThrowable(Throwable throwable) {
|
||||
param.setThrowable(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResultOrThrowable() throws Throwable {
|
||||
Throwable throwable = param.getThrowable();
|
||||
if (throwable != null) {
|
||||
throw throwable;
|
||||
}
|
||||
return param.getResult();
|
||||
}
|
||||
}
|
||||
|
||||
private static class WrappedHookCallback implements IHookBridge.IMemberHookCallback {
|
||||
|
||||
private final XC_MethodHook callback;
|
||||
|
||||
public WrappedHookCallback(@NonNull XC_MethodHook callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeHookedMember(@NonNull IHookBridge.IMemberHookParam param) throws Throwable {
|
||||
WrappedHookParam wrappedParam = new WrappedHookParam();
|
||||
wrappedParam.param = param;
|
||||
wrappedParam.method = param.getMember();
|
||||
wrappedParam.thisObject = param.getThisObject();
|
||||
wrappedParam.args = param.getArgs();
|
||||
param.setExtra(wrappedParam);
|
||||
callback.beforeHookedMethod(wrappedParam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHookedMember(@NonNull IHookBridge.IMemberHookParam param) throws Throwable {
|
||||
WrappedHookParam wrappedParam = (WrappedHookParam) param.getExtra();
|
||||
if (wrappedParam == null) {
|
||||
throw new IllegalStateException("beforeHookedMember not called");
|
||||
}
|
||||
wrappedParam.method = param.getMember();
|
||||
wrappedParam.thisObject = param.getThisObject();
|
||||
wrappedParam.args = param.getArgs();
|
||||
callback.afterHookedMethod(wrappedParam);
|
||||
}
|
||||
}
|
||||
|
||||
public static void log(String message) {
|
||||
StartupInfo.getLoaderInfo().log(message);
|
||||
}
|
||||
|
||||
public static void log(Throwable tr) {
|
||||
StartupInfo.getLoaderInfo().log(tr);
|
||||
}
|
||||
|
||||
}
|
||||
1674
app/src/main/java/io/github/qauxv/util/xpcompat/XposedHelpers.java
Normal file
1674
app/src/main/java/io/github/qauxv/util/xpcompat/XposedHelpers.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,6 @@ import io.github.qauxv.base.annotation.FunctionHookEntry
|
||||
import io.github.qauxv.base.annotation.UiItemAgentEntry
|
||||
import io.github.qauxv.dsl.FunctionEntryRouter
|
||||
import io.github.qauxv.hook.CommonConfigFunctionHook
|
||||
import io.github.qauxv.startup.HookEntry
|
||||
import io.github.qauxv.ui.CommonContextWrapper
|
||||
import io.github.qauxv.util.Toasts
|
||||
import io.github.qauxv.util.hostInfo
|
||||
@@ -65,7 +64,7 @@ object ManageComponent : CommonConfigFunctionHook("Ketal_ManageComponent") {
|
||||
"发送到我的电脑" to ComponentName(hostInfo.packageName, "com.tencent.mobileqq.activity.qfileJumpActivity"),
|
||||
"保存到QQ收藏" to ComponentName(hostInfo.packageName, "cooperation.qqfav.widget.QfavJumpActivity"),
|
||||
"面对面快传" to ComponentName(hostInfo.packageName, "cooperation.qlink.QlinkShareJumpActivity"),
|
||||
"发送到我的iPad" to ComponentName(HookEntry.PACKAGE_NAME_SELF, "me.ketal.ui.activity.QFileShareToIpadActivity")
|
||||
"发送到我的iPad" to ComponentName(BuildConfig.APPLICATION_ID, "me.ketal.ui.activity.QFileShareToIpadActivity")
|
||||
)
|
||||
|
||||
private fun showDialog(context: Context) {
|
||||
|
||||
@@ -27,7 +27,7 @@ import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.os.Bundle
|
||||
import io.github.qauxv.startup.HookEntry
|
||||
import io.github.qauxv.util.PackageConstants
|
||||
|
||||
class QFileShareToIpadActivity : Activity() {
|
||||
companion object {
|
||||
@@ -38,7 +38,7 @@ class QFileShareToIpadActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pkg = HookEntry.PACKAGE_NAME_QQ
|
||||
val pkg = PackageConstants.PACKAGE_NAME_QQ
|
||||
intent.apply {
|
||||
putExtra("targetUin", "9962")
|
||||
putExtra("device_type", 1)
|
||||
|
||||
@@ -31,7 +31,7 @@ lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", versi
|
||||
material = { module = "com.google.android.material:material", version = "1.12.0" }
|
||||
material-dialogs-core = { module = "com.afollestad.material-dialogs:core", version.ref = "materialDialog" }
|
||||
material-dialogs-input = { module = "com.afollestad.material-dialogs:input", version.ref = "materialDialog" }
|
||||
xposed = { module = "de.robv.android.xposed:api", version = "82" }
|
||||
xposed-api = { module = "de.robv.android.xposed:api", version = "82" }
|
||||
flexbox = { module = "com.google.android.flexbox:flexbox", version = "3.0.0" }
|
||||
colorpicker = { module = "com.jaredrummler:colorpicker", version = "1.1.0" }
|
||||
ezXHelper = { module = "com.github.kyuubiran:EzXHelper", version = "1.0.3" }
|
||||
@@ -54,3 +54,4 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version = "11.2.2" }
|
||||
protobuf = { id = "com.google.protobuf", version = "0.9.4" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
|
||||
21
loader/hookapi/build.gradle.kts
Normal file
21
loader/hookapi/build.gradle.kts
Normal file
@@ -0,0 +1,21 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.github.qauxv.loader.hookapi"
|
||||
compileSdk = Version.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Version.minSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.androidx.annotation)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package io.github.qauxv.loader.hookapi;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@Keep
|
||||
public interface IHookBridge {
|
||||
|
||||
/**
|
||||
* The default hook priority.
|
||||
*/
|
||||
int PRIORITY_DEFAULT = 50;
|
||||
|
||||
/**
|
||||
* Execute the hook callback late.
|
||||
*/
|
||||
int PRIORITY_LOWEST = -10000;
|
||||
|
||||
/**
|
||||
* Execute the hook callback early.
|
||||
*/
|
||||
int PRIORITY_HIGHEST = 10000;
|
||||
|
||||
interface IMemberHookCallback {
|
||||
|
||||
void beforeHookedMember(@NonNull IMemberHookParam param) throws Throwable;
|
||||
|
||||
void afterHookedMember(@NonNull IMemberHookParam param) throws Throwable;
|
||||
|
||||
}
|
||||
|
||||
interface IMemberHookParam {
|
||||
|
||||
/**
|
||||
* Gets the method or constructor being hooked.
|
||||
*
|
||||
* @return The method or constructor
|
||||
*/
|
||||
@NonNull
|
||||
Member getMember();
|
||||
|
||||
/**
|
||||
* Gets the `this` reference for an instance method, or null for a static method or constructor.
|
||||
*
|
||||
* @return The `this` reference
|
||||
*/
|
||||
@NonNull
|
||||
Object getThisObject();
|
||||
|
||||
/**
|
||||
* Gets the arguments passed to the method or constructor. You may modify the arguments.
|
||||
*
|
||||
* @return The arguments
|
||||
*/
|
||||
@NonNull
|
||||
Object[] getArgs();
|
||||
|
||||
/**
|
||||
* Gets the return value of the method or constructor.
|
||||
*
|
||||
* @return The return value
|
||||
*/
|
||||
@Nullable
|
||||
Object getResult();
|
||||
|
||||
/**
|
||||
* Sets the return value of the method or constructor.
|
||||
* If called in beforeHookedMember, the original method or constructor will not be called.
|
||||
*
|
||||
* @param result The new return value
|
||||
*/
|
||||
void setResult(@Nullable Object result);
|
||||
|
||||
/**
|
||||
* Gets the throwable thrown by the method or constructor, or null if it didn't throw anything.
|
||||
*
|
||||
* @return The throwable
|
||||
*/
|
||||
@Nullable
|
||||
Throwable getThrowable();
|
||||
|
||||
/**
|
||||
* Sets the throwable to be thrown by the method or constructor.
|
||||
* If called in beforeHookedMember, the original method or constructor will not be called.
|
||||
*
|
||||
* @param throwable The throwable to throw
|
||||
*/
|
||||
void setThrowable(@NonNull Throwable throwable);
|
||||
|
||||
/**
|
||||
* Get the extra data for the current IMemberHookParam.
|
||||
* The IMemberHookParam lifecycle is the same as the hooked member invocation.
|
||||
* That is one IMemberHookParam instance per hooked member invocation.
|
||||
* Any data can be stored here.
|
||||
*
|
||||
* @return The extra data
|
||||
*/
|
||||
@Nullable
|
||||
Object getExtra();
|
||||
|
||||
/**
|
||||
* Set the extra data for the current IMemberHookParam.
|
||||
*
|
||||
* @param extra The extra data
|
||||
*/
|
||||
void setExtra(@Nullable Object extra);
|
||||
|
||||
}
|
||||
|
||||
interface MemberUnhookHandle {
|
||||
|
||||
/**
|
||||
* Gets the method or constructor being hooked.
|
||||
*
|
||||
* @return The method or constructor
|
||||
*/
|
||||
@NonNull
|
||||
Member getMember();
|
||||
|
||||
/**
|
||||
* Gets the callback for the member.
|
||||
*
|
||||
* @return The callback
|
||||
*/
|
||||
@NonNull
|
||||
IMemberHookCallback getCallback();
|
||||
|
||||
/**
|
||||
* Checks if the hook for the member is still active.
|
||||
*
|
||||
* @return True if the hook is still active, false otherwise
|
||||
*/
|
||||
boolean isHookActive();
|
||||
|
||||
/**
|
||||
* Removes the hook for the member.
|
||||
*/
|
||||
void unhook();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the API level of the current implementation. eg, 51-100
|
||||
*
|
||||
* @return API level
|
||||
*/
|
||||
int getApiLevel();
|
||||
|
||||
/**
|
||||
* Gets the Xposed framework name of current implementation.
|
||||
*
|
||||
* @return Framework name
|
||||
*/
|
||||
@NonNull
|
||||
String getFrameworkName();
|
||||
|
||||
/**
|
||||
* Gets the Xposed framework version of current implementation.
|
||||
*
|
||||
* @return Framework version
|
||||
*/
|
||||
@NonNull
|
||||
String getFrameworkVersion();
|
||||
|
||||
/**
|
||||
* Gets the Xposed framework version code of current implementation.
|
||||
*
|
||||
* @return Framework version code
|
||||
*/
|
||||
long getFrameworkVersionCode();
|
||||
|
||||
/**
|
||||
* Hook a method or constructor.
|
||||
* A member can be hooked multiple times with different callbacks.
|
||||
* If hook fails, it will throw an exception.
|
||||
*
|
||||
* @param member The method or constructor to hook
|
||||
* @param callback The callback to be invoked
|
||||
* @param priority The priority of the callback
|
||||
* @return A handle that can be used to unhook the method or constructor
|
||||
* @throws IllegalArgumentException if origin is abstract, framework internal or {@link Method#invoke} or hooker is invalid
|
||||
* @throws RuntimeException if something goes wrong
|
||||
*/
|
||||
@NonNull
|
||||
MemberUnhookHandle hookMethod(@NonNull Member member, @NonNull IMemberHookCallback callback, int priority);
|
||||
|
||||
/**
|
||||
* Check if the current implementation supports optimization.
|
||||
*
|
||||
* @return true if deoptimization is supported, false otherwise
|
||||
*/
|
||||
boolean isDeoptimizationSupported();
|
||||
|
||||
/**
|
||||
* Deoptimize the specified method or constructor.
|
||||
* <p>
|
||||
* Deoptimization is an optional feature that only a few implementations support. It is used to
|
||||
* undo the effects of optimization, which can be useful for hooking an inlined method or constructor.
|
||||
*
|
||||
* @param member The method or constructor to deoptimize
|
||||
* @return true if the method or constructor was deoptimized or if it was already deoptimized, false otherwise
|
||||
*/
|
||||
boolean deoptimize(@NonNull Member member);
|
||||
|
||||
/**
|
||||
* Query the extension of the current implementation.
|
||||
*
|
||||
* @param key The key of the extension
|
||||
* @param args The arguments for the extension, may be empty
|
||||
* @return The result of the extension, may be null
|
||||
*/
|
||||
@Nullable
|
||||
Object queryExtension(@NonNull String key, @Nullable Object... args);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.github.qauxv.loader.hookapi;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@Keep
|
||||
public interface ILoaderInfo {
|
||||
|
||||
@NonNull
|
||||
String getEntryPointName();
|
||||
|
||||
@NonNull
|
||||
String getLoaderVersionName();
|
||||
|
||||
int getLoaderVersionCode();
|
||||
|
||||
/**
|
||||
* Get the main module path (loaded target module path).
|
||||
*
|
||||
* @return The main module path
|
||||
*/
|
||||
@NonNull
|
||||
String getMainModulePath();
|
||||
|
||||
void log(@NonNull String msg);
|
||||
|
||||
void log(@NonNull Throwable tr);
|
||||
|
||||
}
|
||||
33
loader/sbl/build.gradle.kts
Normal file
33
loader/sbl/build.gradle.kts
Normal file
@@ -0,0 +1,33 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.github.qauxv.loader.sbl"
|
||||
compileSdk = Version.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Version.minSdk
|
||||
|
||||
buildConfigField("String", "VERSION_NAME", "\"${Common.getBuildVersionName(rootProject)}\"")
|
||||
buildConfigField("int", "VERSION_CODE", "${Common.getBuildVersionCode(rootProject)}")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Xposed API 89
|
||||
compileOnly(libs.xposed.api)
|
||||
// LSPosed API 100
|
||||
// compileOnly(libs.libxposed.api)
|
||||
compileOnly(libs.androidx.annotation)
|
||||
implementation(projects.loader.hookapi)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2024 QAuxiliary developers
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is an opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the General Public License
|
||||
* along with this software.
|
||||
* If not, see
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
|
||||
package io.github.qauxv.loader.sbl.common;
|
||||
|
||||
public class CheckUtils {
|
||||
|
||||
private CheckUtils() {
|
||||
throw new UnsupportedOperationException("No instances");
|
||||
}
|
||||
|
||||
public static void checkNonNull(Object obj, String message) {
|
||||
if (obj == null) {
|
||||
throw new NullPointerException(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package io.github.qauxv.loader.sbl.common;
|
||||
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import dalvik.system.BaseDexClassLoader;
|
||||
import io.github.qauxv.loader.hookapi.IHookBridge;
|
||||
import io.github.qauxv.loader.hookapi.ILoaderInfo;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ModuleLoader {
|
||||
|
||||
private ModuleLoader() {
|
||||
}
|
||||
|
||||
private static boolean sLoaded = false;
|
||||
private static final ArrayList<Throwable> sInitErrors = new ArrayList<>(1);
|
||||
|
||||
@Nullable
|
||||
public static String findTargetModulePath(@NonNull ApplicationInfo ai) {
|
||||
// TODO: 2024-07-21 implement this method
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ClassLoader createTargetClassLoader(@NonNull File path, @NonNull ApplicationInfo ai) {
|
||||
ClassLoader parent = new TransitClassLoader();
|
||||
String dataDirPath = ai.dataDir;
|
||||
File dataDir = new File(dataDirPath);
|
||||
if (!dataDir.canWrite()) {
|
||||
sInitErrors.add(new IOException("createTargetClassLoader: dataDir is not writable: " + dataDirPath));
|
||||
return null;
|
||||
}
|
||||
// create odex directory if sdk < 26
|
||||
File odexDir;
|
||||
if (android.os.Build.VERSION.SDK_INT < 26) {
|
||||
odexDir = new File(dataDir, "app_odex");
|
||||
if (!odexDir.exists() && !odexDir.mkdirs()) {
|
||||
sInitErrors.add(new IOException("createTargetClassLoader: failed to create odexDir: " + odexDir));
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// optimizedDirectory – this parameter is deprecated and has no effect since API level 26.
|
||||
odexDir = null;
|
||||
}
|
||||
// create new class loader
|
||||
ClassLoader cl = new BaseDexClassLoader(path.getAbsolutePath(), odexDir, null, parent);
|
||||
return cl;
|
||||
}
|
||||
|
||||
public static void initialize(
|
||||
@NonNull ApplicationInfo ai,
|
||||
@NonNull ClassLoader hostClassLoader,
|
||||
@NonNull ILoaderInfo loaderInfo,
|
||||
@Nullable IHookBridge hookBridge,
|
||||
@NonNull String selfPath
|
||||
) throws ReflectiveOperationException {
|
||||
if (sLoaded) {
|
||||
return;
|
||||
}
|
||||
File targetModule = null;
|
||||
boolean useDynamicLoad = false;
|
||||
try {
|
||||
String path = findTargetModulePath(ai);
|
||||
if (path != null) {
|
||||
targetModule = new File(path);
|
||||
}
|
||||
} catch (Exception | Error e) {
|
||||
sInitErrors.add(e);
|
||||
android.util.Log.e("QAuxv", "initialize: findTargetModulePath failed", e);
|
||||
}
|
||||
if (targetModule != null && targetModule.isFile() && !targetModule.canWrite()) {
|
||||
// ART requires W^X since Android 14
|
||||
useDynamicLoad = true;
|
||||
}
|
||||
ClassLoader targetClassLoader = null;
|
||||
try {
|
||||
if (useDynamicLoad) {
|
||||
targetClassLoader = createTargetClassLoader(targetModule, ai);
|
||||
}
|
||||
} catch (Exception | Error e) {
|
||||
sInitErrors.add(e);
|
||||
android.util.Log.e("QAuxv", "initialize: createTargetClassLoader failed", e);
|
||||
}
|
||||
// if we failed to create targetClassLoader, fallback to normal startup
|
||||
String modulePath;
|
||||
if (targetClassLoader == null) {
|
||||
targetClassLoader = ModuleLoader.class.getClassLoader();
|
||||
modulePath = selfPath;
|
||||
} else {
|
||||
modulePath = targetModule.getAbsolutePath();
|
||||
}
|
||||
assert targetClassLoader != null;
|
||||
// invoke the startup routine
|
||||
Class<?> kUnifiedEntryPoint = targetClassLoader.loadClass("io.github.qauxv.startup.UnifiedEntryPoint");
|
||||
Method initialize = kUnifiedEntryPoint.getMethod("entry",
|
||||
String.class, ApplicationInfo.class, ILoaderInfo.class, ClassLoader.class, IHookBridge.class);
|
||||
sLoaded = true;
|
||||
initialize.invoke(null, modulePath, ai, loaderInfo, hostClassLoader, hookBridge);
|
||||
}
|
||||
|
||||
public static List<Throwable> getInitErrors() {
|
||||
return sInitErrors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.github.qauxv.loader.sbl.common;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class TransitClassLoader extends ClassLoader {
|
||||
|
||||
private static final ClassLoader sSystem = Context.class.getClassLoader();
|
||||
private static final ClassLoader sCurrent = TransitClassLoader.class.getClassLoader();
|
||||
|
||||
public TransitClassLoader() {
|
||||
super(sSystem);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
if (name != null && name.startsWith("io.github.qauxv.loader.")) {
|
||||
return sCurrent.loadClass(name);
|
||||
}
|
||||
return super.findClass(name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.github.qauxv.loader.sbl.lsp100;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
/**
|
||||
* Entry point for libxpsoed API 100 (typically LSPosed).
|
||||
* <p>
|
||||
* The libxpsoed API is used as ART hook implementation.
|
||||
*/
|
||||
@Keep
|
||||
public class Lsp100HookEntry {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.github.qauxv.loader.sbl.rti6t;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
/**
|
||||
* Entry point for runtime injection.
|
||||
* <p>
|
||||
* No hook provider is available in this way.
|
||||
*/
|
||||
@Keep
|
||||
public class RtInjectEntry {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.github.qauxv.loader.sbl.xp51;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import io.github.qauxv.loader.sbl.common.CheckUtils;
|
||||
import io.github.qauxv.loader.sbl.common.ModuleLoader;
|
||||
|
||||
public class Xp51ExtCmd {
|
||||
|
||||
private Xp51ExtCmd() {
|
||||
}
|
||||
|
||||
public static Object handleQueryExtension(@NonNull String cmd, @Nullable Object[] arg) {
|
||||
CheckUtils.checkNonNull(cmd, "cmd");
|
||||
switch (cmd) {
|
||||
case "GetXposedBridgeClass":
|
||||
return XposedBridge.class;
|
||||
case "GetLoadPackageParam":
|
||||
return Xp51HookEntry.getLoadPackageParam();
|
||||
case "GetInitZygoteStartupParam":
|
||||
return Xp51HookEntry.getInitZygoteStartupParam();
|
||||
case "GetInitErrors":
|
||||
return ModuleLoader.getInitErrors();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,41 +1,23 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2022 qwq233@qwq2333.top
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is non-free but opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the GNU Affero General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version and our eula as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* and eula along with this software. If not, see
|
||||
* <https://www.gnu.org/licenses/>
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
package io.github.qauxv.startup;
|
||||
package io.github.qauxv.loader.sbl.xp51;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import de.robv.android.xposed.IXposedHookLoadPackage;
|
||||
import de.robv.android.xposed.IXposedHookZygoteInit;
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
import io.github.qauxv.R;
|
||||
import io.github.qauxv.util.hookstatus.HookStatusInit;
|
||||
import io.github.qauxv.loader.sbl.common.ModuleLoader;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Xposed entry class DO NOT MODIFY ANY CODE HERE UNLESS NECESSARY. DO NOT INVOKE ANY METHOD THAT MAY GET IN TOUCH WITH KOTLIN HERE. DO NOT TOUCH ANDROIDX OR
|
||||
* KOTLIN HERE, WHATEVER DIRECTLY OR INDIRECTLY. THIS CLASS SHOULD ONLY CALL {@code StartupHook.getInstance().doInit()} AND RETURN GRACEFULLY. OTHERWISE
|
||||
* SOMETHING MAY HAPPEN BECAUSE OF A NON-STANDARD PLUGIN CLASSLOADER.
|
||||
*
|
||||
* @author kinit
|
||||
* Entry point for started Xposed API 51-99.
|
||||
* <p>
|
||||
* Xposed is used as ART hook implementation.
|
||||
*/
|
||||
public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
@Keep
|
||||
public class Xp51HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
|
||||
public static final String PACKAGE_NAME_QQ = "com.tencent.mobileqq";
|
||||
public static final String PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi";
|
||||
@@ -54,23 +36,17 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
|
||||
/**
|
||||
* *** No kotlin code should be invoked here.*** May cause a crash.
|
||||
*/
|
||||
@Keep
|
||||
@Override
|
||||
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
|
||||
if (R.string.res_inject_success >>> 24 == 0x7f) {
|
||||
XposedBridge.log("package id must NOT be 0x7f, reject loading...");
|
||||
return;
|
||||
}
|
||||
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws ReflectiveOperationException {
|
||||
sLoadPackageParam = lpparam;
|
||||
// check LSPosed dex-obfuscation
|
||||
Class<?> kXposedBridge = XposedBridge.class;
|
||||
if (!"de.robv.android.xposed.XposedBridge".equals(kXposedBridge.getName())) {
|
||||
String className = kXposedBridge.getName();
|
||||
String pkgName = className.substring(0, className.lastIndexOf('.'));
|
||||
HybridClassLoader.setObfuscatedXposedApiPackage(pkgName);
|
||||
}
|
||||
switch (lpparam.packageName) {
|
||||
case PACKAGE_NAME_SELF: {
|
||||
HookStatusInit.init(lpparam.classLoader);
|
||||
Class<?> kHookStatusInit = Class.forName("io.github.qauxv.util.hookstatus.HookStatusInit");
|
||||
Method init = kHookStatusInit.getDeclaredMethod("init", ClassLoader.class);
|
||||
init.invoke(null, lpparam.classLoader);
|
||||
break;
|
||||
}
|
||||
case PACKAGE_NAME_TIM:
|
||||
@@ -81,7 +57,8 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
|
||||
throw new IllegalStateException("handleLoadPackage: sInitZygoteStartupParam is null");
|
||||
}
|
||||
sCurrentPackageName = lpparam.packageName;
|
||||
StartupHook.getInstance().initialize(lpparam.classLoader);
|
||||
ModuleLoader.initialize(lpparam.appInfo, lpparam.classLoader,
|
||||
Xp51HookImpl.INSTANCE, Xp51HookImpl.INSTANCE, getModulePath());
|
||||
break;
|
||||
}
|
||||
case PACKAGE_NAME_QQ_INTERNATIONAL: {
|
||||
@@ -104,8 +81,6 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
|
||||
|
||||
/**
|
||||
* Get the {@link XC_LoadPackage.LoadPackageParam} of the current module.
|
||||
* <p>
|
||||
* Do NOT add @NonNull annotation to this method. *** No kotlin code should be invoked here.*** May cause a crash.
|
||||
*
|
||||
* @return the lpparam
|
||||
*/
|
||||
@@ -118,8 +93,6 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
|
||||
|
||||
/**
|
||||
* Get the path of the current module.
|
||||
* <p>
|
||||
* Do NOT add @NonNull annotation to this method. *** No kotlin code should be invoked here.*** May cause a crash.
|
||||
*
|
||||
* @return the module path
|
||||
*/
|
||||
@@ -132,8 +105,6 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
|
||||
|
||||
/**
|
||||
* Get the {@link IXposedHookZygoteInit.StartupParam} of the current module.
|
||||
* <p>
|
||||
* Do NOT add @NonNull annotation to this method. *** No kotlin code should be invoked here.*** May cause a crash.
|
||||
*
|
||||
* @return the initZygote param
|
||||
*/
|
||||
@@ -143,4 +114,5 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
|
||||
}
|
||||
return sInitZygoteStartupParam;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2024 QAuxiliary developers
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is an opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the General Public License
|
||||
* along with this software.
|
||||
* If not, see
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
|
||||
package io.github.qauxv.loader.sbl.xp51;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import io.github.qauxv.loader.hookapi.IHookBridge;
|
||||
import io.github.qauxv.loader.hookapi.ILoaderInfo;
|
||||
import io.github.qauxv.loader.sbl.common.CheckUtils;
|
||||
import java.lang.reflect.Member;
|
||||
|
||||
public class Xp51HookImpl implements IHookBridge, ILoaderInfo {
|
||||
|
||||
public static final Xp51HookImpl INSTANCE = new Xp51HookImpl();
|
||||
|
||||
@Override
|
||||
public int getApiLevel() {
|
||||
return XposedBridge.getXposedVersion();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getFrameworkName() {
|
||||
return "Xposed";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getFrameworkVersion() {
|
||||
return String.valueOf(XposedBridge.getXposedVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrameworkVersionCode() {
|
||||
return XposedBridge.getXposedVersion();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MemberUnhookHandle hookMethod(@NonNull Member member, @NonNull IMemberHookCallback callback, int priority) {
|
||||
CheckUtils.checkNonNull(member, "member");
|
||||
CheckUtils.checkNonNull(callback, "callback");
|
||||
// check member is method or constructor
|
||||
if (!(member instanceof java.lang.reflect.Method) && !(member instanceof java.lang.reflect.Constructor)) {
|
||||
throw new IllegalArgumentException("member must be method or constructor");
|
||||
}
|
||||
Xp51HookWrapper.Xp51HookCallback cb = new Xp51HookWrapper.Xp51HookCallback(callback, priority);
|
||||
XC_MethodHook.Unhook unhook = XposedBridge.hookMethod(member, cb);
|
||||
if (unhook == null) {
|
||||
throw new UnsupportedOperationException("XposedBridge.hookMethod return null for member: " + member);
|
||||
}
|
||||
return new Xp51HookWrapper.Xp51UnhookHandle(unhook, member, cb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeoptimizationSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deoptimize(@NonNull Member member) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object queryExtension(@NonNull String key, @Nullable Object... args) {
|
||||
return Xp51ExtCmd.handleQueryExtension(key, args);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getEntryPointName() {
|
||||
return "Xp51HookEntry";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getLoaderVersionName() {
|
||||
return io.github.qauxv.loader.sbl.BuildConfig.VERSION_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLoaderVersionCode() {
|
||||
return io.github.qauxv.loader.sbl.BuildConfig.VERSION_CODE;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getMainModulePath() {
|
||||
return Xp51HookEntry.getModulePath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NonNull String msg) {
|
||||
if (TextUtils.isEmpty(msg)) {
|
||||
return;
|
||||
}
|
||||
XposedBridge.log(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NonNull Throwable tr) {
|
||||
XposedBridge.log(tr);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* QAuxiliary - An Xposed module for QQ/TIM
|
||||
* Copyright (C) 2019-2024 QAuxiliary developers
|
||||
* https://github.com/cinit/QAuxiliary
|
||||
*
|
||||
* This software is an opensource software: you can redistribute it
|
||||
* and/or modify it under the terms of the General Public License
|
||||
* as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version as published
|
||||
* by QAuxiliary contributors.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the General Public License
|
||||
* along with this software.
|
||||
* If not, see
|
||||
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
|
||||
*/
|
||||
|
||||
package io.github.qauxv.loader.sbl.xp51;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import io.github.qauxv.loader.hookapi.IHookBridge;
|
||||
import java.lang.reflect.Member;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class Xp51HookWrapper {
|
||||
|
||||
private static final AtomicLong sNextHookId = new AtomicLong(1);
|
||||
private static final String TAG_PREFIX = "qauxv_hcb_";
|
||||
|
||||
public static class Xp51HookParam implements IHookBridge.IMemberHookParam {
|
||||
|
||||
private XC_MethodHook.MethodHookParam mParam;
|
||||
private Object mExtra;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Member getMember() {
|
||||
return mParam.method;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object getThisObject() {
|
||||
return mParam.thisObject;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object[] getArgs() {
|
||||
return mParam.args;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getResult() {
|
||||
return mParam.getResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResult(@Nullable Object result) {
|
||||
mParam.setResult(result);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Throwable getThrowable() {
|
||||
return mParam.getThrowable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThrowable(@NonNull Throwable throwable) {
|
||||
mParam.setThrowable(throwable);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getExtra() {
|
||||
return mExtra;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExtra(@Nullable Object extra) {
|
||||
mExtra = extra;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Xp51HookCallback extends XC_MethodHook {
|
||||
|
||||
private final IHookBridge.IMemberHookCallback mCallback;
|
||||
private final long mHookId = sNextHookId.getAndIncrement();
|
||||
private boolean mAlive = true;
|
||||
|
||||
public Xp51HookCallback(@NonNull IHookBridge.IMemberHookCallback c, int priority) {
|
||||
super(priority);
|
||||
mCallback = c;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
|
||||
if (!mAlive) {
|
||||
return;
|
||||
}
|
||||
String tag = TAG_PREFIX + mHookId;
|
||||
Xp51HookParam hcbParam = new Xp51HookParam();
|
||||
hcbParam.mParam = param;
|
||||
param.setObjectExtra(tag, hcbParam);
|
||||
mCallback.beforeHookedMember(hcbParam);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
|
||||
if (!mAlive) {
|
||||
return;
|
||||
}
|
||||
String tag = TAG_PREFIX + mHookId;
|
||||
Xp51HookParam hcbParam = (Xp51HookParam) param.getObjectExtra(tag);
|
||||
if (hcbParam == null) {
|
||||
throw new AssertionError("hcbParam is null, tag: " + tag);
|
||||
}
|
||||
mCallback.afterHookedMember(hcbParam);
|
||||
// for gc
|
||||
param.setObjectExtra(tag, null);
|
||||
hcbParam.mParam = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Xp51UnhookHandle implements IHookBridge.MemberUnhookHandle {
|
||||
|
||||
private final XC_MethodHook.Unhook mUnhook;
|
||||
private final Xp51HookCallback mCallback;
|
||||
private final Member mMember;
|
||||
|
||||
public Xp51UnhookHandle(@NonNull XC_MethodHook.Unhook unhook, @NonNull Member member, @NonNull Xp51HookCallback callback) {
|
||||
mUnhook = unhook;
|
||||
mMember = member;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Member getMember() {
|
||||
return mMember;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public IHookBridge.IMemberHookCallback getCallback() {
|
||||
return mCallback.mCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHookActive() {
|
||||
return mCallback.mAlive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unhook() {
|
||||
mUnhook.unhook();
|
||||
mCallback.mAlive = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
22
loader/startup/build.gradle.kts
Normal file
22
loader/startup/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.github.qauxv.startup"
|
||||
compileSdk = Version.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Version.minSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.androidx.annotation)
|
||||
compileOnly(projects.loader.hookapi)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
|
||||
package io.github.qauxv.startup;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class HybridClassLoader extends ClassLoader {
|
||||
|
||||
private static final ClassLoader sBootClassLoader = Context.class.getClassLoader();
|
||||
|
||||
public static final HybridClassLoader INSTANCE = new HybridClassLoader();
|
||||
|
||||
private HybridClassLoader() {
|
||||
super(sBootClassLoader);
|
||||
}
|
||||
|
||||
private static ClassLoader sLoaderParentClassLoader;
|
||||
private static ClassLoader sHostClassLoader;
|
||||
|
||||
public static void setLoaderParentClassLoader(ClassLoader loaderClassLoader) {
|
||||
if (loaderClassLoader == HybridClassLoader.class.getClassLoader()) {
|
||||
sLoaderParentClassLoader = null;
|
||||
} else {
|
||||
sLoaderParentClassLoader = loaderClassLoader;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setHostClassLoader(ClassLoader hostClassLoader) {
|
||||
sHostClassLoader = hostClassLoader;
|
||||
}
|
||||
|
||||
public static ClassLoader getHostClassLoader() {
|
||||
return sHostClassLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (name == null) {
|
||||
return super.loadClass(null, resolve);
|
||||
}
|
||||
try {
|
||||
return sBootClassLoader.loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
if (sLoaderParentClassLoader != null && name.startsWith("io.github.qauxv.loader.")) {
|
||||
return sLoaderParentClassLoader.loadClass(name);
|
||||
}
|
||||
if (isConflictingClass(name)) {
|
||||
// Nevertheless, this will not interfere with the host application,
|
||||
// classes in host application SHOULD find with their own ClassLoader, eg Class.forName()
|
||||
// use shipped androidx and kotlin lib.
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
// The ClassLoader for some apk-modifying frameworks are terrible, XposedBridge.class.getClassLoader()
|
||||
// is the sane as Context.getClassLoader(), which mess up with 3rd lib, can cause the ART to crash.
|
||||
if (sLoaderParentClassLoader != null) {
|
||||
try {
|
||||
return sLoaderParentClassLoader.loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
if (sHostClassLoader != null) {
|
||||
try {
|
||||
return sHostClassLoader.loadClass(name);
|
||||
} catch (ClassNotFoundException e) {
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
}
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把宿主和模块共有的 package 扔这里.
|
||||
*
|
||||
* @param name NonNull, class name
|
||||
* @return true if conflicting
|
||||
*/
|
||||
public static boolean isConflictingClass(String name) {
|
||||
return name.startsWith("androidx.") || name.startsWith("android.support.")
|
||||
|| name.startsWith("kotlin.") || name.startsWith("kotlinx.")
|
||||
|| name.startsWith("com.tencent.mmkv.")
|
||||
|| name.startsWith("com.android.tools.r8.")
|
||||
|| name.startsWith("com.google.android.")
|
||||
|| name.startsWith("com.google.gson.")
|
||||
|| name.startsWith("com.google.common.")
|
||||
|| name.startsWith("com.google.protobuf.")
|
||||
|| name.startsWith("com.microsoft.appcenter.")
|
||||
|| name.startsWith("org.intellij.lang.annotations.")
|
||||
|| name.startsWith("org.jetbrains.annotations.")
|
||||
|| name.startsWith("com.bumptech.glide.")
|
||||
|| name.startsWith("com.google.errorprone.annotations.")
|
||||
|| name.startsWith("_COROUTINE.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package io.github.qauxv.startup;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import io.github.qauxv.loader.hookapi.IHookBridge;
|
||||
import io.github.qauxv.loader.hookapi.ILoaderInfo;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
@Keep
|
||||
public class UnifiedEntryPoint {
|
||||
|
||||
private static boolean sInitialized = false;
|
||||
|
||||
private UnifiedEntryPoint() {
|
||||
}
|
||||
|
||||
@Keep
|
||||
public static void entry(
|
||||
@NonNull String modulePath,
|
||||
@NonNull ApplicationInfo appInfo,
|
||||
@NonNull ILoaderInfo loaderInfo,
|
||||
@NonNull ClassLoader hostClassLoader,
|
||||
@Nullable IHookBridge hookBridge
|
||||
) {
|
||||
if (sInitialized) {
|
||||
return;
|
||||
}
|
||||
sInitialized = true;
|
||||
// fix up the class loader
|
||||
HybridClassLoader loader = HybridClassLoader.INSTANCE;
|
||||
ClassLoader self = UnifiedEntryPoint.class.getClassLoader();
|
||||
assert self != null;
|
||||
ClassLoader parent = self.getParent();
|
||||
HybridClassLoader.setLoaderParentClassLoader(parent);
|
||||
injectClassLoader(self, loader);
|
||||
callNextStep(modulePath, appInfo, loaderInfo, hostClassLoader, hookBridge);
|
||||
}
|
||||
|
||||
private static void callNextStep(
|
||||
@NonNull String modulePath,
|
||||
@NonNull ApplicationInfo appInfo,
|
||||
@NonNull ILoaderInfo loaderInfo,
|
||||
@NonNull ClassLoader hostClassLoader,
|
||||
@Nullable IHookBridge hookBridge
|
||||
) {
|
||||
try {
|
||||
Class<?> kStartupAgent = Class.forName("io.github.qauxv.poststartup.StartupAgent");
|
||||
kStartupAgent.getMethod("startup", String.class, ApplicationInfo.class, ILoaderInfo.class, ClassLoader.class, IHookBridge.class)
|
||||
.invoke(null, modulePath, appInfo, loaderInfo, hostClassLoader, hookBridge);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
android.util.Log.e("QAuxv", "StartupAgent.startup: failed", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("JavaReflectionMemberAccess")
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
private static void injectClassLoader(ClassLoader self, ClassLoader newParent) {
|
||||
try {
|
||||
Field fParent = ClassLoader.class.getDeclaredField("parent");
|
||||
fParent.setAccessible(true);
|
||||
fParent.set(self, newParent);
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("QAuxv", "injectClassLoader: failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,6 +60,9 @@ develocity {
|
||||
rootProject.name = "QAuxiliary"
|
||||
include(
|
||||
":app",
|
||||
":loader:startup",
|
||||
":loader:sbl",
|
||||
":loader:hookapi",
|
||||
":libs:stub",
|
||||
":libs:ksp",
|
||||
":libs:mmkv",
|
||||
|
||||
Reference in New Issue
Block a user