The
:ipcGradle module is a small Android library that defines the contract LineCode uses to talk to third-party, in-process-isolated shell providers. The bundled:terminal-provideris the reference implementation; you can write your own provider app and LineCode will auto-detect, bind, and route shell + file ops through it.
- Why IPC?
- Architecture at a glance
- Concepts
- Protocol surface
- Permissions & security model
- Tutorial: build your own provider in 5 minutes
- Tutorial: consume a provider from the client side
- Lifecycle, state machine & state listeners
- Packaging & building
- Versioning & compatibility
- Troubleshooting
- API reference (cheat sheet)
LineCode lets the model run shell commands and read / write files. On Android there are three ways to host that:
| Where shell runs | Trade-off |
|---|---|
| In the app's own process | Simplest. But every command runs with the app's full permissions, the app's UID, and direct access to the app's data. A buggy or compromised model can wipe user data. |
| In Termux (user-space) | Sandboxed in a separate package, but Termux is an external dependency the user has to install and grant RUN_COMMAND to. |
| In a dedicated provider app via IPC | A normal Android app you (or anyone) ships independently. It runs in its own UID, its own data directory, its own process. The caller (LineCode) can only invoke the methods exposed by AIDL, and only if it holds the right permission. |
The :ipc module makes option three trivial: define a new AIDL interface, ship a tiny app, and LineCode discovers it at runtime.
Two examples:
- A privileged "root" provider — runs commands as
rootviasu, in a different UID than LineCode. Useful for system-modding workflows. - A Docker / sandbox provider — runs every command inside a disposable container, so even if the model misbehaves the blast radius is a fresh container.
- A read-only provider — exposes only
listDir,readFile,fileExistsand rejects everything else.
The contract is the same for all of them.
┌─────────────────────────────────┐ ┌─────────────────────────────────────┐
│ :app (cn.lineai) │ │ :ipc library (cn.lineai.ipc) │
│ ───────────────────────────── │ │ ───────────────────────────────── │
│ │ addStateListener(...) │ │
│ IpcProviderManager ◄───────────┼───────────── observes ─────────┤ IpcProviderStateListener │
│ │ │ │ IpcProviderConnectionState │
│ │ registerAndBind(config) │ base class for clients │ IpcProviderType │
│ │ │ │ IpcProviderConfig (immutable) │
│ ▼ │ │ IpcProviderRegistry / Factory │
│ BaseIpcProvider (abstract) │ │ IpcProviderScanner │
│ │ │ │ AbstractIpcProviderService (server) │
│ │ getProviderType() │ │ IpcServerExecutors │
│ │ requiresConfirmation() │ │ │
│ ▼ │ └────────────────▲────────────────────┘
│ TerminalIpcProvider (client) │ │
│ │ executeShell / readFile / │ │
│ │ writeFile / listDir / … │ │
│ │ │ │ shared AIDL contract
│ │ ┌──────────────────────┐ │ IPC over Binder (Android Service) │ (compiled into both sides)
│ │ │ ITerminalProvider │◄──┼─────────────────────────────────────────────────┘
│ │ │ Service.Stub.asInt.│ │
│ │ └──────────────────────┘ │
└─────────────────────────────────┘
┌─────────────────────────────────────┐
│ Third-party provider app │
│ ───────────────────────────────── │
│ <service android:permission=…> │
│ ITerminalProviderService.Stub { │
│ executeShell, readFile, … │
│ } │
│ Shared thread pool: │
│ IpcServerExecutors.shared() │
└─────────────────────────────────────┘
The split:
:app— owns the runtime. BuildsIpcProviderManager, asks theIpcProviderScannerto discover installed providers, persists user choices inIpcProviderRepository, drives bind / unbind, and reacts toIpcProviderStateListenercallbacks to keep the UI in sync.:ipc— owns the protocol. AIDL definitions, the abstractBaseIpcProvider(client side), the abstractAbstractIpcProviderService(server side), the connection state machine, the type / config / registry / scanner / factory.- Third-party provider app — depends on
:ipc(only the abstractAbstractIpcProviderService+ the AIDL), implements a concreteITerminalProviderService.Stub, exposes it through a<service>tag with the rightandroid:permission. That's it.
| Concept | Lives in | What it is |
|---|---|---|
IpcProviderType |
:ipc |
A logical provider type (currently TERMINAL). Defines an intent action and a permission that the provider must declare on its <service>. New types register a new enum value + their AIDL package. |
IpcProviderConfig |
:ipc |
Immutable value object (id, enabled, providerType, name, packageName, serviceClass, createdAt, updatedAt) — the address of an installed provider. Built via IpcProviderConfig.builder(). |
IpcProviderStateListener |
:ipc |
Observer callback (BaseIpcProvider, IpcProviderConnectionState, Throwable) -> void. |
IpcProviderConnectionState |
:ipc |
Enum: DISCONNECTED, CONNECTING, CONNECTED, FAILED. |
BaseIpcProvider |
:ipc |
Client-side abstract base class. Subclasses declare getProviderType() and expose typed methods (e.g. executeShell, readFile). The base handles bind / unbind, the state machine, and the listener fan-out. |
IpcProviderFactory |
:ipc |
BaseIpcProvider create(IpcProviderConfig config). Lets the manager create subclasses without knowing their concrete types. |
IpcProviderRegistry |
:ipc |
Singleton registry of IpcProviderFactory per IpcProviderType. Defaults to TerminalProviderServiceFactory. OCP-friendly: add a new type, register a factory, the manager works. |
IpcProviderScanner |
:ipc |
Uses PackageManager.queryIntentServices(intent) to find every app that has a <service> with the action for the requested type and has the matching <uses-permission>. Returns List<ScannedProvider>. |
IpcProviderManager |
:ipc |
The runtime façade: registerAndBind, unregisterAndUnbind, `getProvider(id |
AbstractIpcProviderService |
:ipc |
Server-side abstract Service base class. Subclasses implement createBinder() and (optionally) onProviderUnbind / onProviderDestroy. |
IpcServerExecutors |
:ipc |
Process-wide shared thread pool for server-side work. Daemon threads; do not shut it down in onDestroy. |
IpcPermission |
:ipc |
Constant holder. IpcPermission.TERMINAL_PROVIDER = "cn.lineai.permission.IPC_TERMINAL_PROVIDER". |
Every provider implements this. It's a tiny handshake.
// ipc/src/main/aidl/cn/lineai/ipc/IBaseIpcService.aidl
package cn.lineai.ipc;
interface IBaseIpcService {
String getProviderType(); // must equal IpcProviderType.TERMINAL.getId() for terminal providers
String getProviderInfo(); // JSON blob — see below
boolean isAvailable(); // fast liveness probe; false makes the scanner grey out the entry
}getProviderInfo() returns a JSON string. The terminal convention (recognised by the client) is:
{
"name": "Android Shell Terminal Provider",
"version": "1.0",
"shell": "/system/bin/sh",
"home": "/data/user/0/cn.lineai.terminalprovider/files",
"capabilities": ["executeShell", "readFile", "writeFile", "deleteFile",
"listDir", "fileExists", "fileSize"]
}nameandversionare shown in the LineCode UI.shellis the path to the shell binary to invoke forexecuteShell.homeis the absolute path the provider advertises as its "workspace root" (LineCode'sTerminalIpcProvider.getHomePath()reads this back to populate the project picker).capabilitiesis an informational list of AIDL methods the provider implements.
The terminal-specific AIDL, compiled into both :ipc and the provider app. The client side receives these calls as BaseIpcProvider.getService() (cast to ITerminalProviderService.Stub.asInterface(binder)) and exposes typed Java methods (executeShell, readFile, writeFile, listDirDetailed, …).
// ipc/src/main/aidl/cn/lineai/ipc/terminal/ITerminalProviderService.aidl
package cn.lineai.ipc.terminal;
import cn.lineai.ipc.terminal.ITerminalProviderCallback;
interface ITerminalProviderService {
// inherited from IBaseIpcService
String getProviderType();
String getProviderInfo();
boolean isAvailable();
// shell
int executeShell(String command, String cwd, long timeoutMs,
ITerminalProviderCallback callback);
// SFTP-style file ops
byte[] readFile(String path);
boolean writeFile(String path, in byte[] data);
boolean deleteFile(String path);
String[] listDir(String path);
boolean fileExists(String path);
long fileSize(String path);
// chunked for large files
byte[] readFileChunk(String path, long offset, int size);
boolean writeFileChunk(String path, long offset, in byte[] data);
long getFileSize(String path);
// structured listing
String listDirDetailed(String path);
}// ipc/src/main/aidl/cn/lineai/ipc/terminal/ITerminalProviderCallback.aidl
package cn.lineai.ipc.terminal;
interface ITerminalProviderCallback {
void onOutput(String content);
void onError(String error);
void onComplete(int exitCode);
}Implementers do not need to add anything to the protocol to support new shell semantics — just route them through executeShell. If you want a richer interface, define a new AIDL under your own package and a new IpcProviderType enum value, then register a new factory in IpcProviderRegistry.
The trust boundary is enforced at the Android framework layer, not in app code:
-
The provider app declares the permission in its manifest:
<uses-permission android:name="cn.lineai.permission.IPC_TERMINAL_PROVIDER" /> <service android:name=".TerminalProviderService" android:exported="true" android:permission="cn.lineai.permission.IPC_TERMINAL_PROVIDER"> <intent-filter> <action android:name="cn.lineai.action.IPC_TERMINAL_PROVIDER" /> </intent-filter> </service>
-
LineCode declares the permission and asks for it:
<permission android:name="cn.lineai.permission.IPC_TERMINAL_PROVIDER" android:protectionLevel="normal" android:description="@string/permission_ipc_terminal_provider_desc" /> <uses-permission android:name="cn.lineai.permission.IPC_TERMINAL_PROVIDER" /> <queries> <intent> <action android:name="cn.lineai.action.IPC_TERMINAL_PROVIDER" /> </intent> </queries>
-
At
bindServicetime, Android checks the caller (LineCode) holdsIPC_TERMINAL_PROVIDER. If not, the bind is rejected withSecurityException. The provider never has to write a permission check of its own. -
The scanner additionally verifies the package actually requests the permission (
PackageManager.GET_PERMISSIONS), so the picker can grey out providers that wouldn't survive a bind.
Rule of thumb: if the permission string is in IpcPermission.*, use it as android:permission on the service tag. If it's something else, treat the provider as untrusted.
Don't skip the
<queries>block — on Android 11+queryIntentServicesonly returns installed services whose intent the app has explicitly declared an interest in.
We're going to ship a cn.lineai.myprovider app that, on every executeShell call, just logs the command and returns exit code 0. After that, the model can call shell_execute and it'll be routed here.
Add to settings.gradle.kts:
include(":my-provider")my-provider/build.gradle.kts:
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "cn.lineai.myprovider"
compileSdk = 36
defaultConfig {
applicationId = "cn.lineai.myprovider"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "0.1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
aidl = true
}
}
dependencies {
implementation(project(":ipc"))
}my-provider/src/main/AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="cn.lineai.permission.IPC_TERMINAL_PROVIDER" />
<application
android:allowBackup="false"
android:label="My Provider"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MyProviderService"
android:exported="true"
android:permission="cn.lineai.permission.IPC_TERMINAL_PROVIDER">
<intent-filter>
<action android:name="cn.lineai.action.IPC_TERMINAL_PROVIDER" />
</intent-filter>
</service>
</application>
</manifest>Note: the android:permission is what stops random apps from binding. Without it, anything can call you.
The :ipc library already declares the AIDL contracts you need. You have two options:
-
Option A — depend on the AIDL via
:ipc. Already done. The AIDL gets compiled into your APK on its own. -
Option B — vendor the AIDL files if you want a fully self-contained app (e.g. you don't want to depend on
:ipc):my-provider/src/main/aidl/cn/lineai/ipc/IBaseIpcService.aidl my-provider/src/main/aidl/cn/lineai/ipc/terminal/ITerminalProviderService.aidl my-provider/src/main/aidl/cn/lineai/ipc/terminal/ITerminalProviderCallback.aidlSame package, same contents, copied from the
:ipcsource tree. AGP compiles them withbuildFeatures { aidl = true }.
package cn.lineai.myprovider;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import cn.lineai.ipc.IBaseIpcService; // generated from the AIDL
import cn.lineai.ipc.terminal.ITerminalProviderCallback;
import cn.lineai.ipc.terminal.ITerminalProviderService;
import cn.lineai.ipc.service.AbstractIpcProviderService;
import cn.lineai.ipc.service.IpcServerExecutors;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public final class MyProviderService extends AbstractIpcProviderService {
private static final String TAG = "MyProvider";
private static final String SHELL = "/system/bin/sh";
// Use the shared thread pool — never spawn your own.
private final ExecutorService executor = IpcServerExecutors.shared();
@Override
protected IBinder createBinder() {
return new ITerminalProviderService.Stub() {
@Override
public String getProviderType() {
return "terminal";
}
@Override
public String getProviderInfo() {
JSONObject info = new JSONObject();
try {
info.put("name", "My Provider");
info.put("version", "0.1.0");
info.put("shell", SHELL);
info.put("home", getFilesDir().getAbsolutePath());
info.put("capabilities", new JSONArray()
.put("executeShell")
.put("readFile")
.put("listDir")
.put("fileExists")
.put("fileSize"));
} catch (Exception ignored) {
}
return info.toString();
}
@Override
public boolean isAvailable() {
return new File(SHELL).exists();
}
@Override
public int executeShell(String command, String cwd, long timeoutMs,
ITerminalProviderCallback callback) {
Log.i(TAG, "executeShell: " + command);
File workingDir = (cwd != null && !cwd.isEmpty())
? new File(cwd)
: getFilesDir();
if (!workingDir.exists()) workingDir = getFilesDir();
File finalCwd = workingDir;
Future<Integer> future = executor.submit(() -> {
try {
Process p = new ProcessBuilder(SHELL, "-c", command)
.directory(finalCwd)
.redirectErrorStream(true)
.start();
// drain stdout to callback
byte[] buf = new byte[4096];
int n;
while ((n = p.getInputStream().read(buf)) > 0) {
if (callback != null) {
callback.onOutput(new String(buf, 0, n));
}
}
int code = p.waitFor();
if (callback != null) callback.onComplete(code);
return code;
} catch (Exception e) {
if (callback != null) {
try { callback.onError(e.getMessage()); } catch (RemoteException ignored) {}
}
return -1;
}
});
try {
long effective = timeoutMs > 0 ? timeoutMs : 30000L;
return future.get(effective, TimeUnit.MILLISECONDS);
} catch (Exception e) {
future.cancel(true);
if (callback != null) {
try { callback.onError("timeout"); } catch (RemoteException ignored) {}
}
return -2;
}
}
// Stub the rest of the interface — return empty / false.
@Override public byte[] readFile(String path) { return new byte[0]; }
@Override public boolean writeFile(String p, byte[] d){ return false; }
@Override public boolean deleteFile(String path) { return false; }
@Override public String[] listDir(String path) { return new String[0]; }
@Override public boolean fileExists(String path) { return false; }
@Override public long fileSize(String path) { return -1; }
@Override public byte[] readFileChunk(String p, long o, int s) { return new byte[0]; }
@Override public boolean writeFileChunk(String p, long o, byte[] d) { return false; }
@Override public long getFileSize(String path) { return -1; }
@Override public String listDirDetailed(String path) { return "[]"; }
};
}
@Override
protected void onProviderDestroy() {
// Don't shut down IpcServerExecutors — it's process-scoped and shared.
Log.i(TAG, "MyProviderService destroyed");
}
}That's the entire provider. It exposes the AIDL contract. Now build & install:
./gradlew :my-provider:assembleDebug
adb install -r my-provider/build/outputs/apk/debug/my-provider-debug.apk- Open LineCode → Settings → MCP execution mode → Terminal Provider → Add provider.
- The scanner picks up
cn.lineai.myproviderautomatically (it has the right<service>+<uses-permission>). You can also tap Scan to refresh. - Enable the entry. LineCode calls
bindService, the state machine goesDISCONNECTED → CONNECTING → CONNECTED, the listener fires, the project path is set to your provider'shome, and the file tree / attachment picker / model shell tool all start routing through your app.
To verify, watch logcat:
adb logcat -s BaseIpcProvider AbstractIpcProviderService MyProvider
# Look for: "BaseIpcProvider 状态迁移: CONNECTING -> CONNECTED"In the LineCode chat, ask the model to run a command:
"Run
uname -ain the terminal provider and tell me what the kernel version is."
The model will call shell_execute. LineCode will route it through MyProviderService.executeShell(...), which logs the command, returns the kernel version, and the model relays it back.
If you want to write your own client (e.g. an integration test, or a different app that wants to talk to terminal providers), here's the minimal client pattern.
// Build a config from a known package + service.
IpcProviderConfig config = IpcProviderConfig.builder()
.enabled(true)
.providerType(IpcProviderType.TERMINAL.getId())
.name("My Provider")
.packageName("cn.lineai.myprovider")
.serviceClass("cn.lineai.myprovider.MyProviderService")
.build();
// Set up a manager.
IpcProviderManager manager = new IpcProviderManager(context);
manager.addStateListener((provider, state, cause) -> {
Log.i("Client", "state = " + state);
if (state == IpcProviderConnectionState.CONNECTED) {
// ready to call
} else if (state == IpcProviderConnectionState.FAILED) {
Log.e("Client", "bind failed", cause);
}
});
// Bind. The listener fires asynchronously.
BaseIpcProvider base = manager.registerAndBind(config);
if (base instanceof TerminalIpcProvider) {
TerminalIpcProvider term = (TerminalIpcProvider) base;
try {
String home = term.getHomePath();
Log.i("Client", "provider home = " + home);
// term.executeShell(...), term.readFile(...), etc.
} catch (RemoteException e) {
Log.e("Client", "IPC call failed", e);
}
}
// Later, tear down.
manager.unregisterAndUnbind(config.getId());TerminalIpcProvider is the typed client for terminal providers — it lives in :ipc/src/main/java/cn/lineai/ipc/terminal/TerminalIpcProvider.java and is the model your code should follow if you write a new provider type.
BaseIpcProvider.bind(context) walks the state machine:
bind()
DISCONNECTED ──────► CONNECTING ──────► CONNECTED
▲ │ │
│ └── bind returned │
│ false → FAILED │
│ │
│ unbind() / onServiceDisconnected │
└──────────────────────────────────────┘
Every transition is published to:
- Provider-level listeners —
provider.addStateListener(l), useful for toolcards that need to render bind progress. - Global listeners —
manager.addStateListener(l), fed via theproviderStateForwarder.:appregisters exactly one of these on construction; that's how the project path, the file tree, the auto-rebind on cold start, and theDISCONNECTED/FAILEDcleanup hook all work.
The recommended pattern for new provider types:
private final IpcProviderStateListener myListener = (provider, state, cause) -> {
switch (state) {
case CONNECTED:
applyRemoteWorkspace(provider);
break;
case DISCONNECTED:
case FAILED:
if (isRelevant(provider)) {
clearRemoteWorkspace();
}
break;
}
};
// In ctor: manager.addStateListener(myListener);
// In destroy: manager.removeStateListener(myListener);IpcProviderManager.removeStateListener uses List.remove(Object) — make sure you compare the same listener reference. The reference equality pitfall is the most common source of "listener didn't unregister" bugs; store the lambda in a field, as :app's MainCoordinator.ipcStateListener does.
./gradlew :ipc:assembleDebug builds the library. ./gradlew :terminal-provider:assembleDebug builds the reference provider APK. ./gradlew :app:assembleDebug builds LineCode.
Once you have a provider APK of your own:
# 1. Install both APKs.
adb install -r linecode/app/build/outputs/apk/debug/app-debug.apk
adb install -r my-provider/build/outputs/apk/debug/my-provider-debug.apk
# 2. Open LineCode. The scanner should find your provider in
# Settings → MCP execution mode → Terminal Provider → Scan.Provider APKs are normal signed Android APKs. For release builds use a private keystore — the :terminal-provider module ships a validateReleaseSigning task that refuses to sign release artifacts with the debug certificate; replicate this in your own module.
-
<uses-permission android:name="cn.lineai.permission.IPC_TERMINAL_PROVIDER" />in the provider app -
<service android:exported="true" android:permission="cn.lineai.permission.IPC_TERMINAL_PROVIDER"> -
<intent-filter><action android:name="cn.lineai.action.IPC_TERMINAL_PROVIDER" /></intent-filter>on the service - (Caller side)
<permission>+<uses-permission>+<queries><intent><action .../></intent></queries>
- The AIDL interface under
ipc/src/main/aidl/is the stable surface. Adding a new method is backwards compatible (existing providers just don't implement it; the client must tolerateNoSuchMethodErrorat the Stub level, or check for the method's existence). - Removing or renaming a method is a breaking change — bump the module's
versionand call it out in the changelog. getProviderInfo()is JSON — add new fields freely, never repurpose or remove existing ones.IpcProviderTypeenum values are stable per protocol version. To add a new protocol, add a new enum value with a newintentActionandpermissionName, then ship matching AIDL under a new package.
The scanner doesn't find my provider.
Check, in order: (1) the <service> tag has the right intent-filter; (2) the android:exported is true; (3) your <uses-permission> matches the action's permission; (4) you have the <queries> block in the caller's manifest; (5) the install actually succeeded (adb shell pm list packages | grep myprovider).
Bind throws SecurityException.
The caller (LineCode) doesn't hold the IPC_TERMINAL_PROVIDER permission — most likely the permission is protectionLevel="signature" instead of "normal", or it's missing from the caller's manifest.
State is FAILED, cause = IllegalStateException("bindService 返回 false").
The intent didn't resolve. Check that packageName and serviceClass in the config match the installed provider exactly, and that the <service> is exported.
Provider runs but model can't find the home path.
Make sure getProviderInfo() includes "home": getFilesDir().getAbsolutePath(). The client reads it back via TerminalIpcProvider.getHomePath().
Threads piling up / app stuck on shutdown.
Don't newCachedThreadPool inside the service. Use IpcServerExecutors.shared() — it returns a daemon pool, so a stuck shell call won't block the app from exiting.
Tests.
The unit tests under app/src/test/java/cn/lineai/... cover the client-side state machine and the registry. Server-side AIDL methods are best tested by running both the provider and the client on a connected device with adb logcat -s ....
// Build a config
IpcProviderConfig config = IpcProviderConfig.builder()
.id("ipc_myprovider_1") // optional; auto-generated UUID if blank
.enabled(true)
.providerType(IpcProviderType.TERMINAL.getId())
.name("My Provider")
.packageName("cn.lineai.myprovider")
.serviceClass("cn.lineai.myprovider.MyProviderService")
.build();
// Manager lifecycle
IpcProviderManager manager = new IpcProviderManager(context);
manager.addStateListener(listener);
BaseIpcProvider provider = manager.registerAndBind(config);
manager.unregisterAndUnbind(config.getId());
manager.unregisterAll();
manager.removeStateListener(listener);
// Discovery
IpcProviderScanner scanner = new IpcProviderScanner();
List<ScannedProvider> hits = scanner.scan(context, IpcProviderType.TERMINAL);
// Registry (e.g. for adding a new type)
IpcProviderRegistry.getInstance().register(new MyProviderFactory());
// State polling
IpcProviderConnectionState state = provider.getConnectionState();
boolean bound = provider.isBound();
IpcProviderConfig cfg = provider.getConfig();// Subclass AbstractIpcProviderService
public final class MyProviderService extends AbstractIpcProviderService {
@Override
protected IBinder createBinder() {
return new ITerminalProviderService.Stub() {
// override AIDL methods
};
}
// optional hooks
@Override protected void onProviderUnbind(Intent intent) { /* ... */ }
@Override protected void onProviderDestroy() { /* ... */ }
}
// Shared thread pool
ExecutorService pool = IpcServerExecutors.shared(); // daemon threads; do NOT shutdown<uses-permission android:name="cn.lineai.permission.IPC_TERMINAL_PROVIDER" />
<service
android:name=".MyProviderService"
android:exported="true"
android:permission="cn.lineai.permission.IPC_TERMINAL_PROVIDER">
<intent-filter>
<action android:name="cn.lineai.action.IPC_TERMINAL_PROVIDER" />
</intent-filter>
</service><permission
android:name="cn.lineai.permission.IPC_TERMINAL_PROVIDER"
android:protectionLevel="normal" />
<uses-permission android:name="cn.lineai.permission.IPC_TERMINAL_PROVIDER" />
<queries>
<intent>
<action android:name="cn.lineai.action.IPC_TERMINAL_PROVIDER" />
</intent>
</queries>That's the whole surface. Three AIDL methods, one abstract class on the client, one abstract class on the server, one permission, one intent action, one shared thread pool.