diff --git a/api/src/main/java/io/github/libxposed/api/XposedInterface.java b/api/src/main/java/io/github/libxposed/api/XposedInterface.java index 3c4ac86..8c80569 100644 --- a/api/src/main/java/io/github/libxposed/api/XposedInterface.java +++ b/api/src/main/java/io/github/libxposed/api/XposedInterface.java @@ -10,10 +10,12 @@ import androidx.annotation.Nullable; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentHashMap; import io.github.libxposed.api.errors.HookFailedError; import io.github.libxposed.api.utils.DexParser; @@ -24,9 +26,28 @@ import io.github.libxposed.api.utils.DexParser; @SuppressWarnings("unused") public interface XposedInterface { /** - * SDK API version. + * The API version of this library. */ - int API = 100; + int LIB_API = 101; + + /** + * @deprecated Use {@link #LIB_API}. Kept for API100 compatibility. + */ + @Deprecated + int API = LIB_API; + + /** + * The framework has capability to hook system_server. + */ + long PROP_CAP_SYSTEM = 1L; + /** + * The framework provides remote preferences and remote files support. + */ + long PROP_CAP_REMOTE = 1L << 1; + /** + * The framework enables runtime Xposed API protection. + */ + long PROP_RT_API_PROTECTION = 1L << 2; /** * Indicates that the framework is running as root. @@ -208,6 +229,16 @@ public interface XposedInterface { * } */ interface Hooker { + @Nullable + default Object intercept(@NonNull Chain chain) throws Throwable { + return chain.proceed(); + } + } + + enum ExceptionMode { + DEFAULT, + PASSTHROUGH, + PROTECTIVE } /** @@ -228,6 +259,221 @@ public interface XposedInterface { void unhook(); } + interface HookHandle { + @NonNull + Executable getExecutable(); + + void unhook(); + } + + /** + * Chain context for the API 101 style interceptor. + */ + interface Chain { + @NonNull + Executable getExecutable(); + + @Nullable + Object getThisObject(); + + @NonNull + Object[] getArgs(); + + @SuppressWarnings("unchecked") + @Nullable + default T getArg(int index) { + return (T) getArgs()[index]; + } + + default void setArg(int index, @Nullable Object value) { + getArgs()[index] = value; + } + + @Nullable + Object proceed() throws InvocationTargetException, IllegalArgumentException, IllegalAccessException; + } + + /** + * API 101 interceptor. + */ + interface Interceptor { + @Nullable + Object intercept(@NonNull Chain chain) throws Throwable; + } + + /** + * API 101 hook builder. + */ + interface HookBuilder { + @NonNull + default HookBuilder setPriority(int priority) { + return this; + } + + @NonNull + default HookBuilder setExceptionMode(@NonNull ExceptionMode mode) { + return this; + } + + @NonNull + HookHandle intercept(@NonNull Hooker hooker); + + @NonNull + default HookHandle intercept(@NonNull Interceptor interceptor) { + return intercept(new Hooker() { + @SuppressWarnings("unused") + public Object intercept(@NonNull Chain chain) throws Throwable { + return interceptor.intercept(chain); + } + }); + } + } + + final class LegacyHookRegistry { + private LegacyHookRegistry() { + } + + static final ConcurrentHashMap ENTRIES = new ConcurrentHashMap<>(); + } + + final class HookEntry { + final XposedInterface base; + final Hooker hooker; + final Method interceptMethod; + final ExceptionMode exceptionMode; + + HookEntry(@NonNull XposedInterface base, @NonNull Hooker hooker, @NonNull Method interceptMethod, @NonNull ExceptionMode exceptionMode) { + this.base = base; + this.hooker = hooker; + this.interceptMethod = interceptMethod; + this.exceptionMode = exceptionMode; + } + } + + final class CompatHookBuilder implements HookBuilder { + private final XposedInterface base; + private final Executable origin; + private int priority = PRIORITY_DEFAULT; + private ExceptionMode exceptionMode = ExceptionMode.DEFAULT; + + CompatHookBuilder(@NonNull XposedInterface base, @NonNull Executable origin) { + this.base = base; + this.origin = origin; + } + + @NonNull + @Override + public HookBuilder setPriority(int priority) { + this.priority = priority; + return this; + } + + @NonNull + @Override + public HookBuilder setExceptionMode(@NonNull ExceptionMode mode) { + this.exceptionMode = mode; + return this; + } + + @NonNull + @Override + public HookHandle intercept(@NonNull Hooker hooker) { + final Method method = CompatHooker.findInterceptMethod(hooker.getClass()); + LegacyHookRegistry.ENTRIES.put(origin, new HookEntry(base, hooker, method, exceptionMode)); + final MethodUnhooker raw; + if (origin instanceof Method) { + raw = base.hook((Method) origin, priority, CompatHooker.class); + } else if (origin instanceof Constructor) { + @SuppressWarnings({"rawtypes", "unchecked"}) + MethodUnhooker cast = (MethodUnhooker) base.hook((Constructor) origin, priority, CompatHooker.class); + raw = cast; + } else { + LegacyHookRegistry.ENTRIES.remove(origin); + throw new IllegalArgumentException("Unsupported executable type: " + origin.getClass().getName()); + } + return new HookHandle() { + @NonNull + @Override + public Executable getExecutable() { + return origin; + } + + @Override + public void unhook() { + LegacyHookRegistry.ENTRIES.remove(origin); + raw.unhook(); + } + }; + } + } + + final class CompatHooker implements Hooker { + private CompatHooker() { + } + + static Method findInterceptMethod(@NonNull Class cls) { + for (Method method : cls.getMethods()) { + if (!method.getName().equals("intercept") || method.getParameterCount() != 1) { + continue; + } + if (method.getParameterTypes()[0] == Chain.class) { + method.setAccessible(true); + return method; + } + } + throw new IllegalArgumentException("Hooker must declare public intercept(Chain): " + cls.getName()); + } + + public static void before(@NonNull BeforeHookCallback callback) { + var member = callback.getMember(); + if (!(member instanceof Executable)) { + return; + } + var executable = (Executable) member; + var entry = LegacyHookRegistry.ENTRIES.get(executable); + if (entry == null) { + return; + } + var chain = new Chain() { + @NonNull + @Override + public Executable getExecutable() { + return executable; + } + + @Nullable + @Override + public Object getThisObject() { + return callback.getThisObject(); + } + + @NonNull + @Override + public Object[] getArgs() { + return callback.getArgs(); + } + + @Nullable + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public Object proceed() throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { + if (executable instanceof Method method) { + return entry.base.invokeOrigin(method, callback.getThisObject(), callback.getArgs()); + } + entry.base.invokeOrigin((Constructor) executable, callback.getThisObject(), callback.getArgs()); + return callback.getThisObject(); + } + }; + try { + callback.returnAndSkip(entry.interceptMethod.invoke(entry.hooker, chain)); + } catch (InvocationTargetException ite) { + callback.throwAndSkip(ite.getCause() != null ? ite.getCause() : ite); + } catch (Throwable t) { + callback.throwAndSkip(t); + } + } + } + /** * Gets the Xposed framework name of current implementation. * @@ -236,6 +482,13 @@ public interface XposedInterface { @NonNull String getFrameworkName(); + /** + * Gets runtime API version. + */ + default int getApiVersion() { + return LIB_API; + } + /** * Gets the Xposed framework version of current implementation. * @@ -251,6 +504,21 @@ public interface XposedInterface { */ long getFrameworkVersionCode(); + /** + * Gets the Xposed framework properties. + */ + default long getFrameworkProperties() { + long prop = 0L; + if (getFrameworkPrivilege() == FRAMEWORK_PRIVILEGE_ROOT || + getFrameworkPrivilege() == FRAMEWORK_PRIVILEGE_CONTAINER) { + prop |= PROP_CAP_SYSTEM; + } + if (getFrameworkPrivilege() != FRAMEWORK_PRIVILEGE_EMBEDDED) { + prop |= PROP_CAP_REMOTE; + } + return prop; + } + /** * Gets the Xposed framework privilege of current implementation. * @@ -345,6 +613,16 @@ public interface XposedInterface { @NonNull MethodUnhooker> hook(@NonNull Constructor origin, int priority, @NonNull Class hooker); + @NonNull + default HookBuilder hook(@NonNull Executable origin) { + return new CompatHookBuilder(this, origin); + } + + @NonNull + default HookBuilder hook(@NonNull Executable origin, int priority) { + return new CompatHookBuilder(this, origin).setPriority(priority); + } + /** * Deoptimizes a method in case hooked callee is not called because of inline. * @@ -468,6 +746,14 @@ public interface XposedInterface { */ void log(@NonNull String message); + /** + * Writes a message to the Xposed log with explicit priority and optional tag. + */ + default void log(int priority, @Nullable String tag, @NonNull String msg) { + String prefix = tag == null || tag.trim().isEmpty() ? "" : ("[" + tag + "] "); + log(prefix + msg); + } + /** * Writes a message with a stack trace to the Xposed log. * @@ -476,6 +762,18 @@ public interface XposedInterface { */ void log(@NonNull String message, @NonNull Throwable throwable); + /** + * Writes a message with stack trace to the Xposed log with explicit priority and optional tag. + */ + default void log(int priority, @Nullable String tag, @NonNull String msg, @Nullable Throwable tr) { + String prefix = tag == null || tag.trim().isEmpty() ? "" : ("[" + tag + "] "); + if (tr == null) { + log(prefix + msg); + } else { + log(prefix + msg, tr); + } + } + /** * Parse a dex file in memory. * @@ -491,8 +789,17 @@ public interface XposedInterface { * Gets the application info of the module. */ @NonNull + @Deprecated ApplicationInfo getApplicationInfo(); + /** + * Gets the application info of the module. + */ + @NonNull + default ApplicationInfo getModuleApplicationInfo() { + return getApplicationInfo(); + } + /** * Gets remote preferences stored in Xposed framework. Note that those are read-only in hooked apps. * diff --git a/api/src/main/java/io/github/libxposed/api/XposedInterfaceWrapper.java b/api/src/main/java/io/github/libxposed/api/XposedInterfaceWrapper.java index 425596f..3b76cef 100644 --- a/api/src/main/java/io/github/libxposed/api/XposedInterfaceWrapper.java +++ b/api/src/main/java/io/github/libxposed/api/XposedInterfaceWrapper.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteBuffer; @@ -21,151 +22,210 @@ import io.github.libxposed.api.utils.DexParser; */ public class XposedInterfaceWrapper implements XposedInterface { - private final XposedInterface mBase; + private volatile XposedInterface mBase; + + public XposedInterfaceWrapper() { + } XposedInterfaceWrapper(@NonNull XposedInterface base) { mBase = base; } + /** + * Attaches the framework interface to the module. Modules should never call this method directly. + */ + @SuppressWarnings("unused") + public final void attachFramework(@NonNull XposedInterface base) { + if (mBase != null) { + throw new IllegalStateException("Framework already attached"); + } + mBase = base; + } + + private void ensureAttached() { + if (mBase == null) { + throw new IllegalStateException("Framework not attached"); + } + } + @NonNull @Override public final String getFrameworkName() { + ensureAttached(); return mBase.getFrameworkName(); } @NonNull @Override public final String getFrameworkVersion() { + ensureAttached(); return mBase.getFrameworkVersion(); } @Override public final long getFrameworkVersionCode() { + ensureAttached(); return mBase.getFrameworkVersionCode(); } @Override public final int getFrameworkPrivilege() { + ensureAttached(); return mBase.getFrameworkPrivilege(); } @NonNull @Override public final MethodUnhooker hook(@NonNull Method origin, @NonNull Class hooker) { + ensureAttached(); return mBase.hook(origin, hooker); } + @NonNull + @Override + public final HookBuilder hook(@NonNull Executable origin) { + ensureAttached(); + return mBase.hook(origin); + } + + @NonNull + @Override + public final HookBuilder hook(@NonNull Executable origin, int priority) { + ensureAttached(); + return mBase.hook(origin, priority); + } + @NonNull @Override public MethodUnhooker> hookClassInitializer(@NonNull Class origin, @NonNull Class hooker) { + ensureAttached(); return mBase.hookClassInitializer(origin, hooker); } @NonNull @Override public MethodUnhooker> hookClassInitializer(@NonNull Class origin, int priority, @NonNull Class hooker) { + ensureAttached(); return mBase.hookClassInitializer(origin, priority, hooker); } @NonNull @Override public final MethodUnhooker hook(@NonNull Method origin, int priority, @NonNull Class hooker) { + ensureAttached(); return mBase.hook(origin, priority, hooker); } @NonNull @Override public final MethodUnhooker> hook(@NonNull Constructor origin, @NonNull Class hooker) { + ensureAttached(); return mBase.hook(origin, hooker); } @NonNull @Override public final MethodUnhooker> hook(@NonNull Constructor origin, int priority, @NonNull Class hooker) { + ensureAttached(); return mBase.hook(origin, priority, hooker); } @Override public final boolean deoptimize(@NonNull Method method) { + ensureAttached(); return mBase.deoptimize(method); } @Override public final boolean deoptimize(@NonNull Constructor constructor) { + ensureAttached(); return mBase.deoptimize(constructor); } @Nullable @Override public final Object invokeOrigin(@NonNull Method method, @Nullable Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { + ensureAttached(); return mBase.invokeOrigin(method, thisObject, args); } @Override public void invokeOrigin(@NonNull Constructor constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { + ensureAttached(); mBase.invokeOrigin(constructor, thisObject, args); } @Nullable @Override public final Object invokeSpecial(@NonNull Method method, @NonNull Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { + ensureAttached(); return mBase.invokeSpecial(method, thisObject, args); } @Override public void invokeSpecial(@NonNull Constructor constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { + ensureAttached(); mBase.invokeSpecial(constructor, thisObject, args); } @NonNull @Override public final T newInstanceOrigin(@NonNull Constructor constructor, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException, InstantiationException { + ensureAttached(); return mBase.newInstanceOrigin(constructor, args); } @NonNull @Override public final U newInstanceSpecial(@NonNull Constructor constructor, @NonNull Class subClass, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException, InstantiationException { + ensureAttached(); return mBase.newInstanceSpecial(constructor, subClass, args); } @Override public final void log(@NonNull String message) { + ensureAttached(); mBase.log(message); } @Override public final void log(@NonNull String message, @NonNull Throwable throwable) { + ensureAttached(); mBase.log(message, throwable); } @Nullable @Override public final DexParser parseDex(@NonNull ByteBuffer dexData, boolean includeAnnotations) throws IOException { + ensureAttached(); return mBase.parseDex(dexData, includeAnnotations); } @NonNull @Override public SharedPreferences getRemotePreferences(@NonNull String name) { + ensureAttached(); return mBase.getRemotePreferences(name); } @NonNull @Override public ApplicationInfo getApplicationInfo() { + ensureAttached(); return mBase.getApplicationInfo(); } @NonNull @Override public String[] listRemoteFiles() { + ensureAttached(); return mBase.listRemoteFiles(); } @NonNull @Override public ParcelFileDescriptor openRemoteFile(@NonNull String name) throws FileNotFoundException { + ensureAttached(); return mBase.openRemoteFile(name); } } diff --git a/api/src/main/java/io/github/libxposed/api/XposedModule.java b/api/src/main/java/io/github/libxposed/api/XposedModule.java index b2e1a03..5dd546f 100644 --- a/api/src/main/java/io/github/libxposed/api/XposedModule.java +++ b/api/src/main/java/io/github/libxposed/api/XposedModule.java @@ -4,10 +4,19 @@ import androidx.annotation.NonNull; /** * Super class which all Xposed module entry classes should extend.
- * Entry classes will be instantiated exactly once for each process. + * Entry classes will be instantiated exactly once for each process. Modules should avoid + * initialization work before {@link #onModuleLoaded(ModuleLoadedParam)} is called. */ @SuppressWarnings("unused") public abstract class XposedModule extends XposedInterfaceWrapper implements XposedModuleInterface { + /** + * New API-style constructor. The framework will attach itself via + * {@link #attachFramework(XposedInterface)} and then dispatch lifecycle callbacks. + */ + public XposedModule() { + super(); + } + /** * Instantiates a new Xposed module.
* When the module is loaded into the target process, the constructor will be called. @@ -15,7 +24,10 @@ public abstract class XposedModule extends XposedInterfaceWrapper implements Xpo * @param base The implementation interface provided by the framework, should not be used by the module * @param param Information about the process in which the module is loaded */ + @Deprecated public XposedModule(@NonNull XposedInterface base, @NonNull ModuleLoadedParam param) { - super(base); + this(); + attachFramework(base); + onModuleLoaded(param); } } diff --git a/api/src/main/java/io/github/libxposed/api/XposedModuleInterface.java b/api/src/main/java/io/github/libxposed/api/XposedModuleInterface.java index 1cb548c..5d17855 100644 --- a/api/src/main/java/io/github/libxposed/api/XposedModuleInterface.java +++ b/api/src/main/java/io/github/libxposed/api/XposedModuleInterface.java @@ -1,5 +1,6 @@ package io.github.libxposed.api; +import android.app.AppComponentFactory; import android.content.pm.ApplicationInfo; import android.os.Build; @@ -34,7 +35,7 @@ public interface XposedModuleInterface { /** * Wraps information about system server. */ - interface SystemServerLoadedParam { + interface SystemServerStartingParam { /** * Gets the class loader of system server. * @@ -44,6 +45,13 @@ public interface XposedModuleInterface { ClassLoader getClassLoader(); } + /** + * @deprecated Kept for API100 compatibility. + */ + @Deprecated + interface SystemServerLoadedParam extends SystemServerStartingParam { + } + /** * Wraps information about the package being loaded. */ @@ -73,6 +81,27 @@ public interface XposedModuleInterface { @NonNull ClassLoader getDefaultClassLoader(); + /** + * Gets information about whether is this package the first and main package of the app process. + * + * @return {@code true} if this is the first package. + */ + boolean isFirstPackage(); + + /** + * @deprecated Kept for API100 compatibility. Use {@link PackageReadyParam#getClassLoader()}. + */ + @Deprecated + @NonNull + default ClassLoader getClassLoader() { + return getDefaultClassLoader(); + } + } + + /** + * Wraps information about the package whose class loader is ready. + */ + interface PackageReadyParam extends PackageLoadedParam { /** * Gets the class loader of the package being loaded. * @@ -82,11 +111,17 @@ public interface XposedModuleInterface { ClassLoader getClassLoader(); /** - * Gets information about whether is this package the first and main package of the app process. - * - * @return {@code true} if this is the first package. + * Gets the app component factory of the package being loaded. */ - boolean isFirstPackage(); + @RequiresApi(Build.VERSION_CODES.P) + @NonNull + AppComponentFactory getAppComponentFactory(); + } + + /** + * Gets notified when the module is loaded into the target process. + */ + default void onModuleLoaded(@NonNull ModuleLoadedParam param) { } /** @@ -104,5 +139,19 @@ public interface XposedModuleInterface { * @param param Information about system server */ default void onSystemServerLoaded(@NonNull SystemServerLoadedParam param) { + onSystemServerStarting(param); + } + + /** + * Gets notified when custom {@link android.app.AppComponentFactory} has created the final + * class loader. + */ + default void onPackageReady(@NonNull PackageReadyParam param) { + } + + /** + * Gets notified when system server is ready to start critical services. + */ + default void onSystemServerStarting(@NonNull SystemServerStartingParam param) { } }