Skip to content

NPatch Remote API Developer Guide

NPatch Remote API shares small preferences and files between a module app (its settings UI) and Local-mode target processes. NPatch Manager stores the data and isolates it by module package.

The SDK is maintained as a standalone open-source project at 7723mod/NPatch-Remote-API. The public client, versions, AAR, lower-level API, and security documentation are all maintained on this page; the repository is used for source code and release builds.

It is not a general IPC channel and does not replace module scope:

  • The module app obtains a standard writable IXposedService through NPatchRemoteClient
  • API 101/102 code inside a target reads Remote Preferences and Remote Files through libxposed XposedInterface
  • Manager forwards module-app preference changes to injected API 101/102 runtimes
  • Legacy XSharedPreferences is not migrated automatically; Legacy modules must adopt this API explicitly

Relationship to libxposed

NPatch Remote API is not another Xposed API and does not replace libxposed.

They belong to different processes and lifecycles. XposedInterface is supplied to module code injected into a target app; XposedService is the service used by the module/settings app to communicate with the framework or Manager. NPatch Remote API does not participate in target-process injection and does not provide XposedInterface; it only adds a module-side entry point to the same API 102 IXposedService contract in NPatch Local mode.

ChannelProcessPurposeDelivery
XposedInterfaceInjected target appModule entry, hooks, and target lifecyclelibxposed lifecycle callbacks such as XposedModule.attachFramework(...)
XposedServiceModule or settings appScope, Remote Preferences, Remote Files, and hot reload<module-package>.XposedService + XposedServiceHelper.registerListener(...)
NPatchRemoteClientModule settings appExplicit/fallback NPatch Local connectionAuthenticated Manager Provider returning the same API 102 contract

The module settings app is encouraged to use NPatchRemoteClient for a more complete, consistent Local-mode experience for users without Root or Shizuku. If the module already receives XposedService through the standard libxposed path, it may continue using XposedServiceHelper.registerListener(...). Do not create NPatchRemoteClient in target-process code.

Scope and Requirements

The public NPatchRemoteClient is for Local mode with NPatch Manager installed. Integrated mode has no Manager Provider and uses a target-local Store that the public module-app client cannot reach.

Requirements:

  • NPatch v1.0.7 or newer
  • Android 9 (API 28) or newer
  • The module is installed and recognized by NPatch Manager
  • The module app's real package name matches the package registered in NPatch

No Root, Shizuku, or extra Android permission is required.

Add the SDK

Download the AAR from NPatch Remote API Releases:

text
npatch-remote-api-v<SDK-version>-release.aar

Copy it to app/libs/ in the module app and add:

kotlin
dependencies {
    implementation(files("libs/npatch-remote-api-v1.0.0-release.aar"))

    // io.github.libxposed:service:102.0.0 normally brings this interface
    // transitively. Add it explicitly if the module app does not use service.
    implementation("io.github.libxposed:interface:102.0.0")
}

Declare package visibility for the NPatch Provider; otherwise isAvailable() cannot resolve the Provider on Android 11 and newer:

xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <queries>
        <provider android:authorities="top.nkbe.npatch.remote" />
    </queries>

    <application>
        <!-- Existing module-app declarations -->
    </application>
</manifest>

Connect

Connecting may start the NPatch Manager process. Prefer the asynchronous API from UI:

kotlin
import top.nkbe.npatch.remote.NPatchRemoteClient

if (!NPatchRemoteClient.isAvailable(applicationContext)) {
    // Hide NPatch-only settings or use the module's local fallback.
    return
}

NPatchRemoteClient.connectAsync(applicationContext)
    .whenComplete { client, error ->
        runOnUiThread {
            if (error != null) {
                // NPatch is missing, the module is unknown, or the service is unavailable.
                return@runOnUiThread
            }
            useRemoteApi(client)
        }
    }

isAvailable() only checks whether the Provider resolves. It does not prove that module authentication will succeed; the connection result remains authoritative.

connect(context) uses the calling app's package. Avoid the package-name overload in normal apps; the Provider verifies that the Binder caller UID actually owns the requested module package.

For a custom Manager application ID, pass both the module package and its ${applicationId}.remote authority:

kotlin
val client = NPatchRemoteClient.connect(
    applicationContext,
    applicationContext.packageName,
    "your.manager.application.id.remote"
)

Synchronous connect() has a three-second bound and belongs on a worker thread. connectAsync() completes on its executor, not the main thread.

API Reference

The only public entry point is top.nkbe.npatch.remote.NPatchRemoteClient.

Constants

  • DEFAULT_AUTHORITY: the official NPatch Manager Remote Provider authority, top.nkbe.npatch.remote.

Probing

  • isAvailable(Context): checks whether the system can resolve the official Manager provider. It only means the provider exists, not that this module will pass authentication.
  • isAvailable(Context, String authority): checks a custom Manager authority.

Connecting

  • connect(Context): synchronous connect using the calling app's package and the official authority.
  • connect(Context, String modulePackageName): connect with a given module package and the official authority.
  • connect(Context, String modulePackageName, String authority): full entry point; times out after three seconds.
  • connectAsync(...): CompletableFuture counterparts of the synchronous entries.
  • connectService(...): advanced entry that returns the libxposed IXposedService directly. Prefer the client wrapper unless you need the raw Binder.

