Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@ name: Deploy to Maven Central

on:
workflow_dispatch:
inputs:
confirm:
description: 'Type "yes" to confirm deployment to Maven Central'
required: true
default: ''

permissions:
contents: read

jobs:
deploy:
Comment thread
Dr-TSNG marked this conversation as resolved.
if: github.event.inputs.confirm == 'yes'
runs-on: ubuntu-latest

steps:
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/snapshot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Publish Sonatype Snapshot

on:
workflow_dispatch:
inputs:
dependency_snapshot:
description: 'Use snapshot annotation and lint dependencies'
type: boolean
default: false

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
submodules: 'recursive'
fetch-depth: 0

- name: set up JDK
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: gradle

- name: Publish snapshot to Sonatype
run: |
echo 'org.gradle.caching=true' >> gradle.properties
echo 'org.gradle.parallel=true' >> gradle.properties
echo 'org.gradle.vfs.watch=true' >> gradle.properties
echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties
echo 'publishSnapshot=true' >> gradle.properties
echo 'dependencySnapshot=${{ github.event.inputs.dependency_snapshot }}' >> gradle.properties
./gradlew publishApiPublicationToSnapshotsRepository
./gradlew --stop
env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.maven_pgp_signingKey }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.maven_pgp_signingPassword }}
ORG_GRADLE_PROJECT_snapshotsUsername: ${{ secrets.OSSRHUSERNAME }}
ORG_GRADLE_PROJECT_snapshotsPassword: ${{ secrets.OSSRHPASSWORD }}
26 changes: 20 additions & 6 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ plugins {

android {
namespace = "io.github.libxposed.api"
compileSdk = 36
buildToolsVersion = "36.1.0"
compileSdk = 37
buildToolsVersion = "37.0.0"
androidResources.enable = false
enableKotlin = false

Expand All @@ -32,13 +32,22 @@ android {
}
}

val libVersion = "102.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
val libxposedAnnotation = "io.github.libxposed:annotation:" + libs.versions.libxposed.annotation.get()
val libxposedLint = "io.github.libxposed:lint:" + libs.versions.libxposed.lint.get()

dependencies {
compileOnly(libs.annotation)
compileOnly(libs.androidx.annotation)
compileOnly(libxposedAnnotation.real(dependencySnapshot))
lintPublish(libxposedLint.real(dependencySnapshot))
}

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

(options as StandardJavadocDocletOptions).apply {
Expand Down Expand Up @@ -71,7 +80,7 @@ publishing {
register<MavenPublication>("api") {
artifactId = "api"
group = "io.github.libxposed"
version = "101.0.1"
version = libVersion.real(publishSnapshot)
artifact(javadocJar)
pom {
name.set("api")
Expand Down Expand Up @@ -105,6 +114,11 @@ publishing {
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
credentials(PasswordCredentials::class)
}
maven {
name = "snapshots"
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
credentials(PasswordCredentials::class)
}
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/libxposed/api")
Expand Down
1 change: 1 addition & 0 deletions api/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
-dontwarn io.github.libxposed.annotation.**
-adaptresourcefilecontents META-INF/xposed/java_init.list
-keep,allowoptimization,allowobfuscation public class * extends io.github.libxposed.api.XposedModule {
public <init>();
Expand Down
82 changes: 77 additions & 5 deletions api/src/main/java/io/github/libxposed/api/XposedInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,52 @@
import java.util.List;

import io.github.libxposed.api.error.HookFailedError;
import io.github.libxposed.annotation.SinceApi;

/**
* Xposed interface for modules to operate on application processes.
*/
@SuppressWarnings("unused")
public interface XposedInterface {
/**
* Behavior changes: all modules
* API version 101.
* <p>Behavior changes: all modules</p>
* <ul>
* <li> Modules cannot be injected into zygote;
* <li>Modules cannot be injected into zygote;
* they are only loaded within the process of the scope.</li>
* </ul>
* Behavior changes: Modules targeting 101 or higher
* <p>Behavior changes: Modules targeting 101 or higher</p>
* <ul>
* <li>This is the first API version.</li>
* </ul>
*/
int API_101 = 101;

/**
* API version 102.
* <p>API additions:</p>
* <ul>
* <li>Hot reloading callbacks are available for modules that declare exactly one Java entry class.</li>
* <li>Module entries can stop receiving subsequent lifecycle callbacks through
* {@link XposedInterfaceWrapper#detach()}.</li>
* <li>Hooks can be assigned an id through {@link HookBuilder#setId(String)}. Hook ids are
* scoped to the current module and executable, and can be queried through
* {@link HookHandle#getId()}.</li>
* <li>Hooks can be atomically replaced through {@link HookHandle#replaceHook(Hooker)}.</li>
* <li>{@link #PROP_RT_HOT_RELOAD} indicates whether hot reload is currently permitted.</li>
* </ul>
* <p>Behavior changes: Modules targeting 102 or higher</p>
* <ul>
* <li>Libxposed modules must not call legacy {@code de.robv.android.xposed} APIs.</li>
* </ul>
*/
int API_102 = 102;

/**
* The API version of this <b>library</b>. This is a static value for the framework.
* Modules should use {@link #getApiVersion()} to check the API version at runtime.
*/
int LIB_API = API_101;
int LIB_API = API_102;

/**
* The framework has the capability to hook system_server and other system processes.
Expand All @@ -53,6 +75,11 @@ public interface XposedInterface {
*/
long PROP_RT_API_PROTECTION = 1L << 2;

/**
* The framework currently permits hot reload through the service.
*/
long PROP_RT_HOT_RELOAD = 1L << 3;

/**
* The default hook priority.
*/
Expand Down Expand Up @@ -279,6 +306,36 @@ interface HookHandle {
* Cancels the hook. This method is idempotent. It is safe to call this method multiple times.
*/
void unhook();

/**
* Gets the unique id of the hook, or null if the hook is not assigned with an id.
*/
@SinceApi(API_102)
@Nullable
String getId();

/**
* Atomically replaces this hook with a new hooker and returns the new hook handle.
* <p>
* The replacement keeps the executable, priority, exception handling mode, and id of this hook.
* For a hook with an id, this targets the same hook as creating a new hook on the same executable
* with the same id. This method is the handle-based form of replacement and can also replace a
* hook without an id. It is useful during hot reloading when new code receives old hook handles
* from {@link XposedModuleInterface.HotReloadedParam#getOldHookHandles()}. After a successful
* replacement, this handle is no longer valid.
* </p>
* <p>The hook chain is snapshot based. Replacing a hook while a call is running does not affect
* that in-flight call.</p>
*
* @param hooker The new hooker object
* @return The new handle for the replaced hook
* @throws IllegalArgumentException if hooker is invalid
* @throws IllegalStateException if this hook handle is no longer valid
* @throws HookFailedError if replacement fails due to framework internal error
*/
@SinceApi(API_102)
@NonNull
HookHandle replaceHook(@NonNull Hooker hooker);
}

/**
Expand Down Expand Up @@ -344,10 +401,25 @@ interface HookBuilder {
*/
@NonNull
HookHandle intercept(@NonNull Hooker hooker);

/**
* Sets a unique id for the hook, default to {@code null}. An id is used for exclusively identifying
* a hook in the same module on the executable. A new hook with the same id in the same module on
* the executable will replace the old one atomically, and the old hook handle will be invalid.
* Hook ids are isolated between modules.
*
* <p>The hook chain is snapshot based. Replacing or adding a hook while a call is running does not
* affect that in-flight call.</p>
*
* @param id The id for the hook. It can be null if you don't care about replacing the hook later.
* @return The builder itself for chaining
*/
@SinceApi(API_102)
HookBuilder setId(@Nullable String id);
}

/**
* Gets the runtime Xposed API version. Framework implementations must <b>not</b> override this method.
* Gets the runtime Xposed API version. Framework implementations <b>must not</b> override this method.
*/
default int getApiVersion() {
return LIB_API;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,32 @@
import java.lang.reflect.Executable;
import java.lang.reflect.Method;

import io.github.libxposed.annotation.InternalApi;
import io.github.libxposed.annotation.SinceApi;

/**
* Wrapper of {@link XposedInterface} used by modules to shield framework implementation details.
*/
public class XposedInterfaceWrapper implements XposedInterface {

private volatile XposedInterface mBase;
private XposedInterface mBase;
private Runnable mDetachImpl;
Comment thread
Dr-TSNG marked this conversation as resolved.

/**
* Attaches the framework interface to the module. Modules should never call this method.
* Attaches the framework interface to the module. Modules <b>must not</b> call this method.
* It is reserved for framework implementations and may change without compatibility guarantees.
*
* @param base The framework interface
* @param base The framework interface
* @param detachImpl The implementation of {@link #detach()}
*/
@InternalApi
@SuppressWarnings("unused")
public final void attachFramework(@NonNull XposedInterface base) {
public final void attachFramework(@NonNull XposedInterface base, @NonNull Runnable detachImpl) {
if (mBase != null) {
throw new IllegalStateException("Framework already attached");
}
mBase = base;
mDetachImpl = detachImpl;
}

private void ensureAttached() {
Expand All @@ -38,6 +46,37 @@ private void ensureAttached() {
}
}

/**
* Stops all subsequent lifecycle callbacks for the <b>current module entry</b> in the current
* process. After this method is called, the framework will no longer invoke any lifecycle
* callbacks (such as {@link XposedModuleInterface#onPackageLoaded},
* {@link XposedModuleInterface#onHotReloading}, etc.) on the entry instance that
* called this method. Only lifecycle callbacks are affected; all {@link XposedInterface} APIs
* remain fully functional.
*
* <p>If the module declares multiple entry classes, only the entry that calls this method is
* affected. Other entries continue to receive their lifecycle callbacks as normal.</p>
*
* <p>This method is idempotent. Calling it multiple times has the same effect as calling it once.</p>
*
* <p>Typical use cases include:</p>
* <ul>
* <li>The module entry has finished all its initialization work and no longer needs to
* respond to further package loading events.</li>
* <li>For modules that target multiple apps with a dedicated entry class per app: if the
* entry detects it is not loaded in its target app, it can call this method immediately to
* avoid receiving any further callbacks.</li>
* <li>Calling this method together with unhooking all registered hooks, so that the module
* classloader can be garbage collected when no longer needed.</li>
* </ul>
*/
@SinceApi(API_102)
@SuppressWarnings("unused")
public final void detach() {
ensureAttached();
mDetachImpl.run();
}

@Override
public final int getApiVersion() {
ensureAttached();
Expand Down
3 changes: 1 addition & 2 deletions api/src/main/java/io/github/libxposed/api/XposedModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

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