From bbada22cb329543c2fbb69f4d2b46d5b86872790 Mon Sep 17 00:00:00 2001 From: LoveSy Date: Sat, 13 Jun 2026 19:57:54 +0800 Subject: [PATCH 1/2] Add API 103: openDex for read-only SQL over a loaded package's APK/DEX XposedModuleInterface.PackageLoadedParam.openDex() returns a DexDatabase: a Closeable, read-only SQL view over that loaded package's APK/DEX. Modules run DexDatabase.query(String) -> android.database.Cursor (consumed like SQLiteDatabase.rawQuery) and close() it when done (it holds the mapped dex/cache) -- a clear lifecycle, not a one-shot query with no handle to release. Scoped to the loaded-apk callback param (not XposedInterface) so it unambiguously targets that package's dex; PackageReadyParam inherits it. Added as API 103 (LIB_API = API_103, libVersion 103.0.0). Implemented by the framework (e.g. LSPosed via its dexsql engine). Co-Authored-By: Claude Opus 4.8 --- api/build.gradle.kts | 2 +- .../io/github/libxposed/api/DexDatabase.java | 53 +++++++++++++++++++ .../github/libxposed/api/XposedInterface.java | 12 ++++- .../libxposed/api/XposedModuleInterface.java | 14 +++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 api/src/main/java/io/github/libxposed/api/DexDatabase.java diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 8f9ea18..79b4893 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -32,7 +32,7 @@ android { } } -val libVersion = "102.0.0" +val libVersion = "103.0.0" val publishSnapshot = providers.gradleProperty("publishSnapshot").orNull == "true" val dependencySnapshot = providers.gradleProperty("dependencySnapshot").orNull == "true" fun String.real(snapshot: Boolean) = if (snapshot) "$this-SNAPSHOT" else this diff --git a/api/src/main/java/io/github/libxposed/api/DexDatabase.java b/api/src/main/java/io/github/libxposed/api/DexDatabase.java new file mode 100644 index 0000000..314ebe0 --- /dev/null +++ b/api/src/main/java/io/github/libxposed/api/DexDatabase.java @@ -0,0 +1,53 @@ +package io.github.libxposed.api; + +import android.database.Cursor; + +import androidx.annotation.NonNull; + +import java.io.Closeable; + +import io.github.libxposed.annotation.SinceApi; + +/** + * A read-only SQL view over an APK/DEX, obtained from + * {@link XposedModuleInterface.PackageLoadedParam#openDex()}. + * + *

Backed by the framework's dex query engine; {@link #query} returns an + * {@link android.database.Cursor} consumed like + * {@link android.database.sqlite.SQLiteDatabase#rawQuery}. Run as many queries as you like + * on one instance.

+ * + *

