package de.robv.android.xposed; import android.annotation.SuppressLint; import android.util.Log; import com.elderdrivers.riru.xposed.core.HookMain; import com.elderdrivers.riru.xposed.dexmaker.DynamicBridge; import com.elderdrivers.riru.xposed.dexmaker.MethodInfo; import java.io.File; import java.io.IOException; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import de.robv.android.xposed.XC_MethodHook.MethodHookParam; import de.robv.android.xposed.callbacks.XC_InitPackageResources; import de.robv.android.xposed.callbacks.XC_LoadPackage; import static de.robv.android.xposed.XposedHelpers.getIntField; /** * This class contains most of Xposed's central logic, such as initialization and callbacks used by * the native side. It also includes methods to add new hooks. */ @SuppressWarnings("JniMissingFunction") public final class XposedBridge { /** * The system class loader which can be used to locate Android framework classes. * Application classes cannot be retrieved from it. * * @see ClassLoader#getSystemClassLoader */ public static final ClassLoader BOOTCLASSLOADER = XposedBridge.class.getClassLoader(); /** @hide */ public static final String TAG = "EdXposed-Bridge"; /** @deprecated Use {@link #getXposedVersion()} instead. */ @Deprecated public static int XPOSED_BRIDGE_VERSION; /*package*/ static boolean isZygote = true; // ed: RuntimeInit.main() tool process not supported yet private static int runtime = 2; // ed: only support art private static final int RUNTIME_DALVIK = 1; private static final int RUNTIME_ART = 2; public static boolean disableHooks = false; // This field is set "magically" on MIUI. /*package*/ static long BOOT_START_TIME; private static final Object[] EMPTY_ARRAY = new Object[0]; // built-in handlers private static final Map> sHookedMethodCallbacks = new HashMap<>(); public static final CopyOnWriteSortedSet sLoadedPackageCallbacks = new CopyOnWriteSortedSet<>(); /*package*/ static final CopyOnWriteSortedSet sInitPackageResourcesCallbacks = new CopyOnWriteSortedSet<>(); private XposedBridge() {} /** * Called when native methods and other things are initialized, but before preloading classes etc. * @hide */ @SuppressWarnings("deprecation") public static void main(String[] args) { // ed: moved } /** @hide */ // protected static final class ToolEntryPoint { // protected static void main(String[] args) { // isZygote = false; // XposedBridge.main(args); // } // } private static void initXResources() throws IOException { // ed: no support for now } @SuppressLint("SetWorldReadable") private static File ensureSuperDexFile(String clz, Class realSuperClz, Class topClz) throws IOException { XposedBridge.removeFinalFlagNative(realSuperClz); File dexFile = DexCreator.ensure(clz, realSuperClz, topClz); dexFile.setReadable(true, false); return dexFile; } // private static boolean hadInitErrors() { // // ed: assuming never had errors // return false; // } // private static native int getRuntime(); // /*package*/ static native boolean startsSystemServer(); // /*package*/ static native String getStartClassName(); // /*package*/ native static boolean initXResourcesNative(); /** * Returns the currently installed version of the Xposed framework. */ public static int getXposedVersion() { // ed: fixed value for now return 90; } /** * Writes a message to the Xposed error log. * *

DON'T FLOOD THE LOG!!! This is only meant for error logging. * If you want to write information/debug messages, use logcat. * * @param text The log message. */ public synchronized static void log(String text) { Log.i(TAG, text); } /** * Logs a stack trace to the Xposed error log. * *

DON'T FLOOD THE LOG!!! This is only meant for error logging. * If you want to write information/debug messages, use logcat. * * @param t The Throwable object for the stack trace. */ public synchronized static void log(Throwable t) { Log.e(TAG, Log.getStackTraceString(t)); } /** * 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 instanceof Method) && !(hookMethod instanceof Constructor)) { throw new IllegalArgumentException("Only methods and constructors can be hooked: " + hookMethod.toString()); } else if (hookMethod.getDeclaringClass().isInterface()) { throw new IllegalArgumentException("Cannot hook interfaces: " + hookMethod.toString()); } else if (Modifier.isAbstract(hookMethod.getModifiers())) { throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod.toString()); } if (callback == null) { throw new IllegalArgumentException("callback should not be null!"); } boolean newMethod = false; CopyOnWriteSortedSet callbacks; synchronized (sHookedMethodCallbacks) { callbacks = sHookedMethodCallbacks.get(hookMethod); if (callbacks == null) { callbacks = new CopyOnWriteSortedSet<>(); sHookedMethodCallbacks.put(hookMethod, callbacks); newMethod = true; } } callbacks.add(callback); if (newMethod) { Class declaringClass = hookMethod.getDeclaringClass(); int slot; Class[] parameterTypes; Class returnType; if (runtime == RUNTIME_ART) { slot = 0; parameterTypes = null; returnType = null; } else if (hookMethod instanceof Method) { slot = getIntField(hookMethod, "slot"); parameterTypes = ((Method) hookMethod).getParameterTypes(); returnType = ((Method) hookMethod).getReturnType(); } else { slot = getIntField(hookMethod, "slot"); parameterTypes = ((Constructor) hookMethod).getParameterTypes(); returnType = null; } AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType); MethodInfo methodInfo = new MethodInfo(hookMethod); declaringClass = methodInfo.getClassForSure(); Member reflectMethod = (Member) HookMain.findMethod( declaringClass, methodInfo.methodName, methodInfo.methodSig); if (reflectMethod == null) { Log.e(TAG, "method not found: name=" + methodInfo.methodName + ", sig=" + methodInfo.methodSig); reflectMethod = hookMethod; } hookMethodNative(reflectMethod, declaringClass, slot, additionalInfo); } return callback.new Unhook(hookMethod); } /** * Removes the callback for a hooked method/constructor. * * @deprecated Use {@link XC_MethodHook.Unhook#unhook} instead. An instance of the {@code Unhook} * class is returned when you hook the method. * * @param hookMethod The method for which the callback should be removed. * @param callback The reference to the callback as specified in {@link #hookMethod}. */ @Deprecated public static void unhookMethod(Member hookMethod, XC_MethodHook callback) { CopyOnWriteSortedSet callbacks; synchronized (sHookedMethodCallbacks) { callbacks = sHookedMethodCallbacks.get(hookMethod); if (callbacks == null) return; } callbacks.remove(callback); } /** * 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 hookAllMethods(Class hookClass, String methodName, XC_MethodHook callback) { Set unhooks = new HashSet<>(); 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 hookAllConstructors(Class hookClass, XC_MethodHook callback) { Set unhooks = new HashSet<>(); for (Member constructor : hookClass.getDeclaredConstructors()) unhooks.add(hookMethod(constructor, callback)); return unhooks; } /** * This method is called as a replacement for hooked methods. */ private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj, Object thisObject, Object[] args) throws Throwable { AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj; if (disableHooks) { try { return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes, additionalInfo.returnType, thisObject, args); } catch (InvocationTargetException e) { throw e.getCause(); } } Object[] callbacksSnapshot = additionalInfo.callbacks.getSnapshot(); final int callbacksLength = callbacksSnapshot.length; if (callbacksLength == 0) { try { return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes, additionalInfo.returnType, thisObject, args); } catch (InvocationTargetException e) { throw e.getCause(); } } MethodHookParam param = new MethodHookParam(); param.method = method; param.thisObject = thisObject; param.args = args; // call "before method" callbacks int beforeIdx = 0; do { try { ((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param); } catch (Throwable t) { XposedBridge.log(t); // reset result (ignoring what the unexpectedly exiting callback did) param.setResult(null); param.returnEarly = false; continue; } if (param.returnEarly) { // skip remaining "before" callbacks and corresponding "after" callbacks beforeIdx++; break; } } while (++beforeIdx < callbacksLength); // call original method if not requested otherwise if (!param.returnEarly) { try { param.setResult(invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args)); } catch (InvocationTargetException e) { param.setThrowable(e.getCause()); } } // call "after method" callbacks int afterIdx = beforeIdx - 1; do { Object lastResult = param.getResult(); Throwable lastThrowable = param.getThrowable(); try { ((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param); } catch (Throwable t) { XposedBridge.log(t); // reset to last result (ignoring what the unexpectedly exiting callback did) if (lastThrowable == null) param.setResult(lastResult); else param.setThrowable(lastThrowable); } } while (--afterIdx >= 0); // return if (param.hasThrowable()) throw param.getThrowable(); else return param.getResult(); } /** * Adds a callback to be executed when an app ("Android package") is loaded. * *

You probably don't need to call this. Simply implement {@link IXposedHookLoadPackage} * in your module class and Xposed will take care of registering it as a callback. * * @param callback The callback to be executed. * @hide */ public static void hookLoadPackage(XC_LoadPackage callback) { synchronized (sLoadedPackageCallbacks) { sLoadedPackageCallbacks.add(callback); } } public static void clearLoadedPackages() { synchronized (sLoadedPackageCallbacks) { sLoadedPackageCallbacks.clear(); } } /** * Adds a callback to be executed when the resources for an app are initialized. * *

You probably don't need to call this. Simply implement {@link IXposedHookInitPackageResources} * in your module class and Xposed will take care of registering it as a callback. * * @param callback The callback to be executed. * @hide */ public static void hookInitPackageResources(XC_InitPackageResources callback) { // TODO not supported yet // synchronized (sInitPackageResourcesCallbacks) { // sInitPackageResourcesCallbacks.add(callback); // } } public static void clearInitPackageResources() { synchronized (sInitPackageResourcesCallbacks) { sInitPackageResourcesCallbacks.clear(); } } /** * Intercept every call to the specified method and call a handler function instead. * @param method The method to intercept */ private synchronized static void hookMethodNative(final Member method, Class declaringClass, int slot, final Object additionalInfoObj) { DynamicBridge.hookMethod(method, (AdditionalHookInfo) additionalInfoObj); } private static Object invokeOriginalMethodNative(Member method, int methodId, Class[] parameterTypes, Class returnType, Object thisObject, Object[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { return DynamicBridge.invokeOriginalMethod(method, thisObject, args); } /** * Basically the same as {@link Method#invoke}, but calls the original method * as it was before the interception by Xposed. Also, access permissions are not checked. * *

There are very few cases where this method is needed. A common mistake is * to replace a method and then invoke the original one based on dynamic conditions. This * creates overhead and skips further hooks by other modules. Instead, just hook (don't replace) * the method and call {@code param.setResult(null)} in {@link XC_MethodHook#beforeHookedMethod} * if the original method should be skipped. * * @param method The method to be called. * @param thisObject For non-static calls, the "this" pointer, otherwise {@code null}. * @param args Arguments for the method call as Object[] array. * @return The result returned from the invoked method. * @throws NullPointerException * if {@code receiver == null} for a non-static method * @throws IllegalAccessException * if this method is not accessible (see {@link AccessibleObject}) * @throws IllegalArgumentException * if the number of arguments doesn't match the number of parameters, the receiver * is incompatible with the declaring class, or an argument could not be unboxed * or converted by a widening conversion to the corresponding parameter type * @throws InvocationTargetException * if an exception was thrown by the invoked method */ public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args) throws NullPointerException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (args == null) { args = EMPTY_ARRAY; } Class[] parameterTypes; Class returnType; if (runtime == RUNTIME_ART && (method instanceof Method || method instanceof Constructor)) { parameterTypes = null; returnType = null; } else if (method instanceof Method) { parameterTypes = ((Method) method).getParameterTypes(); returnType = ((Method) method).getReturnType(); } else if (method instanceof Constructor) { parameterTypes = ((Constructor) method).getParameterTypes(); returnType = null; } else { throw new IllegalArgumentException("method must be of type Method or Constructor"); } return invokeOriginalMethodNative(method, 0, parameterTypes, returnType, thisObject, args); } /*package*/ static void setObjectClass(Object obj, Class clazz) { if (clazz.isAssignableFrom(obj.getClass())) { throw new IllegalArgumentException("Cannot transfer object from " + obj.getClass() + " to " + clazz); } setObjectClassNative(obj, clazz); } private static native void setObjectClassNative(Object obj, Class clazz); /*package*/ static native void dumpObjectNative(Object obj); /*package*/ static Object cloneToSubclass(Object obj, Class targetClazz) { if (obj == null) return null; if (!obj.getClass().isAssignableFrom(targetClazz)) throw new ClassCastException(targetClazz + " doesn't extend " + obj.getClass()); return cloneToSubclassNative(obj, targetClazz); } private static native Object cloneToSubclassNative(Object obj, Class targetClazz); private static native void removeFinalFlagNative(Class clazz); // /*package*/ static native void closeFilesBeforeForkNative(); // /*package*/ static native void reopenFilesAfterForkNative(); // // /*package*/ static native void invalidateCallersNative(Member[] methods); /** @hide */ public static final class CopyOnWriteSortedSet { private transient volatile Object[] elements = EMPTY_ARRAY; @SuppressWarnings("UnusedReturnValue") public synchronized boolean add(E e) { int index = indexOf(e); if (index >= 0) return false; Object[] newElements = new Object[elements.length + 1]; System.arraycopy(elements, 0, newElements, 0, elements.length); newElements[elements.length] = e; Arrays.sort(newElements); elements = newElements; return true; } @SuppressWarnings("UnusedReturnValue") public synchronized boolean remove(E e) { int index = indexOf(e); if (index == -1) return false; Object[] newElements = new Object[elements.length - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, elements.length - index - 1); elements = newElements; return true; } private int indexOf(Object o) { for (int i = 0; i < elements.length; i++) { if (o.equals(elements[i])) return i; } return -1; } public Object[] getSnapshot() { return elements; } public synchronized void clear() { elements = EMPTY_ARRAY; } } public static class AdditionalHookInfo { public final CopyOnWriteSortedSet callbacks; public final Class[] parameterTypes; public final Class returnType; private AdditionalHookInfo(CopyOnWriteSortedSet callbacks, Class[] parameterTypes, Class returnType) { this.callbacks = callbacks; this.parameterTypes = parameterTypes; this.returnType = returnType; } } }