Skip to content

Commit c4acb6e

Browse files
authored
RFC for API 102 (#62)
1 parent edeb837 commit c4acb6e

16 files changed

Lines changed: 455 additions & 128 deletions

.github/workflows/deploy.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,12 @@ name: Deploy to Maven Central
22

33
on:
44
workflow_dispatch:
5-
inputs:
6-
confirm:
7-
description: 'Type "yes" to confirm deployment to Maven Central'
8-
required: true
9-
default: ''
105

116
permissions:
127
contents: read
138

149
jobs:
1510
deploy:
16-
if: github.event.inputs.confirm == 'yes'
1711
runs-on: ubuntu-latest
1812

1913
steps:

.github/workflows/snapshot.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Publish Sonatype Snapshot
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
dependency_snapshot:
7+
description: 'Use snapshot annotation and lint dependencies'
8+
type: boolean
9+
default: false
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
publish:
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
submodules: 'recursive'
22+
fetch-depth: 0
23+
24+
- name: set up JDK
25+
uses: actions/setup-java@v4
26+
with:
27+
java-version: '21'
28+
distribution: 'temurin'
29+
cache: gradle
30+
31+
- name: Publish snapshot to Sonatype
32+
run: |
33+
echo 'org.gradle.caching=true' >> gradle.properties
34+
echo 'org.gradle.parallel=true' >> gradle.properties
35+
echo 'org.gradle.vfs.watch=true' >> gradle.properties
36+
echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties
37+
echo 'publishSnapshot=true' >> gradle.properties
38+
echo 'dependencySnapshot=${{ github.event.inputs.dependency_snapshot }}' >> gradle.properties
39+
./gradlew publishApiPublicationToSnapshotsRepository
40+
./gradlew --stop
41+
env:
42+
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.maven_pgp_signingKey }}
43+
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.maven_pgp_signingPassword }}
44+
ORG_GRADLE_PROJECT_snapshotsUsername: ${{ secrets.OSSRHUSERNAME }}
45+
ORG_GRADLE_PROJECT_snapshotsPassword: ${{ secrets.OSSRHPASSWORD }}

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# libxposed API
22

