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
IXposedServicethroughNPatchRemoteClient - 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
XSharedPreferencesis 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.
| Channel | Process | Purpose | Delivery |
|---|---|---|---|
XposedInterface | Injected target app | Module entry, hooks, and target lifecycle | libxposed lifecycle callbacks such as XposedModule.attachFramework(...) |
XposedService | Module or settings app | Scope, Remote Preferences, Remote Files, and hot reload | <module-package>.XposedService + XposedServiceHelper.registerListener(...) |
NPatchRemoteClient | Module settings app | Explicit/fallback NPatch Local connection | Authenticated 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:
npatch-remote-api-v<SDK-version>-release.aarCopy it to app/libs/ in the module app and add:
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:
<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:
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:
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(...):CompletableFuturecounterparts of the synchronous entries.connectService(...): advanced entry that returns the libxposedIXposedServicedirectly. Prefer the client wrapper unless you need the raw Binder.
Remote Preferences
getRemotePreferences(String group): returns a remote group implementing AndroidSharedPreferences. 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 writableParcelFileDescriptor. 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:
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:
BooleanIntLongFloatStringSet<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:
client.deleteRemotePreferences("settings")Remote Files
Remote Files accept one flat filename. /, \, ., and .. are rejected:
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:
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:
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:
- The package exists in NPatch's module database.
- The package belongs to the Binder calling UID.
- 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.
isAvailableonly 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 outSecurityException: the module is unknown or the caller UID does not own its packageRemoteException: Manager Binder died during an operationFileNotFoundException: 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.