refactor: module startup loading logic

EXPERIMENTAL: WIP: use unified entry point
This commit is contained in:
ACh Sulfate
2024-07-21 21:31:59 +08:00
parent c3d9cc26b4
commit 35041ad812
43 changed files with 4260 additions and 344 deletions

View File

@@ -251,12 +251,20 @@ kotlin {
} }
dependencies { 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) compileOnly(projects.libs.stub)
// libraries
implementation(projects.libs.mmkv) implementation(projects.libs.mmkv)
implementation(projects.libs.dexkit) implementation(projects.libs.dexkit)
implementation(projects.libs.xView) implementation(projects.libs.xView)
ksp(projects.libs.ksp)
compileOnly(libs.xposed)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)

View File

@@ -1 +1 @@
io.github.qauxv.startup.HookEntry io.github.qauxv.loader.sbl.xp51.Xp51HookEntry

View File

@@ -56,7 +56,7 @@ import io.github.qauxv.fragment.AboutFragment;
import io.github.qauxv.fragment.CheckAbiVariantFragment; import io.github.qauxv.fragment.CheckAbiVariantFragment;
import io.github.qauxv.fragment.CheckAbiVariantModel; import io.github.qauxv.fragment.CheckAbiVariantModel;
import io.github.qauxv.lifecycle.JumpActivityEntryHook; 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.SyncUtils;
import io.github.qauxv.util.Toasts; import io.github.qauxv.util.Toasts;
import io.github.qauxv.util.UiThread; import io.github.qauxv.util.UiThread;
@@ -165,11 +165,11 @@ public class ConfigV2Activity extends AppCompatTransferActivity {
String pkg = null; String pkg = null;
var id = view.getId(); var id = view.getId();
if (id == R.id.mainRelativeLayoutButtonOpenQQ) { if (id == R.id.mainRelativeLayoutButtonOpenQQ) {
pkg = HookEntry.PACKAGE_NAME_QQ; pkg = PackageConstants.PACKAGE_NAME_QQ;
} else if (id == R.id.mainRelativeLayoutButtonOpenTIM) { } else if (id == R.id.mainRelativeLayoutButtonOpenTIM) {
pkg = HookEntry.PACKAGE_NAME_TIM; pkg = PackageConstants.PACKAGE_NAME_TIM;
} else if (id == R.id.mainRelativeLayoutButtonOpenQQLite) { } else if (id == R.id.mainRelativeLayoutButtonOpenQQLite) {
pkg = HookEntry.PACKAGE_NAME_QQ_LITE; pkg = PackageConstants.PACKAGE_NAME_QQ_LITE;
} }
if (pkg != null) { if (pkg != null) {
Intent intent = new Intent(); Intent intent = new Intent();
@@ -205,19 +205,19 @@ public class ConfigV2Activity extends AppCompatTransferActivity {
String pkg = null; String pkg = null;
switch (which) { switch (which) {
case 0: { case 0: {
pkg = HookEntry.PACKAGE_NAME_QQ; pkg = PackageConstants.PACKAGE_NAME_QQ;
break; break;
} }
case 1: { case 1: {
pkg = HookEntry.PACKAGE_NAME_TIM; pkg = PackageConstants.PACKAGE_NAME_TIM;
break; break;
} }
case 2: { case 2: {
pkg = HookEntry.PACKAGE_NAME_QQ_LITE; pkg = PackageConstants.PACKAGE_NAME_QQ_LITE;
break; break;
} }
case 3: { case 3: {
pkg = HookEntry.PACKAGE_NAME_QQ_HD; pkg = PackageConstants.PACKAGE_NAME_QQ_HD;
break; break;
} }
default: { default: {

View File

@@ -22,7 +22,7 @@
package io.github.qauxv.config; package io.github.qauxv.config;
import android.os.Environment; 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.HostInfo;
import io.github.qauxv.util.Log; import io.github.qauxv.util.Log;
import java.io.File; import java.io.File;
@@ -44,7 +44,7 @@ public class SafeModeManager {
} }
INSTANCE.mSafeModeEnableFile = new File( INSTANCE.mSafeModeEnableFile = new File(
Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" +
HookEntry.sCurrentPackageName + "/" + SAFE_MODE_FILE_NAME HostInfo.getHostInfo().getPackageName() + "/" + SAFE_MODE_FILE_NAME
); );
return INSTANCE; return INSTANCE;
} }
@@ -73,10 +73,6 @@ public class SafeModeManager {
if (!isAvailable()) { if (!isAvailable()) {
return false; 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) { if (isEnable) {
try { try {
boolean isCreated = mSafeModeEnableFile.createNewFile(); boolean isCreated = mSafeModeEnableFile.createNewFile();

View File

@@ -72,7 +72,7 @@ import io.github.qauxv.dsl.item.CategoryItem
import io.github.qauxv.dsl.item.DslTMsgListItemInflatable import io.github.qauxv.dsl.item.DslTMsgListItemInflatable
import io.github.qauxv.dsl.item.TextSwitchItem import io.github.qauxv.dsl.item.TextSwitchItem
import io.github.qauxv.lifecycle.ActProxyMgr 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.startup.HybridClassLoader
import io.github.qauxv.tlb.ConfigTable.cacheMap import io.github.qauxv.tlb.ConfigTable.cacheMap
import io.github.qauxv.ui.CustomDialog import io.github.qauxv.ui.CustomDialog
@@ -176,8 +176,7 @@ class TroubleshootFragment : BaseRootLayoutFragment() {
", UID: " + android.os.Process.myUid() + ", UID: " + android.os.Process.myUid() +
", " + (if (android.os.Process.is64Bit()) "64 bit" else "32 bit") + "\n" + ", " + (if (android.os.Process.is64Bit()) "64 bit" else "32 bit") + "\n" +
"Xposed API version: " + XposedBridge.getXposedVersion() + "\n" + "Xposed API version: " + XposedBridge.getXposedVersion() + "\n" +
HybridClassLoader.getXposedBridgeClassName() + "\n" + "module: " + StartupInfo.getModulePath() + "\n" +
"module: " + HookEntry.getModulePath() + "\n" +
"ctx.dataDir: " + hostInfo.application.dataDir "ctx.dataDir: " + hostInfo.application.dataDir
description(statusInfo, isTextSelectable = true) description(statusInfo, isTextSelectable = true)
description(generateDebugInfo(), isTextSelectable = true) description(generateDebugInfo(), isTextSelectable = true)

View File

@@ -53,7 +53,8 @@ import androidx.annotation.RequiresApi;
import cc.ioctl.util.HostInfo; import cc.ioctl.util.HostInfo;
import io.github.qauxv.R; import io.github.qauxv.R;
import io.github.qauxv.core.MainHook; 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.ui.WindowIsTranslucent;
import io.github.qauxv.util.Initiator; import io.github.qauxv.util.Initiator;
import io.github.qauxv.util.Log; import io.github.qauxv.util.Log;
@@ -113,7 +114,7 @@ public class Parasitics {
return; return;
} catch (Resources.NotFoundException ignored) { } catch (Resources.NotFoundException ignored) {
} }
String sModulePath = HookEntry.getModulePath(); String sModulePath = StartupInfo.getModulePath();
if (sModulePath == null) { if (sModulePath == null) {
throw new RuntimeException("get module path failed, loader=" + MainHook.class.getClassLoader()); throw new RuntimeException("get module path failed, loader=" + MainHook.class.getClassLoader());
} }

View 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;
}
}

View File

@@ -1,34 +1,36 @@
/* /*
* QAuxiliary - An Xposed module for QQ/TIM * 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 * https://github.com/cinit/QAuxiliary
* *
* This software is non-free but opensource software: you can redistribute it * This software is an opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License * and/or modify it under the terms of the General Public License
* as published by the Free Software Foundation; either * 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. * by QAuxiliary contributors.
* *
* This software is distributed in the hope that it will be useful, * This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* Affero General Public License for more details. * See the General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the General Public License
* and eula along with this software. If not, see * along with this software.
* <https://www.gnu.org/licenses/> * If not, see
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>. * <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.content.Context;
import android.os.Environment; import android.os.Environment;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import de.robv.android.xposed.XC_MethodHook; import androidx.annotation.NonNull;
import de.robv.android.xposed.XposedBridge; import io.github.qauxv.startup.HybridClassLoader;
import de.robv.android.xposed.XposedHelpers; 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.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@@ -38,9 +40,11 @@ import java.lang.reflect.Modifier;
import java.util.HashMap; 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 * Startup hook for QQ/TIM They should act differently according to the process they belong to.
* with them any more, enjoy it as long as possible. DO NOT INVOKE ANY METHOD THAT MAY GET IN TOUCH WITH KOTLIN HERE. DO * <p>
* NOT MODIFY ANY CODE HERE UNLESS NECESSARY. * 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 * @author cinit
*/ */
@@ -48,7 +52,6 @@ public class StartupHook {
private static StartupHook sInstance; private static StartupHook sInstance;
private static boolean sSecondStageInit = false; private static boolean sSecondStageInit = false;
private boolean mFirstStageInit = false;
private StartupHook() { private StartupHook() {
} }
@@ -65,44 +68,12 @@ public class StartupHook {
if (sSecondStageInit) { if (sSecondStageInit) {
return; return;
} }
ClassLoader classLoader = ctx.getClassLoader(); HybridClassLoader.setHostClassLoader(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);
StartupRoutine.execPostStartupInit(ctx, step, lpwReserved, bReserved); StartupRoutine.execPostStartupInit(ctx, step, lpwReserved, bReserved);
sSecondStageInit = true; sSecondStageInit = true;
deleteDirIfNecessaryNoThrow(ctx); 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) { static void deleteDirIfNecessaryNoThrow(Context ctx) {
try { try {
deleteFile(new File(ctx.getDataDir(), "app_qqprotect")); deleteFile(new File(ctx.getDataDir(), "app_qqprotect"));
@@ -147,58 +118,27 @@ public class StartupHook {
String msg = Log.getStackTraceString(th); String msg = Log.getStackTraceString(th);
Log.e("QAuxv", msg); Log.e("QAuxv", msg);
try { try {
XposedBridge.log(th); StartupInfo.getLoaderInfo().log(th);
} catch (NoClassDefFoundError e) { } catch (NoClassDefFoundError | NullPointerException e) {
Log.e("Xposed", msg); Log.e("Xposed", msg);
Log.e("EdXposed-Bridge", msg); Log.e("EdXposed-Bridge", msg);
} }
} }
private static void checkClassLoaderIsolation() { public void initializeAfterAppCreate(@NonNull Context ctx) {
Class<?> stub; execStartupInit(ctx, null, null, false);
try { applyTargetDpiIfNecessary(ctx);
stub = Class.forName("com.tencent.common.app.BaseApplicationImpl"); deleteDirIfNecessaryNoThrow(ctx);
} 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 initialize(ClassLoader rtLoader) throws Throwable { public void initializeBeforeAppCreate(@NonNull ClassLoader rtLoader) {
if (mFirstStageInit) {
return;
}
try { try {
XC_MethodHook startup = new XC_MethodHook(51) { XC_MethodHook startup = new XC_MethodHook(51) {
@Override @Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable { protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try { ClassLoader cl = param.thisObject.getClass().getClassLoader();
Context app; Context app = StartupAgent.getBaseApplicationImpl(cl);
Class<?> clz = param.thisObject.getClass().getClassLoader() execStartupInit(app, param.thisObject, null, false);
.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);
execStartupInit(app, param.thisObject, null, false);
} catch (Throwable e) {
log_e(e);
throw e;
}
} }
}; };
Class<?> loadDex = findLoadDexTaskClass(rtLoader); Class<?> loadDex = findLoadDexTaskClass(rtLoader);
@@ -218,7 +158,6 @@ public class StartupHook {
} }
} }
XposedBridge.hookMethod(m, startup); XposedBridge.hookMethod(m, startup);
mFirstStageInit = true;
} catch (Throwable e) { } catch (Throwable e) {
if ((e + "").contains("com.bug.zqq")) { if ((e + "").contains("com.bug.zqq")) {
return; return;
@@ -227,7 +166,7 @@ public class StartupHook {
return; return;
} }
log_e(e); log_e(e);
throw e; throw IoUtils.unsafeThrow(e);
} }
try { try {
XposedHelpers.findAndHookMethod(rtLoader.loadClass("com.tencent.mobileqq.qfix.QFixApplication"), XposedHelpers.findAndHookMethod(rtLoader.loadClass("com.tencent.mobileqq.qfix.QFixApplication"),

View File

@@ -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;
}
}

View File

@@ -1,27 +1,26 @@
/* /*
* QAuxiliary - An Xposed module for QQ/TIM * 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 * https://github.com/cinit/QAuxiliary
* *
* This software is non-free but opensource software: you can redistribute it * This software is an opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License * and/or modify it under the terms of the General Public License
* as published by the Free Software Foundation; either * 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. * by QAuxiliary contributors.
* *
* This software is distributed in the hope that it will be useful, * This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* Affero General Public License for more details. * See the General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the General Public License
* and eula along with this software. If not, see * along with this software.
* <https://www.gnu.org/licenses/> * If not, see
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>. * <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.app.Application;
import android.content.Context; import android.content.Context;
import android.os.Build; 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.Initiator;
import io.github.qauxv.util.Natives; import io.github.qauxv.util.Natives;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import org.lsposed.hiddenapibypass.HiddenApiBypass;
public class StartupRoutine { public class StartupRoutine {
@@ -51,10 +49,9 @@ public class StartupRoutine {
* @param bReserved false, not used * @param bReserved false, not used
*/ */
public static void execPostStartupInit(Context ctx, Object step, String lpwReserved, boolean bReserved) { public static void execPostStartupInit(Context ctx, Object step, String lpwReserved, boolean bReserved) {
ensureHiddenApiAccess();
// init all kotlin utils here // init all kotlin utils here
EzXHelperInit.INSTANCE.initZygote(HookEntry.getInitZygoteStartupParam()); EzXHelperInit.INSTANCE.setHostPackageName(ctx.getPackageName());
EzXHelperInit.INSTANCE.initHandleLoadPackage(HookEntry.getLoadPackageParam()); EzXHelperInit.INSTANCE.setEzClassLoader(ctx.getClassLoader());
// resource injection is done somewhere else, do not init it here // resource injection is done somewhere else, do not init it here
EzXHelperInit.INSTANCE.initAppContext(ctx, false, false); EzXHelperInit.INSTANCE.initAppContext(ctx, false, false);
EzXHelperInit.INSTANCE.setLogTag("QAuxv"); 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;
}
} }

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -30,8 +30,7 @@ import android.system.Os;
import android.system.StructUtsname; import android.system.StructUtsname;
import com.tencent.mmkv.MMKV; import com.tencent.mmkv.MMKV;
import io.github.qauxv.BuildConfig; import io.github.qauxv.BuildConfig;
import io.github.qauxv.startup.HookEntry; import io.github.qauxv.poststartup.StartupInfo;
import io.github.qauxv.startup.HybridClassLoader;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOError; import java.io.IOError;
@@ -198,10 +197,10 @@ public class Natives {
return; return;
} }
try { try {
Class<?> xp = Class.forName(HybridClassLoader.getXposedBridgeClassName()); Class<?> xp = Class.forName(LspObfuscationHelper.getXposedBridgeClassName());
try { try {
xp.getClassLoader() xp.getClassLoader()
.loadClass(HybridClassLoader.getObfuscatedLsposedNativeApiClassName()) .loadClass(LspObfuscationHelper.getObfuscatedLsposedNativeApiClassName())
.getMethod("recordNativeEntrypoint", String.class) .getMethod("recordNativeEntrypoint", String.class)
.invoke(null, soTailingName); .invoke(null, soTailingName);
} catch (ClassNotFoundException ignored) { } catch (ClassNotFoundException ignored) {
@@ -222,10 +221,10 @@ public class Natives {
} catch (UnsatisfiedLinkError ignored) { } catch (UnsatisfiedLinkError ignored) {
} }
try { try {
Class.forName(HybridClassLoader.getXposedBridgeClassName()); Class.forName(LspObfuscationHelper.getXposedBridgeClassName());
// in host process // in host process
List<String> abis = getAbiForLibrary(); List<String> abis = getAbiForLibrary();
String modulePath = HookEntry.getModulePath(); String modulePath = StartupInfo.getModulePath();
loadNativeLibraryInHost(ctx, modulePath, abis); loadNativeLibraryInHost(ctx, modulePath, abis);
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
// not in host process, ignore // not in host process, ignore

View 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";
}

View File

@@ -27,7 +27,7 @@ import android.content.pm.PackageManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import cc.ioctl.util.HostInfo; import cc.ioctl.util.HostInfo;
import io.github.qauxv.startup.HookEntry; import io.github.qauxv.poststartup.StartupInfo;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -89,7 +89,7 @@ public class AbiUtils {
} }
String apkPath; String apkPath;
if (HostInfo.isInHostProcess()) { if (HostInfo.isInHostProcess()) {
apkPath = HookEntry.getModulePath(); apkPath = StartupInfo.getModulePath();
} else { } else {
// self process // self process
apkPath = HostInfo.getApplication().getPackageCodePath(); apkPath = HostInfo.getApplication().getPackageCodePath();

View File

@@ -22,17 +22,20 @@
package io.github.qauxv.util.hookstatus; package io.github.qauxv.util.hookstatus;
import androidx.annotation.Keep;
import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedBridge;
import java.lang.reflect.Field; import java.lang.reflect.Field;
/** /**
* Called in handleLoadPackage, NO KOTLIN, NO ANDROIDX * Called in handleLoadPackage, NO KOTLIN, NO ANDROIDX
**/ **/
@Keep
public class HookStatusInit { public class HookStatusInit {
private HookStatusInit() { private HookStatusInit() {
} }
@Keep
public static void init(ClassLoader classLoader) throws Throwable { public static void init(ClassLoader classLoader) throws Throwable {
Class<?> kHookStatusImpl = classLoader.loadClass("io.github.qauxv.util.hookstatus.HookStatusImpl"); Class<?> kHookStatusImpl = classLoader.loadClass("io.github.qauxv.util.hookstatus.HookStatusImpl");
Field f = kHookStatusImpl.getDeclaredField("sZygoteHookMode"); Field f = kHookStatusImpl.getDeclaredField("sZygoteHookMode");

View 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;
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,6 @@ import io.github.qauxv.base.annotation.FunctionHookEntry
import io.github.qauxv.base.annotation.UiItemAgentEntry import io.github.qauxv.base.annotation.UiItemAgentEntry
import io.github.qauxv.dsl.FunctionEntryRouter import io.github.qauxv.dsl.FunctionEntryRouter
import io.github.qauxv.hook.CommonConfigFunctionHook import io.github.qauxv.hook.CommonConfigFunctionHook
import io.github.qauxv.startup.HookEntry
import io.github.qauxv.ui.CommonContextWrapper import io.github.qauxv.ui.CommonContextWrapper
import io.github.qauxv.util.Toasts import io.github.qauxv.util.Toasts
import io.github.qauxv.util.hostInfo import io.github.qauxv.util.hostInfo
@@ -65,7 +64,7 @@ object ManageComponent : CommonConfigFunctionHook("Ketal_ManageComponent") {
"发送到我的电脑" to ComponentName(hostInfo.packageName, "com.tencent.mobileqq.activity.qfileJumpActivity"), "发送到我的电脑" to ComponentName(hostInfo.packageName, "com.tencent.mobileqq.activity.qfileJumpActivity"),
"保存到QQ收藏" to ComponentName(hostInfo.packageName, "cooperation.qqfav.widget.QfavJumpActivity"), "保存到QQ收藏" to ComponentName(hostInfo.packageName, "cooperation.qqfav.widget.QfavJumpActivity"),
"面对面快传" to ComponentName(hostInfo.packageName, "cooperation.qlink.QlinkShareJumpActivity"), "面对面快传" 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) { private fun showDialog(context: Context) {

View File

@@ -27,7 +27,7 @@ import android.app.AlertDialog
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ComponentName import android.content.ComponentName
import android.os.Bundle import android.os.Bundle
import io.github.qauxv.startup.HookEntry import io.github.qauxv.util.PackageConstants
class QFileShareToIpadActivity : Activity() { class QFileShareToIpadActivity : Activity() {
companion object { companion object {
@@ -38,7 +38,7 @@ class QFileShareToIpadActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pkg = HookEntry.PACKAGE_NAME_QQ val pkg = PackageConstants.PACKAGE_NAME_QQ
intent.apply { intent.apply {
putExtra("targetUin", "9962") putExtra("targetUin", "9962")
putExtra("device_type", 1) putExtra("device_type", 1)

View File

@@ -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 = { module = "com.google.android.material:material", version = "1.12.0" }
material-dialogs-core = { module = "com.afollestad.material-dialogs:core", version.ref = "materialDialog" } material-dialogs-core = { module = "com.afollestad.material-dialogs:core", version.ref = "materialDialog" }
material-dialogs-input = { module = "com.afollestad.material-dialogs:input", 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" } flexbox = { module = "com.google.android.flexbox:flexbox", version = "3.0.0" }
colorpicker = { module = "com.jaredrummler:colorpicker", version = "1.1.0" } colorpicker = { module = "com.jaredrummler:colorpicker", version = "1.1.0" }
ezXHelper = { module = "com.github.kyuubiran:EzXHelper", version = "1.0.3" } 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" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version = "11.2.2" } aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version = "11.2.2" }
protobuf = { id = "com.google.protobuf", version = "0.9.4" } protobuf = { id = "com.google.protobuf", version = "0.9.4" }
android-library = { id = "com.android.library", version.ref = "agp" }

View 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)
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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)
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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;
}
}
}

View File

@@ -1,41 +1,23 @@
/* package io.github.qauxv.loader.sbl.xp51;
* 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 androidx.annotation.Keep;
import androidx.annotation.NonNull;
import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.IXposedHookZygoteInit; import de.robv.android.xposed.IXposedHookZygoteInit;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage; import de.robv.android.xposed.callbacks.XC_LoadPackage;
import io.github.qauxv.R; import io.github.qauxv.loader.sbl.common.ModuleLoader;
import io.github.qauxv.util.hookstatus.HookStatusInit; 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 * Entry point for started Xposed API 51-99.
* KOTLIN HERE, WHATEVER DIRECTLY OR INDIRECTLY. THIS CLASS SHOULD ONLY CALL {@code StartupHook.getInstance().doInit()} AND RETURN GRACEFULLY. OTHERWISE * <p>
* SOMETHING MAY HAPPEN BECAUSE OF A NON-STANDARD PLUGIN CLASSLOADER. * Xposed is used as ART hook implementation.
*
* @author kinit
*/ */
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 = "com.tencent.mobileqq";
public static final String PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi"; 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. * *** No kotlin code should be invoked here.*** May cause a crash.
*/ */
@Keep
@Override @Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws ReflectiveOperationException {
if (R.string.res_inject_success >>> 24 == 0x7f) {
XposedBridge.log("package id must NOT be 0x7f, reject loading...");
return;
}
sLoadPackageParam = lpparam; sLoadPackageParam = lpparam;
// check LSPosed dex-obfuscation // check LSPosed dex-obfuscation
Class<?> kXposedBridge = XposedBridge.class; 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) { switch (lpparam.packageName) {
case PACKAGE_NAME_SELF: { 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; break;
} }
case PACKAGE_NAME_TIM: case PACKAGE_NAME_TIM:
@@ -81,7 +57,8 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
throw new IllegalStateException("handleLoadPackage: sInitZygoteStartupParam is null"); throw new IllegalStateException("handleLoadPackage: sInitZygoteStartupParam is null");
} }
sCurrentPackageName = lpparam.packageName; sCurrentPackageName = lpparam.packageName;
StartupHook.getInstance().initialize(lpparam.classLoader); ModuleLoader.initialize(lpparam.appInfo, lpparam.classLoader,
Xp51HookImpl.INSTANCE, Xp51HookImpl.INSTANCE, getModulePath());
break; break;
} }
case PACKAGE_NAME_QQ_INTERNATIONAL: { 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. * 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 * @return the lpparam
*/ */
@@ -118,8 +93,6 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
/** /**
* Get the path of the current module. * 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 * @return the module path
*/ */
@@ -132,8 +105,6 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
/** /**
* Get the {@link IXposedHookZygoteInit.StartupParam} of the current module. * 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 * @return the initZygote param
*/ */
@@ -143,4 +114,5 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit
} }
return sInitZygoteStartupParam; return sInitZygoteStartupParam;
} }
} }

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View 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)
}

View File

@@ -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.");
}
}

View File

@@ -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);
}
}
}

View File

@@ -60,6 +60,9 @@ develocity {
rootProject.name = "QAuxiliary" rootProject.name = "QAuxiliary"
include( include(
":app", ":app",
":loader:startup",
":loader:sbl",
":loader:hookapi",
":libs:stub", ":libs:stub",
":libs:ksp", ":libs:ksp",
":libs:mmkv", ":libs:mmkv",