Remote Preferences

  • getRemotePreferences(String group): returns a remote group implementing Android SharedPreferences. The same group is cached per client.
  • deleteRemotePreferences(String group): deletes the remote group and clears the current client's cached snapshot.

Remote Files

  • listRemoteFiles(): returns a sorted array of flat file names.
  • openRemoteFile(String name): returns a writable ParcelFileDescriptor. The caller closes the descriptor and any stream built on it.
  • deleteRemoteFile(String name): deletes a remote file and reports whether it succeeded.

Threading

A client is safe to share across threads; the Preferences snapshot and listener set are concurrency-protected. Synchronous Binder and connect calls can still block, so use the async entries from UI code or switch threads yourself.

Remote Preferences

The module app receives a standard SharedPreferences view:

kotlin
private fun useRemoteApi(client: NPatchRemoteClient) {
    val preferences = client.getRemotePreferences("settings")
    val enabled = preferences.getBoolean("enabled", false)

    val saved = preferences.edit()
        .putBoolean("enabled", !enabled)
        .putString("mode", "safe")
        .commit()

    if (!saved) {
        // The Binder call or persistence failed.
    }
}

Supported value types:

  • Boolean
  • Int
  • Long
  • Float
  • String
  • Set<String>

A group name must not be empty and cannot contain / or \.

commit() waits for Manager persistence and reports the result. apply() updates the client snapshot first and writes in the background; Android's SharedPreferences.apply() cannot report asynchronous failures, and an early process exit may drop a pending apply() write.

OnSharedPreferenceChangeListener observes local changes made through the same client instance. Writes from another module-UI process do not refresh an existing client snapshot; reconnect to read the latest state. Injected API 101/102 Remote Preferences receive Manager-side change callbacks.

Delete a whole group:

kotlin
client.deleteRemotePreferences("settings")

Remote Files

Remote Files accept one flat filename. /, \, ., and .. are rejected:

kotlin
import java.io.FileInputStream
import java.io.FileOutputStream

client.openRemoteFile("rules.json").use { descriptor ->
    FileOutputStream(descriptor.fileDescriptor).use { output ->
        output.write("""{"enabled":true}""".toByteArray())
    }
}

client.openRemoteFile("rules.json").use { descriptor ->
    FileInputStream(descriptor.fileDescriptor).bufferedReader().use { input ->
        val json = input.readText()
    }
}

val names: Array<String> = client.listRemoteFiles()
val deleted: Boolean = client.deleteRemoteFile("rules.json")

Module-app descriptors are writable. API 101/102 target runtimes receive read-only descriptors.

API 101/102 Runtime Side

Do not create NPatchRemoteClient inside a target process. Use the libxposed XposedInterface supplied to the module:

java
SharedPreferences preferences = getRemotePreferences("settings");
boolean enabled = preferences.getBoolean("enabled", false);

try (ParcelFileDescriptor file = openRemoteFile("rules.json")) {
    // read only
}

Injected Remote Preferences are read-only; edit() throws UnsupportedOperationException. The module app performs writes. This matches the Vector/libxposed API 101/102 service split.

Check the framework capability before use:

java
boolean supported =
        (getFrameworkProperties() & XposedInterface.PROP_CAP_REMOTE) != 0;

Security and Data Boundary

The module app calls getRemoteService on the Manager's RemoteApiProvider through the ContentResolver and receives an IXposedService Binder. The Provider must be exported to serve module apps, but it does not trust the package name passed in the extras alone; Manager verifies all of the following:

  1. The package exists in NPatch's module database.
  2. The package belongs to the Binder calling UID.
  3. The returned service Binder keeps coming from the original UID on every later call.

So a forged package name in the extras cannot read another module's data.

Preferences and Files are partitioned by module package. Group and file names reject path separators, and Files only accept a single flat name, to prevent path traversal.

Patched targets use a separate, framework-internal, read-only injected service. This SDK does not include that AIDL and provides no public way to obtain it, so a module app or third-party app cannot impersonate an injected target. The IXposedService obtained by the SDK may still expose API 102 methods such as scope, running targets, or hot reload; NPatch Manager validates them against the module UID, and the Remote SDK neither wraps nor loosens them.

Threat-model limits:

  • If Android allows an attacker to run code with the same UID, that code already shares the app's trust boundary.
  • isAvailable only probes the provider; it is not proof of successful authentication.
  • Binder objects must not be handed to other processes; Manager rejects later calls from a different UID.
  • The SDK does not encrypt Remote Files. Data lives in Manager's private directory, and security relies on the Android sandbox and the device.

Error Handling

Common failures:

  • IllegalStateException: NPatch is missing, Provider startup failed, or connection timed out
  • SecurityException: the module is unknown or the caller UID does not own its package
  • RemoteException: Manager Binder died during an operation
  • FileNotFoundException: Manager returned no valid file descriptor; the writable endpoint normally creates a missing file

Returned Binders are bound to the original caller UID and cannot be handed to another app. Scoped targets receive only the read-only injected Binder and cannot impersonate the module app to write data.

An existing Binder can go stale after a Manager update, a system stop, or a process rebuild. When an operation hits a RemoteException, discard the client and reconnect later.

Use Remote Store for preferences, rules, and small state files. Use the module's own IPC or ContentProvider for databases, bulk data, or high-frequency messages.

NPatch Official Documentation