The database holds native resources (the mapped dex/cache), so it is {@link Closeable} — + * {@link #close() close} it when done, ideally with try-with-resources. Closing the database + * does not close cursors already returned by {@link #query}; close those separately. + * After {@link #close()}, calling {@link #query} throws {@link IllegalStateException}.

+ * + *
{@code
+ * // inside onPackageLoaded(PackageLoadedParam param):
+ * try (DexDatabase db = param.openDex();
+ *      Cursor c = db.query("SELECT name FROM methods WHERE class = 'Lcom/foo/Bar;'")) {
+ *     while (c.moveToNext()) {
+ *         String name = c.getString(0);
+ *     }
+ * }
+ * }
+ */ +@SinceApi(XposedInterface.API_103) +public interface DexDatabase extends Closeable { + /** + * Runs a read-only SQL query against the dex. + * + * @param sql The SQL query; only {@code SELECT}-style reads are supported + * @return A {@link Cursor} over the result; close it when done + * @throws IllegalStateException If the database has been closed + */ + @NonNull + Cursor query(@NonNull String sql); + + /** + * Releases the native resources (mapped dex/cache) held by this database. Idempotent; + * does not affect cursors already returned by {@link #query}. + */ + @Override + void close(); +} 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 c36b775..bbefcde 100644 --- a/api/src/main/java/io/github/libxposed/api/XposedInterface.java +++ b/api/src/main/java/io/github/libxposed/api/XposedInterface.java @@ -51,11 +51,21 @@ public interface XposedInterface { */ int API_102 = 102; + /** + * API version 103. + *

New features

+ * + */ + int API_103 = 103; + /** * The API version of this library. This is a static value for the framework. * Modules should use {@link #getApiVersion()} to check the API version at runtime. */ - int LIB_API = API_102; + int LIB_API = API_103; /** * The framework has the capability to hook system_server and other system processes. 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 b07bc69..cdb969b 100644 --- a/api/src/main/java/io/github/libxposed/api/XposedModuleInterface.java +++ b/api/src/main/java/io/github/libxposed/api/XposedModuleInterface.java @@ -75,6 +75,20 @@ interface PackageLoadedParam { @RequiresApi(Build.VERSION_CODES.Q) @NonNull ClassLoader getDefaultClassLoader(); + + /** + * Opens a read-only SQL view over this package's APK/DEX. The returned + * {@link DexDatabase} runs multiple {@link DexDatabase#query queries} and must be + * {@link DexDatabase#close() closed} when done (it holds the mapped dex/cache). The + * query is consumed like {@link android.database.sqlite.SQLiteDatabase#rawQuery}; only + * {@code SELECT}-style reads are supported. + * + * @return A {@link DexDatabase} over this package's dex; close it when done + * @throws UnsupportedOperationException If the framework does not support dex queries + */ + @SinceApi(XposedInterface.API_103) + @NonNull + DexDatabase openDex(); } /** From 157366e110e58151368bf1a8cc3cb7dc9afcd66f Mon Sep 17 00:00:00 2001 From: Nullptr Date: Sun, 14 Jun 2026 20:32:03 +0200 Subject: [PATCH 2/2] Update API semantics --- .../io/github/libxposed/api/DexDatabase.java | 20 ++------ .../github/libxposed/api/XposedInterface.java | 50 ++++++++++++++++++- .../libxposed/api/XposedInterfaceWrapper.java | 17 +++++++ .../libxposed/api/XposedModuleInterface.java | 14 ------ 4 files changed, 70 insertions(+), 31 deletions(-) diff --git a/api/src/main/java/io/github/libxposed/api/DexDatabase.java b/api/src/main/java/io/github/libxposed/api/DexDatabase.java index 314ebe0..30cee95 100644 --- a/api/src/main/java/io/github/libxposed/api/DexDatabase.java +++ b/api/src/main/java/io/github/libxposed/api/DexDatabase.java @@ -9,28 +9,17 @@ import io.github.libxposed.annotation.SinceApi; /** - * A read-only SQL view over an APK/DEX, obtained from - * {@link XposedModuleInterface.PackageLoadedParam#openDex()}. + * Read-only SQL view over an APK/JAR/DEX. * *

Backed by the framework's dex query engine; {@link #query} returns an * {@link android.database.Cursor} consumed like - * {@link android.database.sqlite.SQLiteDatabase#rawQuery}. Run as many queries as you like - * on one instance.

+ * {@link android.database.sqlite.SQLiteDatabase#rawQuery}. Run as many queries as you like on + * one instance.

* *

The database holds native resources (the mapped dex/cache), so it is {@link Closeable} — * {@link #close() close} it when done, ideally with try-with-resources. Closing the database * does not close cursors already returned by {@link #query}; close those separately. * After {@link #close()}, calling {@link #query} throws {@link IllegalStateException}.

- * - *
{@code
- * // inside onPackageLoaded(PackageLoadedParam param):
- * try (DexDatabase db = param.openDex();
- *      Cursor c = db.query("SELECT name FROM methods WHERE class = 'Lcom/foo/Bar;'")) {
- *     while (c.moveToNext()) {
- *         String name = c.getString(0);
- *     }
- * }
- * }
*/ @SinceApi(XposedInterface.API_103) public interface DexDatabase extends Closeable { @@ -39,7 +28,8 @@ public interface DexDatabase extends Closeable { * * @param sql The SQL query; only {@code SELECT}-style reads are supported * @return A {@link Cursor} over the result; close it when done - * @throws IllegalStateException If the database has been closed + * @throws IllegalArgumentException If the SQL is invalid or not a read-only query + * @throws IllegalStateException If the database has been closed */ @NonNull Cursor query(@NonNull String sql); 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 bbefcde..a174a7d 100644 --- a/api/src/main/java/io/github/libxposed/api/XposedInterface.java +++ b/api/src/main/java/io/github/libxposed/api/XposedInterface.java @@ -12,6 +12,7 @@ import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.ByteBuffer; import java.util.List; import io.github.libxposed.api.error.HookFailedError; @@ -55,8 +56,9 @@ public interface XposedInterface { * API version 103. *

New features

*
    - *
  • Modules can run read-only SQL queries over a loaded package's APK/DEX via - * {@link XposedModuleInterface.PackageLoadedParam#openDex}.
  • + *
  • Modules can open read-only SQL views over dex sources owned by arbitrary + * {@link ClassLoader class loaders} or supplied as in-memory APK/JAR/DEX data via + * {@link #openDex(ClassLoader)} and {@link #openDex(ByteBuffer...)}.
  • *
*/ int API_103 = 103; @@ -496,6 +498,50 @@ default int getApiVersion() { */ boolean deoptimize(@NonNull Executable executable); + /** + * Opens a read-only SQL view over dex sources owned by a class loader. + * + *

The query covers dex sources owned directly by {@code classLoader}; parent class loaders + * are not included. Framework implementations should support common Android dex class loaders + * such as {@link dalvik.system.PathClassLoader}, {@link dalvik.system.DexClassLoader}, + * {@link dalvik.system.DelegateLastClassLoader}, and + * {@link dalvik.system.InMemoryDexClassLoader}.

+ * + *

This method must not load classes from {@code classLoader}. The returned + * {@link DexDatabase} owns the mapped dex/cache resources until + * {@link DexDatabase#close() closed}; closing the database does not affect the class loader.

+ * + * @param classLoader The class loader whose dex sources should be queried + * @return A {@link DexDatabase} over the class loader's dex sources; close it when done + * @throws IllegalArgumentException If {@code classLoader} is not a supported dex class + * loader or does not contain supported dex content + */ + @SinceApi(API_103) + @NonNull + DexDatabase openDex(@NonNull ClassLoader classLoader); + + /** + * Opens a read-only SQL view over in-memory APK/JAR/DEX data. + * + *

Each source may contain a raw {@code .dex} file or a zip-based container such as an + * APK/JAR containing {@code classes*.dex} entries.

+ * + *

For each buffer, the bytes between its current {@link ByteBuffer#position() position} and + * {@link ByteBuffer#limit() limit} are used. This method must not change the buffer's + * position, limit, or mark. Heap, direct, mapped, and read-only buffers are all valid. + * Framework implementations may retain the supplied buffers until the returned + * {@link DexDatabase} is {@link DexDatabase#close() closed}; callers must not mutate the + * remaining bytes or their backing storage before then.

+ * + * @param sources One or more buffers containing APK/JAR/DEX data + * @return A {@link DexDatabase} over the supplied sources; close it when done + * @throws IllegalArgumentException If {@code sources} is empty, contains {@code null}, or + * does not contain supported dex content + */ + @SinceApi(API_103) + @NonNull + DexDatabase openDex(@NonNull ByteBuffer... sources); + /** * Get a method invoker for the given method. Invocations through invokers will bypass access * checks. The default type of the invoker is {@link Invoker.Type.Chain#FULL}. 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 fdfc9ae..ddf49b5 100644 --- a/api/src/main/java/io/github/libxposed/api/XposedInterfaceWrapper.java +++ b/api/src/main/java/io/github/libxposed/api/XposedInterfaceWrapper.java @@ -11,6 +11,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.nio.ByteBuffer; import io.github.libxposed.annotation.InternalApi; import io.github.libxposed.annotation.SinceApi; @@ -136,6 +137,22 @@ public final boolean deoptimize(@NonNull Executable executable) { return mBase.deoptimize(executable); } + @SinceApi(API_103) + @NonNull + @Override + public final DexDatabase openDex(@NonNull ClassLoader classLoader) { + ensureAttached(); + return mBase.openDex(classLoader); + } + + @SinceApi(API_103) + @NonNull + @Override + public final DexDatabase openDex(@NonNull ByteBuffer... sources) { + ensureAttached(); + return mBase.openDex(sources); + } + @NonNull @Override public final Invoker getInvoker(@NonNull Method method) { 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 cdb969b..b07bc69 100644 --- a/api/src/main/java/io/github/libxposed/api/XposedModuleInterface.java +++ b/api/src/main/java/io/github/libxposed/api/XposedModuleInterface.java @@ -75,20 +75,6 @@ interface PackageLoadedParam { @RequiresApi(Build.VERSION_CODES.Q) @NonNull ClassLoader getDefaultClassLoader(); - - /** - * Opens a read-only SQL view over this package's APK/DEX. The returned - * {@link DexDatabase} runs multiple {@link DexDatabase#query queries} and must be - * {@link DexDatabase#close() closed} when done (it holds the mapped dex/cache). The - * query is consumed like {@link android.database.sqlite.SQLiteDatabase#rawQuery}; only - * {@code SELECT}-style reads are supported. - * - * @return A {@link DexDatabase} over this package's dex; close it when done - * @throws UnsupportedOperationException If the framework does not support dex queries - */ - @SinceApi(XposedInterface.API_103) - @NonNull - DexDatabase openDex(); } /**