3-
[![API](https://img.shields.io/badge/API-101-brightgreen)](https://github.com/libxposed/api)
3+
[![API](https://img.shields.io/badge/API-102-brightgreen)](https://github.com/libxposed/api)
44
[![Maven Central](https://img.shields.io/maven-central/v/io.github.libxposed/api?color=blue)](https://central.sonatype.com/artifact/io.github.libxposed/api)
55
[![Android Min SDK](https://img.shields.io/badge/minSdk-26-orange)](https://developer.android.com/about/versions/oreo)
66
[![License](https://img.shields.io/github/license/libxposed/api)](LICENSE)
@@ -13,15 +13,15 @@ Modern Xposed Module API — a type-safe, redesigned replacement for the legacy
1313

1414
```kotlin
1515
dependencies {
16-
compileOnly("io.github.libxposed:api:101.0.1")
16+
compileOnly("io.github.libxposed:api:102.0.0")
1717
}
1818
```
1919

2020
### For Framework Developers
2121

2222
```kotlin
2323
dependencies {
24-
implementation("io.github.libxposed:api:101.0.1")
24+
implementation("io.github.libxposed:api:102.0.0")
2525
}
2626
```
2727

api/build.gradle.kts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ plugins {
66

77
android {
88
namespace = "io.github.libxposed.api"
9-
compileSdk = 36
10-
buildToolsVersion = "36.1.0"
9+
compileSdk = 37
10+
buildToolsVersion = "37.0.0"
1111
androidResources.enable = false
1212
enableKotlin = false
1313

@@ -32,13 +32,22 @@ android {
3232
}
3333
}
3434

35+
val libVersion = "102.0.0"
36+
val publishSnapshot = providers.gradleProperty("publishSnapshot").orNull == "true"
37+
val dependencySnapshot = providers.gradleProperty("dependencySnapshot").orNull == "true"
38+
fun String.real(snapshot: Boolean) = if (snapshot) "$this-SNAPSHOT" else this
39+
val libxposedAnnotation = "io.github.libxposed:annotation:" + libs.versions.libxposed.annotation.get()
40+
val libxposedLint = "io.github.libxposed:lint:" + libs.versions.libxposed.lint.get()
41+
3542
dependencies {
36-
compileOnly(libs.annotation)
43+
compileOnly(libs.androidx.annotation)
44+
compileOnly(libxposedAnnotation.real(dependencySnapshot))
45+
lintPublish(libxposedLint.real(dependencySnapshot))
3746
}
3847

3948
val androidJavadoc by tasks.registering(Javadoc::class) {
40-
title = "libxposed API $version"
41-
source(android.sourceSets["main"].java.srcDirs)
49+
title = "libxposed API $libVersion"
50+
source(layout.projectDirectory.dir("src/main/java"))
4251
destinationDir = layout.buildDirectory.dir("javadoc").get().asFile
4352

4453
(options as StandardJavadocDocletOptions).apply {
@@ -71,7 +80,7 @@ publishing {
7180
register<MavenPublication>("api") {
7281
artifactId = "api"
7382
group = "io.github.libxposed"
74-
version = "101.0.1"
83+
version = libVersion.real(publishSnapshot)
7584
artifact(javadocJar)
7685
pom {
7786
name.set("api")
@@ -105,6 +114,11 @@ publishing {
105114
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
106115
credentials(PasswordCredentials::class)
107116
}
117+
maven {
118+
name = "snapshots"
119+
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
120+
credentials(PasswordCredentials::class)
121+
}
108122
maven {
109123
name = "GitHubPackages"
110124
url = uri("https://maven.pkg.github.com/libxposed/api")

api/proguard-rules.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
-dontwarn io.github.libxposed.annotation.**
12
-adaptresourcefilecontents META-INF/xposed/java_init.list
23
-keep,allowoptimization,allowobfuscation public class * extends io.github.libxposed.api.XposedModule {
34
public <init>();

api/src/main/java/io/github/libxposed/api/XposedInterface.java

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,47 @@
1515
import java.util.List;
1616

1717
import io.github.libxposed.api.error.HookFailedError;
18+
import io.github.libxposed.annotation.SinceApi;
1819

1920
/**
2021
* Xposed interface for modules to operate on application processes.
2122
*/
2223
@SuppressWarnings("unused")
2324
public interface XposedInterface {
2425
/**
25-
* Behavior changes: all modules
26+
* API version 101.
27+
* <p>Behavior changes: all modules</p>
2628
* <ul>
27-
* <li> Modules cannot be injected into zygote;
29+
* <li>Modules cannot be injected into zygote;
2830
* they are only loaded within the process of the scope.</li>
2931
* </ul>
30-
* Behavior changes: Modules targeting 101 or higher
32+
* <p>Behavior changes: Modules targeting 101 or higher</p>
3133
* <ul>
3234
* <li>This is the first API version.</li>
3335
* </ul>
3436
*/
3537
int API_101 = 101;
3638

39+
/**
40+
* API version 102.
41+
* <p>New features</p>
42+
* <ul>
43+
* <li>Hot reload allows modules to be updated without restarting the process.</li>
44+
* <li>Module entries can stop receiving subsequent lifecycle callbacks.</li>
45+
* <li>Hooks can be atomically replaced by api or same id.</li>
46+
* </ul>
47+
* <p>Behavior changes: Modules targeting 102 or higher</p>
48+
* <ul>
49+
* <li>Libxposed modules can not call legacy {@code de.robv.android.xposed} APIs.</li>
50+
* </ul>
51+
*/
52+
int API_102 = 102;
53+
3754
/**
3855
* The API version of this <b>library</b>. This is a static value for the framework.
3956
* Modules should use {@link #getApiVersion()} to check the API version at runtime.
4057
*/
41-
int LIB_API = API_101;
58+
int LIB_API = API_102;
4259

4360
/**
4461
* The framework has the capability to hook system_server and other system processes.
@@ -279,6 +296,36 @@ interface HookHandle {
279296
* Cancels the hook. This method is idempotent. It is safe to call this method multiple times.
280297
*/
281298
void unhook();
299+
300+
/**
301+
* Gets the unique id of the hook, or null if the hook is not assigned with an id.
302+
*/
303+
@SinceApi(API_102)
304+
@Nullable
305+
String getId();
306+
307+
/**
308+
* Atomically replaces this hook with a new hooker and returns the new hook handle.
309+
* <p>
310+
* The replacement keeps the executable, priority, exception handling mode, and id of this hook.
311+
* For a hook with an id, this targets the same hook as creating a new hook on the same executable
312+
* with the same id. This method is the handle-based form of replacement and can also replace a
313+
* hook without an id. It is useful during hot reloading when new code receives old hook handles
314+
* from {@link XposedModuleInterface.HotReloadedParam#getOldHookHandles()}. After a successful
315+
* replacement, this handle is no longer valid.
316+
* </p>
317+
* <p>The hook chain is snapshot based. Replacing a hook while a call is running does not affect
318+
* that in-flight call.</p>
319+
*
320+
* @param hooker The new hooker object
321+
* @return The new handle for the replaced hook
322+
* @throws IllegalArgumentException if hooker is invalid
323+
* @throws IllegalStateException if this hook handle is no longer valid
324+
* @throws HookFailedError if replacement fails due to framework internal error
325+
*/
326+
@SinceApi(API_102)
327+
@NonNull
328+
HookHandle replaceHook(@NonNull Hooker hooker);
282329
}
283330

284331
/**
@@ -344,10 +391,25 @@ interface HookBuilder {
344391
*/
345392
@NonNull
346393
HookHandle intercept(@NonNull Hooker hooker);
394+
395+
/**
396+
* Sets a unique id for the hook, default to {@code null}. An id is used for exclusively identifying
397+
* a hook in the same module on the executable. A new hook with the same id in the same module on
398+
* the executable will replace the old one atomically, and the old hook handle will be invalid.
399+
* Hook ids are isolated between modules.
400+
*
401+
* <p>The hook chain is snapshot based. Replacing or adding a hook while a call is running does not
402+
* affect that in-flight call.</p>
403+
*
404+
* @param id The id for the hook. It can be null if you don't care about replacing the hook later.
405+
* @return The builder itself for chaining
406+
*/
407+
@SinceApi(API_102)
408+
HookBuilder setId(@Nullable String id);
347409
}
348410

349411
/**
350-
* Gets the runtime Xposed API version. Framework implementations must <b>not</b> override this method.
412+
* Gets the runtime Xposed API version. Framework implementations <b>must not</b> override this method.
351413
*/
352414
default int getApiVersion() {
353415
return LIB_API;

api/src/main/java/io/github/libxposed/api/XposedInterfaceWrapper.java

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,32 @@
1212
import java.lang.reflect.Executable;
1313
import java.lang.reflect.Method;
1414

15+
import io.github.libxposed.annotation.InternalApi;
16+
import io.github.libxposed.annotation.SinceApi;
17+
1518
/**
1619
* Wrapper of {@link XposedInterface} used by modules to shield framework implementation details.
1720
*/
1821
public class XposedInterfaceWrapper implements XposedInterface {
1922

20-
private volatile XposedInterface mBase;
23+
private XposedInterface mBase;
24+
private Runnable mDetachImpl;
2125

2226
/**
23-
* Attaches the framework interface to the module. Modules should never call this method.
27+
* Attaches the framework interface to the module. Modules <b>must not</b> call this method.
28+
* It is reserved for framework implementations and may change without compatibility guarantees.
2429
*
25-
* @param base The framework interface
30+
* @param base The framework interface
31+
* @param detachImpl The implementation of {@link #detach()}
2632
*/
33+
@InternalApi
2734
@SuppressWarnings("unused")
28-
public final void attachFramework(@NonNull XposedInterface base) {
35+
public final void attachFramework(@NonNull XposedInterface base, @NonNull Runnable detachImpl) {
2936
if (mBase != null) {
3037
throw new IllegalStateException("Framework already attached");
3138
}
3239
mBase = base;
40+
mDetachImpl = detachImpl;
3341
}
3442

3543
private void ensureAttached() {
@@ -38,6 +46,44 @@ private void ensureAttached() {
3846
}
3947
}
4048

49+
/**
50+
* Stops all subsequent lifecycle callbacks for the <b>current module entry</b> in the current
51+
* process. After this method is called, the framework removes its reference to the entry
52+
* instance and will no longer invoke any lifecycle callbacks (such as
53+
* {@link XposedModuleInterface#onPackageLoaded},
54+
* {@link XposedModuleInterface#onHotReloading}, etc.) on the entry instance that
55+
* called this method. Only lifecycle callbacks are affected; all {@link XposedInterface} APIs
56+
* remain fully functional.
57+
*
58+
* <p>If the module declares multiple entry classes, only the entry that calls this method is
59+
* affected. Other entries continue to receive their lifecycle callbacks as normal.</p>
60+
*
61+
* <p>This method is idempotent. Calling it multiple times has the same effect as calling it once.</p>
62+
*
63+
* <p>If the module expects its classloader to become collectible after detaching, it must also
64+
* remove module-owned references and execution contexts that keep module objects reachable, such
65+
* as installed hooks, Java threads, and callbacks held by system or app objects. If native code
66+
* is still running after all Java references to the module classloader are cleared, later runtime
67+
* unloading of native libraries may crash the process; this is a module lifecycle bug.</p>
68+
*
69+
* <p>Typical use cases include:</p>
70+
* <ul>
71+
* <li>The module entry has finished all its initialization work and no longer needs to
72+
* respond to further package loading events.</li>
73+
* <li>For modules that target multiple apps with a dedicated entry class per app: if the
74+
* entry detects it is not loaded in its target app, it can call this method immediately to
75+
* avoid receiving any further callbacks.</li>
76+
* <li>Calling this method together with unhooking all registered hooks, so that the module
77+
* classloader can be garbage collected when no longer needed.</li>
78+
* </ul>
79+
*/
80+
@SinceApi(API_102)
81+
@SuppressWarnings("unused")
82+
public final void detach() {
83+
ensureAttached();
84+
mDetachImpl.run();
85+
}
86+
4187
@Override
4288
public final int getApiVersion() {
4389
ensureAttached();

api/src/main/java/io/github/libxposed/api/XposedModule.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
/**
44
* Super class which all Xposed module entry classes should extend.<br/>
5-
* Entry classes will be instantiated exactly once for each process. Modules should not do initialization
6-
* work before {@link #onModuleLoaded(ModuleLoadedParam)} is called.
5+
* Entry classes will be instantiated once for each loaded module generation in a process.
76
*/
87
@SuppressWarnings("unused")
98
public abstract class XposedModule extends XposedInterfaceWrapper implements XposedModuleInterface {

0 commit comments

Comments
 (0)