Talking across the wall: every way Android apps communicate
Why every app lives behind its own wall, how Binder carries every call across it, and the doors the platform cuts into it: intents, broadcasts, content providers, bound services and AIDL, shared memory, sockets and shared files.
On this page
Every Android app is a separate Linux user in a separate process, and the platform is built so that two apps cannot see each other's memory or files by accident. Yet a phone is one long conversation between processes: the share sheet, a payment hand-off, a password manager filling a form, a music app driving a car display. All of it goes through a small set of doors in the wall, and every door is built on the same hinge, Binder. This article walks through the wall, the hinge, and each door in turn, with the rules that decide which one to use.
The map
Read this first when short on time. Every branch is a section below.
The wall: one Linux user per app
Android's sandbox is the ordinary Linux permission model applied with unusual discipline: every installed package gets its own user id, its own process and its own private directory, so the kernel, not the framework, is what stops one app from reading another.
At install time PackageManager assigns the app a uid from 10000 upwards (shown as u0_a123 in ps). When the app starts, Zygote forks a fresh process and switches it to that uid before any app code runs, as the app startup article describes. The app's data directory is owned by that uid with mode 0700, and SELinux puts the process in the untrusted_app domain, which spells out the few kernel objects it may touch. Two apps therefore share nothing: not memory, not files, not even the right to signal each other.
flowchart TD A["App A, uid 10087<br/>own process and ART heap<br/>/data/data/a, mode 0700"] B["App B, uid 10142<br/>own process and ART heap<br/>/data/data/b, mode 0700"] K["Kernel: uid checks, SELinux, Binder driver"] S["system_server: the broker that decides who may talk"] A --> K B --> K K --> S
system_server before the two apps hold a direct line.The wall is what makes IPC necessary, and it is also what makes it safe: because the kernel tags every Binder transaction with the sender's real uid, a receiving process always knows exactly who is on the other side. That single fact underlies every permission check in the rest of this article.
One app can also be split across several processes with android:process on a component, or run helpers in an isolatedProcess that has no permissions and a uid of its own. Those processes talk to their parent through the very same doors as two unrelated apps, which is why the mechanisms below matter even for an app that never talks to anyone else.
- Chrome runs web content in
isolatedProcessrenderers with no permissions at all; the browser process feeds them over Binder and shared memory. - Google Play services is a single app whose uid is trusted by nothing special; every SDK that "uses Play services" is binding to services inside that one process.
- WebView since Android 8 renders in a separate sandboxed process too, so a renderer crash does not take the host app down.
- uid
- The Linux user id. On Android, one per app, or one per group of apps that share a signing key and a
sharedUserId. - SELinux domain
- A label on a process that a policy uses to allow or deny each kernel operation.
untrusted_appis the label for ordinary apps. - isolatedProcess
- A service attribute that runs the service in a process with a throwaway uid and no permissions, reachable only by its own app through
bindService.
Binder: the hinge every door swings on
Binder is a kernel driver plus a thin user-space library that lets one process call a method on an object in another process as if it were local, with the caller's identity attached and the bytes copied exactly once.
Linux already had pipes, sockets and shared memory when Android was designed. Binder was chosen because it gives three things those do not give together: a notion of objects with reference counting across processes, a built-in answer to "who is calling me", and a thread pool on the receiving side so that a call feels like a function call rather than a message to be parsed. Everything in the rest of this article, including intents and content providers, is a Binder transaction underneath.
The pieces
/dev/binderis the driver. Each process opens it once and maps a buffer of about 1 MB into which the driver writes incoming transactions.- A
Parcelis the wire format: a flat byte array of primitives and strings plus a table of "objects" (Binder references and file descriptors) that the driver translates as the Parcel crosses. - An
IBinderis a reference to a remote object. Locally it is aBindersubclass that implementsonTransact; remotely it is aBinderProxyholding a handle the driver understands. - The Stub is the server-side class that unpacks a Parcel and calls the real method; the Proxy is the client-side class that packs one. AIDL generates both, and the framework's own services use hand-written or generated pairs of exactly this shape.
- The ServiceManager is the one object everyone can find: it owns handle 0 and maps names like
"activity"or"window"to the Binder objects of system services. App services are never registered there; apps receive anIBinderfromActivityManagerthroughbindServiceinstead.
sequenceDiagram autonumber participant C as Client thread participant D as Binder driver participant T as Binder thread participant S as Stub (real object) C->>C: Proxy writes args into a Parcel C->>D: ioctl BINDER_WRITE_READ, transaction D->>D: copy the Parcel into the receiver's mapped buffer D->>T: wake a thread from the pool, deliver T->>S: onTransact(code, data, reply) S-->>T: real method runs, reply Parcel written T-->>D: BC_REPLY D-->>C: reply copied back, client thread wakes
What a transaction guarantees
- One copy. The driver copies the Parcel from the sender's memory straight into the buffer it mapped in the receiver. There is no intermediate kernel buffer that gets copied again on read.
- A thread appears on the other side. The receiver never reads a socket. The driver wakes one of the process's Binder threads, named
binder:pid_Nin a thread dump; an app process allows up to 16 of them. If none is idle and the pool is full, the call waits. - Identity travels with the call. The driver records the sender's uid and pid in the transaction. On the receiving thread
Binder.getCallingUid()reads it for exactly as long as the transaction is in progress. The sender cannot fake it. - Objects keep their identity. If a Parcel carries an
IBinder, the driver translates it: a local object becomes a handle in the receiver, and a handle that points back at the receiver's own object turns back into that very object. This is how the sameIBindercan be passed around three processes and still be recognised as one object. File descriptors are translated too, by duplicating them into the receiver. - Synchronous by default. The caller's thread blocks until the reply. There is no timeout. The only way a synchronous call ends without a reply is a
DeadObjectExceptionwhen the receiver dies. - Nested calls come back to the waiting thread. If the server, while handling a call, calls back into the client, the driver delivers that callback to the client thread that is already waiting rather than waking a new one. It is what makes re-entrant patterns work without deadlock, and also what lets a server run code on your thread when you least expect it.
oneway changes the shape: the call returns as soon as the driver has queued it, the reply Parcel does not exist, and calls to the same destination object are delivered in order but never concurrently with each other. The framework uses oneway for the direction where the caller must never wait, for example everything ActivityManager says to an app's ApplicationThread.
- The 1 MB buffer is per process, not per call. Every transaction that is in flight into or out of a process shares the same buffer, and oneway transactions may use only half of it. A Parcel of a few hundred kilobytes that works in a test fails on a busy device with
TransactionTooLargeException. Intents, saved instance state and AIDL arguments all count. - A synchronous call blocks the main thread. Binder does not know or care which thread calls it. A call into a slow service from the UI thread is a frozen frame, and five seconds of it is an ANR.
- Stub code runs on a Binder thread. Whatever implements
onTransact, or an AIDL method, is being called from the pool, possibly by several threads at once. Touch UI state from there and you will crash; touch shared state without synchronisation and you will corrupt it.
Death is a first-class event. IBinder.linkToDeath registers a callback that the driver fires when the process behind a handle disappears; ActivityManager uses it to notice app deaths, and a client of a bound service uses it, through ServiceConnection.onServiceDisconnected, to know when to rebind.
- Every system service call is a Binder transaction into
system_server:startActivity,getSystemService(...).something(), drawing a frame throughWindowManager. An app that "does nothing" still makes dozens per second while animating. - Google Play services exposes its APIs as AIDL interfaces; the client SDK in your app is a bundle of generated proxies that bind to the
com.google.android.gmsprocess. - Chrome Custom Tabs is an AIDL service inside Chrome; the
CustomTabsServicea host app binds to is how "warm up" and "may launch URL" hints are delivered. - HALs since Android 8 use their own driver instances,
/dev/hwbinderand/dev/vndbinder, so vendor traffic never competes with the framework's buffer.
- Parcel
- The serialisation format of a Binder transaction: primitives, strings and a table of special objects (binders, file descriptors) that the driver rewrites in transit.
- Stub and Proxy
- The two halves of a remote interface. The stub lives in the server, receives Parcels and calls real code; the proxy lives in the client and turns method calls into Parcels.
- oneway
- A transaction with no reply. The caller returns immediately; delivery is ordered per destination object and never concurrent for the same object.
- ServiceManager
- The name service of Binder. System services register under a string name; apps look them up indirectly through
Context.getSystemService.
Intents: a message the system delivers for you
An Intent is a description of something to be done, handed to system_server, which finds the component that should do it, starts its process if needed and delivers the message. The sender never holds a reference to the receiver; the broker does everything.
An intent is explicit when it names a ComponentName (package plus class) and implicit when it only describes what it wants with an action, a data URI and type, and categories. PackageManager matches an implicit intent against the <intent-filter> elements every app declared at install time; one match goes straight there, several produce a chooser. Explicit intents skip the matching and go where they are told.
flowchart TD
I([startActivity]) --> Q{Component set?}
Q -- Yes --> E["Explicit: deliver to that class"]
Q -- No --> M["Match action, data, categories<br/>against installed intent filters"]
M --> N{Matches?}
N -- None --> X["ActivityNotFoundException"]
N -- One --> E
N -- Several --> C["Chooser dialog, or the user's default"]
C --> E
E --> P{Process alive?}
P -- No --> Z["Zygote fork, bindApplication"]
P -- Yes --> L["Deliver: onCreate or onNewIntent"]
Z --> L
system_server. Only the leftmost path, an explicit intent to a running process, is short; every other path involves PackageManager and possibly a fork.What an intent can carry
The payload is a Bundle of extras, which is a Parcel with a string key per entry. It is parcelled at send time on the sender's side, travels through ActivityManager, and is unparcelled lazily in the receiver. Three consequences follow:
- The entire transaction, extras plus the intent itself plus any
ActivityOptions, must fit the Binder budget. A bitmap in an extra is the classic way to hitTransactionTooLargeException; send a URI or a file descriptor instead. - A custom
Parcelableonly unparcels if the receiver has the same class. Across apps that means a shared library, or sticking to framework types. - Extras are copied, not shared. Mutating an object after
putExtrachanges nothing on the other side.
Results, links and tokens
Three variations on the theme cover most cross-app flows:
- Getting a result.
registerForActivityResultwith theStartActivityForResultcontract starts another app's activity and gets anIntentback throughsetResult. The system, not the callee, routes the result, so the callee never learns who asked unless it callscallingPackage. - Deep links and App Links. An
httporhttpsintent filter withandroid:autoVerify="true"becomes an App Link once the domain publishes a Digital Asset Links file naming the app's signing certificate. Since Android 12 an unverified web link opens in the browser by default instead of offering the app. - PendingIntent. A token created by
ActivityManagerthat wraps an intent together with the creator's identity. Whoever holds it cansend()it later, and the system performs the action as the creator. Notifications, widgets, alarms and geofences all work this way: SystemUI orAlarmManagerfires the token, your app receives the intent.
// Explicit: I know exactly who should handle this
val open = Intent(Intent.ACTION_VIEW, uri).setPackage("com.android.chrome")
// Implicit: anyone who can share text, let the user pick
val share = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "https://example.org/post/42")
}
startActivity(Intent.createChooser(share, null))
// A token another process can fire later, acting as this app
val pi = PendingIntent.getActivity(
context, 0, Intent(context, DetailActivity::class.java).putExtra("id", 42),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
The rules that changed under everyone
| Release | Rule | What breaks without it |
|---|---|---|
| Android 5 | Service intents must be explicit. | bindService with an implicit intent throws. |
| Android 7 | file:// URIs may not leave the app. | FileUriExposedException; use a FileProvider content URI. |
| Android 11 | Package visibility: declare a <queries> element for the packages or intents you look up. | resolveActivity and queryIntentActivities return nothing; startActivity of an implicit intent still works because the system resolves it. |
| Android 12 | Components with an intent filter must declare android:exported; PendingIntents must say FLAG_IMMUTABLE or FLAG_MUTABLE. | Install fails; PendingIntent creation throws. |
| Android 14 | Implicit intents reach only exported components; a mutable PendingIntent must name a package or component. | An implicit intent to your own non-exported activity no longer arrives; make it explicit. |
- The share sheet is
ACTION_SENDwith a MIME type; every app that shows up there declared a matching filter. Direct Share targets on top of it come fromShortcutManager. - UPI payment apps such as Google Pay and PhonePe are reached with a
upi://pay?...intent; the merchant app gets the transaction status back as an activity result. - Google Maps answers
geo:URIs andgoogle.navigation:intents, which is how a food delivery app hands off a route without embedding a map. - The camera via
MediaStore.ACTION_IMAGE_CAPTUREis an implicit intent that carries a content URI the camera app writes into, a small IPC round trip that avoids the camera permission entirely.
An intent's extras are attacker-controlled input whenever the component is exported. Read them defensively, never forward a nested Intent from an extra straight into startActivity (that is intent redirection, and it lets a caller reach your non-exported components with your identity), and never build a PendingIntent around an implicit intent. The security section returns to both.
- Intent filter
- A manifest declaration of the actions, data schemes or types, and categories a component accepts. It is what makes a component a target for implicit intents.
- PendingIntent
- A system-held token that lets another process perform an intent with the creator's identity and permissions at a later time.
- Package visibility
- The Android 11 rule that an app sees only the packages it declares in
<queries>, plus those that interact with it, unless it holdsQUERY_ALL_PACKAGES.
Broadcasts: one message, many listeners
A broadcast is an intent sent to every receiver that matches it rather than to one component, delivered by ActivityManager's broadcast queue with no reply. It is how the system announces events and how apps publish them to whoever cares.
There are two kinds of receiver. A manifest-declared receiver exists even when the app is not running: the system starts the process to deliver to it. A context-registered receiver, created with registerReceiver, lives only while the registering component does. Delivery is normal (all receivers, unordered, in parallel) or ordered (one at a time by android:priority, each able to modify the result or abort the chain). Sticky broadcasts, which stayed around for late listeners, are deprecated; ACTION_BATTERY_CHANGED is the last well-known survivor, and registerReceiver(null, filter) still returns its latest value.
sequenceDiagram autonumber participant A as Sender app participant Q as BroadcastQueue participant R1 as Receiver (running) participant R2 as Receiver (manifest) A->>Q: sendBroadcast(intent) or sendOrderedBroadcast Q->>Q: match filters, apply permissions and background rules Q-)R1: onReceive on the main thread Q->>R2: start the process, then onReceive R2-->>Q: finished (or goAsync, then finish) Note over Q,R2: 10 s for a foreground broadcast, 60 s in the background, then ANR
system_server. A manifest receiver costs a process start; a context-registered one is delivered to a process that already runs.The rules that tamed broadcasts
Broadcasts used to wake half the phone whenever the network changed, so the platform closed the door in stages:
- Android 8: manifest receivers no longer receive implicit broadcasts, with a short exempt list (
BOOT_COMPLETED,LOCKED_BOOT_COMPLETED,LOCALE_CHANGED,TIMEZONE_CHANGED,MY_PACKAGE_REPLACED,SMS_RECEIVEDand a handful more). An explicit broadcast, one withsetPackageorsetComponent, is still delivered, and so is anything to a context-registered receiver. - Android 3.1 onwards: an app in the stopped state that a force stop leaves behind receives nothing until the user opens it.
- Android 12 onwards: the broadcast queue defers deliveries to cached processes until they leave the cached state, so a burst of broadcasts no longer wakes every idle app at once.
- Android 14:
registerReceivermust passRECEIVER_EXPORTEDorRECEIVER_NOT_EXPORTED, except for system-only broadcasts.
Permissions work in both directions. The sender can pass a permission name, and only receivers holding it get the message; the receiver can declare android:permission on its manifest entry or pass it to registerReceiver, and only senders holding it get through. System broadcasts are additionally protected: an app that tries to send ACTION_BOOT_COMPLETED gets a SecurityException.
// Publish to one specific app: explicit, still delivered to its manifest receiver
sendBroadcast(Intent("org.example.SYNC_DONE").setPackage("org.example.companion"))
// Listen while this Activity is alive, and only to senders inside my own app
registerReceiver(receiver, IntentFilter("org.example.SYNC_DONE"),
Context.RECEIVER_NOT_EXPORTED)
class SyncDoneReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Main thread. Do not block; hand real work to WorkManager.
val pending = goAsync() // buys time, but not past the ANR limit
ioScope.launch {
markSynced(intent.getStringExtra("id"))
pending.finish()
}
}
}
onReceiveruns on the main thread of whichever process hosts the receiver, and the process may exist only for that call. Start nothing asynchronous from it that expects the process to survive; enqueue aWorkManagerjob or start a foreground service instead.- A normal broadcast has no reply and no confirmation. If a caller needs an answer, this is the wrong door.
LocalBroadcastManageris deprecated and was never IPC; inside one process aSharedFloworStateFlowfrom the coroutines article does the same job with types.
ACTION_BOOT_COMPLETEDis the broadcast that lets alarm clocks, VPNs and messaging apps re-arm themselves after a reboot; it is on the exempt list precisely because nothing else could do that job.- Firebase Cloud Messaging arrives as an explicit broadcast from the Play services process to a receiver in your app, which is why push works even when the app was not running.
- Tasker and other automation apps use a plugin protocol built on broadcasts: the host fires an intent with a settings bundle, the plugin's receiver acts on it.
- The default SMS app receives
SMS_DELIVER; everyone else may listen toSMS_RECEIVEDbut cannot consume it, the platform's answer to a decade of ordered-broadcast fights.
- Ordered broadcast
- Delivered to one receiver at a time in priority order; each may change the result data or call
abortBroadcast()to stop the chain. - Protected broadcast
- A system action declared with
<protected-broadcast>that only the system may send. - goAsync
- Tells the system the receiver is still working after
onReceivereturns, keeping the process alive untilfinish()or the broadcast timeout.
Content providers: a table with a URL
A content provider exposes structured data or files behind a content:// URI with a fixed verb set, so that any app with permission can query it through ContentResolver without knowing which process, database or file layout stands behind it.
The URI is content://authority/path/id. The authority names the provider, the path picks a table or a file, and the verbs are query, insert, update, delete, openFile and a free-form call(method, arg, extras) for anything that does not fit a table. query returns a Cursor; openFile returns a ParcelFileDescriptor. Clients can also watch a URI with a ContentObserver and the provider pokes them with notifyChange.
Under the hood
sequenceDiagram autonumber participant A as Client app participant AM as ActivityManager participant P as Provider process A->>AM: get the provider for this authority AM->>P: start the process if needed, publish the provider AM-->>A: IContentProvider binder, cached in the client A->>P: query(uri, projection, ...) directly over Binder P-->>A: Cursor backed by a shared-memory CursorWindow A->>P: openFile(uri) directly over Binder P-->>A: ParcelFileDescriptor, dup'd into the client
ActivityManager is involved only in the introduction. Once the client holds the provider's binder, every call goes process to process.- The first call for an authority asks
ActivityManagerfor it. If the provider's process is not running, the system starts it, and the provider'sonCreateruns before the app's ownApplication.onCreate, which the startup article covers. - The client receives an
IContentProviderbinder and caches it. Later calls skipsystem_serverentirely. - A query result does not travel in the Parcel. The provider fills a
CursorWindow, a shared-memory block of 2 MB by default, and sends its descriptor. The client'sCursorreads rows out of it; when it moves past the window, the next window is fetched. Big result sets cross the wall without ever meeting the 1 MB limit; a single row bigger than the window fails withSQLiteBlobTooBigException. - Files cross as descriptors. The provider opens the file with its own permissions and the driver duplicates the descriptor into the client, which now reads a file it could never have opened by path.
There is one sharp edge. ContentResolver makes the first call through an unstable reference so that a crashing provider surfaces as an exception, but the Cursor it hands back holds a stable reference to the provider for as long as it is open. A stable reference means "my correctness depends on that process", and if the provider dies while the cursor is open, ActivityManager kills the client too. It is the reason a contacts-provider crash can take an unrelated app down, and it is why cursors should be read and closed promptly.
Sharing files the right way
A FileProvider is a stock ContentProvider configured in XML that maps directories in your private storage to content URIs. To hand a file to another app you attach the URI to an intent with FLAG_GRANT_READ_URI_PERMISSION; the system records a temporary grant for the receiving component, valid until its task finishes. Context.grantUriPermission makes an explicit, longer-lived grant; the Storage Access Framework's FLAG_GRANT_PERSISTABLE_URI_PERMISSION plus takePersistableUriPermission makes one that survives reboot.
// Manifest: a FileProvider over app-private files, never exported, grants per intent
// <provider android:name="androidx.core.content.FileProvider"
// android:authorities="${applicationId}.files"
// android:exported="false" android:grantUriPermissions="true">
// <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/paths"/>
// </provider>
val uri = FileProvider.getUriForFile(context, "${context.packageName}.files", pdf)
val view = Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, "application/pdf")
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(view)
// Reading someone else's provider
context.contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
arrayOf(ContactsContract.Contacts.DISPLAY_NAME), null, null, null
)?.use { c -> while (c.moveToNext()) names += c.getString(0) }
Access is controlled by android:exported (false by default for apps targeting API 17 and later), by readPermission and writePermission, and by <path-permission> elements for finer cuts. URI grants punch precise, temporary holes through all of those.
- Contacts, calendar and call log are providers inside system apps; every dialer, messenger and calendar client reads the same tables through
ContactsContractand friends. - MediaStore is the provider that scoped storage funnels media through; a gallery app queries it instead of walking the filesystem.
- Settings is a provider too:
Settings.Secure.getStringis acall()into the settings provider. - Google Drive, Dropbox and OneDrive implement
DocumentsProvider, which is what makes them appear in the system file picker for any app that uses the Storage Access Framework. - Messaging apps share photos and documents to other apps through a
FileProvider; the receiving app never sees a path, only a URI it may read for a while.
- Authority
- The host part of a
content://URI, declared in the manifest, that identifies one provider on the device. - CursorWindow
- A shared-memory buffer that carries query rows between processes; 2 MB by default, paged as the cursor moves.
- URI grant
- A temporary, per-URI permission the system records when an intent carries
FLAG_GRANT_*_URI_PERMISSIONorgrantUriPermissionis called. - Stable reference
- A provider connection whose holder is killed if the provider dies; an open
Cursorholds one.
Bound services: calling a method in another process
A bound service hands a client an IBinder through onBind, and from then on the client calls methods on it as a remote object. It is the door to use when two processes need a conversation rather than a message: request and reply, callbacks, a session that lasts.
The client calls bindService(intent, connection, flags). ActivityManager starts the service's process if needed, calls onBind there, and delivers the resulting binder to the client's ServiceConnection.onServiceConnected on the client's main thread. The service stays alive while anyone is bound to it and is destroyed after the last unbindService, unless it was also started. Binding also lends the client's importance to the service's process: while a foreground app is bound, the service process is treated almost as a foreground process by the memory killer, which is why an SDK's service is rarely killed while its host is on screen.
sequenceDiagram autonumber participant C as Client participant AM as ActivityManager participant S as Service process C->>AM: bindService(explicit intent, connection, BIND_AUTO_CREATE) AM->>S: start process if needed, onCreate, onBind(intent) S-->>AM: IBinder AM-)C: onServiceConnected(name, binder), main thread C->>S: Stub.asInterface(binder).method(args) directly S-->>C: reply, or DeadObjectException if the service died C->>AM: unbindService(connection) AM->>S: onUnbind, onDestroy when nobody is bound
ActivityManager introduces the two sides and then steps out of the way.Three ways to build the binder
Local Binder subclass | Messenger | AIDL | |
|---|---|---|---|
| Crosses processes | No. The client casts the binder and calls the service object directly. | Yes | Yes |
| Shape | Ordinary Kotlin methods | Message objects with a what code and a Bundle | Typed interface methods with arguments and return values |
| Threading in the service | Caller's thread | One Handler, so one message at a time, in order | Binder thread pool, concurrent |
| Replies | Return values | Send a Message back via replyTo | Return values, out parameters, or a callback interface |
| Use when | Same process: a music player's UI binding to its own playback service | Simple commands, no need for concurrency, no build-time interface | Anything between apps, or anything with a real API surface |
Messenger is worth knowing because it is AIDL in disguise: it wraps a Handler behind the framework's own IMessenger interface, and its serial delivery is often exactly what a small service wants. Its Message.obj field only crosses processes if it holds a framework Parcelable; anything custom goes into the data bundle with the class loader set on the receiving side.
AIDL in practice
An .aidl file declares the interface. The build generates a Java IFoo with a nested Stub (a Binder you subclass in the service) and a Proxy (what the client gets from Stub.asInterface). Kotlin uses it directly.
// IRemoteCounter.aidl
package org.example.counter;
import org.example.counter.Snapshot; // declared elsewhere as: parcelable Snapshot;
import org.example.counter.ICounterListener;
interface IRemoteCounter {
int increment(int by); // synchronous, returns a value
Snapshot snapshot(); // a Parcelable crossing the wall
void fill(inout List<String> names); // inout: sent, modified, sent back
oneway void log(String line); // fire and forget, ordered
void register(ICounterListener l); // a binder going the other way
}
// Service side: methods run on Binder threads, so guard the state
class CounterService : Service() {
private val count = AtomicInteger()
private val listeners = RemoteCallbackList<ICounterListener>()
private val binder = object : IRemoteCounter.Stub() {
override fun increment(by: Int): Int {
enforceCallingPermission("org.example.counter.USE", "counter")
return count.addAndGet(by)
}
override fun snapshot() = Snapshot(count.get(), System.currentTimeMillis())
override fun fill(names: MutableList<String>) { names += "counter" }
override fun log(line: String) = Log.d("Counter", "${Binder.getCallingUid()}: $line")
override fun register(l: ICounterListener) { listeners.register(l) } // linkToDeath inside
}
override fun onBind(intent: Intent): IBinder = binder // called by the system, not the client
}
// Client side
val conn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
counter = IRemoteCounter.Stub.asInterface(service) // Proxy, or the real object in-process
}
override fun onServiceDisconnected(name: ComponentName) { counter = null } // it died; rebind
}
bindService(Intent().setClassName("org.example.counter", "org.example.counter.CounterService"),
conn, Context.BIND_AUTO_CREATE)
Rules worth memorising, because each one is a bug in the field:
- Direction markers. Primitives and
Stringare alwaysin. Anything else declaresin,outorinout, and every direction is a copy;inoutcopies twice. - Transaction codes come from method order. Each method gets
FIRST_CALL_TRANSACTION + index. Adding a method at the end keeps old clients working; reordering or removing breaks them silently, with the wrong method executing. asInterfaceshort-circuits. If the binder is a local object in the same process it returns that object, so in-process "IPC" costs nothing and runs on the caller's thread.- Only a short list of exceptions crosses.
SecurityException,IllegalArgumentException,IllegalStateException,NullPointerException,UnsupportedOperationExceptionandServiceSpecificExceptionare re-raised in the client. Any other exception thrown in a stub method crashes the service process, and the client sees aDeadObjectException. - Callbacks are binders too. Registering a listener sends the client's own
Binderto the service;RemoteCallbackListkeeps them, links to death, and drops the ones whose process is gone. - The intent must be explicit (since Android 5) and, since Android 11, the target package must be visible through
<queries>.
onBindis called byActivityManager, soBinder.getCallingUid()inside it returns the system's uid, not the client's. Check identity inside each interface method, where the calling uid is the real client.- Started services are a different door with different rules: since Android 8 a background app cannot
startService, and a foreground service must show a notification and, since Android 14, declare a type. Binding has none of these restrictions because the client's own state governs the service. - A synchronous AIDL call from the main thread into a slow service is your ANR, not theirs. Wrap remote calls in
withContext(Dispatchers.IO)or use oneway methods with a callback.
- Google Play Billing binds to a service in the Play Store app; the billing library wraps that AIDL and verifies the package it bound to is the real
com.android.vending. - Chrome Custom Tabs and Trusted Web Activities start with
bindServicetoCustomTabsService, so warm-up and pre-render hints arrive before the tab opens. - Media apps and Android Auto meet through
MediaBrowserService: the car UI binds to the app's service and browses its content tree over AIDL callbacks. - Password managers implement
AutofillService; here the roles reverse, the system binds to the app's service and the app answers fill requests for other apps' screens. - Keyboards (
InputMethodService) and screen readers (AccessibilityService) are the same pattern: a system-bound service that every other app talks to only through the system.
- AIDL
- Android Interface Definition Language. A small declaration language from which the build generates the stub and proxy classes for a Binder interface.
- ServiceConnection
- The client-side callbacks for a binding:
onServiceConnectedwith the binder,onServiceDisconnectedwhen the service's process dies. - RemoteCallbackList
- A framework helper that stores remote listener binders, registers death recipients for them and lets the service broadcast to the live ones.
Shared memory and file descriptors: moving big data
When the payload is a bitmap, a video frame or a database page, the data does not go through Binder at all: the sender puts it in memory or a file both sides can map, and Binder carries only the file descriptor that names it.
The trick rests on one driver feature from the Binder section: a file descriptor in a Parcel is duplicated into the receiving process. The receiver ends up with its own descriptor for the same open file, pipe, socket or memory region, exactly as if it had been forked with it. Everything else is a choice of what the descriptor refers to.
flowchart LR
subgraph P["Producer process"]
W["write pixels into<br/>the mapped region"]
end
subgraph K["Kernel"]
M["one memfd or ashmem region<br/>(physical pages)"]
end
subgraph C["Consumer process"]
R["map the dup'd fd,<br/>read the same pages"]
end
W --> M --> R
P -. "Binder carries only the fd" .-> C
| Object | What it is | Typical use |
|---|---|---|
ParcelFileDescriptor | A Parcelable wrapper around any fd | Returning a file from ContentProvider.openFile; passing a socket or pipe end in an AIDL call |
ParcelFileDescriptor.createPipe() | A pipe, two descriptors | Streaming generated data to another app without a temp file |
SharedMemory (API 27) | An anonymous memory region, ashmem on older kernels and memfd on newer ones, with a ByteBuffer view | Large buffers between your app's processes, or into an isolated renderer |
CursorWindow | A shared-memory page of query rows | Every Cursor that crosses a process, silently |
Bitmap (immutable) | Parcelled as an ashmem descriptor rather than inline bytes | Notifications and widgets ship images to SystemUI this way |
HardwareBuffer | A GPU-visible buffer backed by dma-buf | Camera frames, video decoder output, every window's frames |
Surface | The producer end of a BufferQueue | Handing SurfaceFlinger, Camera2 or MediaCodec a place to put frames |
// Producer: allocate, fill, then make read-only before handing it out
val region = SharedMemory.create("frame", 4 * 1024 * 1024)
region.mapReadWrite().use { buf -> buf.put(pixels) } // ByteBuffer over the pages
region.setProtect(OsConstants.PROT_READ) // consumers cannot write back
remote.publish(region) // SharedMemory is Parcelable: the fd crosses
// Consumer: map the same pages, no copy involved
override fun publish(region: SharedMemory) {
val view = region.mapReadOnly()
decode(view)
SharedMemory.unmap(view)
}
setProtect is the detail that makes shared memory safe to hand to an untrusted party: once the region is sealed read-only the consumer cannot modify pages the producer is still using, and the kernel enforces it. Sizing is the other detail: memory in a shared region is charged to whoever maps it, so a producer that shares a region with ten consumers has made ten processes look bigger to the memory killer.
- SurfaceFlinger never receives pixels over Binder. Every frame an app draws lands in a
HardwareBufferthat the compositor maps; Binder carries the buffer handle and a fence. - Camera2 hands the camera HAL a
Surfacefrom your preview; frames are written by the ISP into buffers your view already has mapped. - MediaCodec exposes decoder output as buffers or a
Surfacefor the same reason; a 4K frame through a Parcel would be absurd. - Chrome passes compressed resources and IPC message payloads to its renderers as shared memory regions.
- ashmem and memfd
- Two kernel mechanisms for anonymous shared memory referenced by a file descriptor. ashmem is Android's original driver;
memfd_createis the upstream Linux replacement that newer Android releases use behind the same APIs. - dma-buf
- The Linux framework for sharing device memory between drivers and processes by descriptor; the backing of
HardwareBuffer. - BufferQueue
- A producer-consumer queue of graphics buffers; a
Surfaceis its producer handle.
Sockets: the door the platform itself uses
Ordinary Linux sockets work on Android, and the platform uses Unix domain sockets wherever it wants a byte stream with kernel-verified peer identity rather than a method call.
LocalSocket and LocalServerSocket wrap Unix domain sockets. The name lives in one of three namespaces: abstract (a name in the kernel, no file, the default), filesystem (a path, protected by directory permissions) or reserved (under /dev/socket, created by init for daemons). getPeerCredentials() returns the uid, gid and pid of the other end, so a server can apply the same "who is calling" logic as a Binder service. Descriptors can be sent over them too, with SCM_RIGHTS, the same trick Binder performs for a Parcel. The Linux sockets and IPC article covers the API itself.
Plain TCP on the loopback interface also works. Any app with INTERNET permission can connect to 127.0.0.1:port, and the kernel does not tell the server who connected, so a local HTTP server that trusts its callers is trusting every app on the phone. Authenticate with a token exchanged through a safer door, or use a Unix socket and read the peer uid.
flowchart TD Z["Zygote<br/>/dev/socket/zygote"] -->|"fork request"| AP["new app process"] WM["WindowManager"] -->|"socketpair: InputChannel"| AP D["adbd"] -->|"abstract: jdwp, devtools"| AP AP -->|"TCP 127.0.0.1"| L["another app's local server"]
- Zygote takes fork requests from
system_serverover a reserved-namespace socket; the startup article follows that message. - Input reaches every window over an
InputChannel, which is a socket pair created by WindowManager when the window is added. - Chrome's remote debugging listens on an abstract socket named
chrome_devtools_remote; anadb forwardto that abstract socket is what desktop DevTools connects to. - Termux and its add-on apps pair the two doors: a broadcast names two abstract Unix sockets, and the command's input and output then stream over them.
An abstract-namespace socket has no file and therefore no file permissions; anything SELinux allows to connect can connect. Prefer a filesystem socket inside your data directory, or check getPeerCredentials() on every accepted connection.
The smaller doors: RemoteViews, storage and system-brokered data
A few mechanisms move data between apps without looking like IPC at all, and it helps to see that each is one of the doors above wearing a different coat.
- RemoteViews. A notification's custom layout or an app widget is not drawn by your app. You build a
RemoteViews, a Parcelable recipe of a layout resource and a list of setter calls, and SystemUI or the launcher inflates it in their process, resolving your resources throughPackageManager. Only the whitelisted view classes and setters work, which is why widgets feel limited. Jetpack Glance compiles a Compose-like tree down to exactly this. - Shared storage. Since scoped storage (Android 10 and 11) the shared parts of external storage are reached through
MediaStorefor media and the Storage Access Framework for documents, both content providers. Two apps that write and read the same file are doing provider IPC with a file descriptor at the end. sharedUserId. Apps signed with the same key could declare the same shared user id and literally become one uid with one data directory. It is deprecated since API 29 because it can never be undone for installed users; signature permissions and providers do the same job without fusing identities.- Clipboard.
ClipboardManageris a system service holding aClipData; since Android 10 only the foreground app and the current keyboard can read it, and since Android 12 the system shows a toast when an app reads it. - System-brokered services. Autofill, Accessibility, notification listeners and input methods all receive other apps' content, but only through
system_server, only after the user enabled them in Settings, and only as the platform's own AIDL interfaces describe.
- Every home-screen widget from a weather card to a calendar agenda is a
RemoteViewsinflated inside the launcher; when the launcher hangs, the widget freezes and the app is blameless. - Password managers such as 1Password and Bitwarden fill other apps' login forms through the Autofill framework, never by reading the screen directly.
- Gboard sees your text because it is the bound input method, and its clipboard suggestions exist because keyboards keep clipboard access.
Guarding the doors: knowing who is on the other side
Every door above is only as safe as the check on the receiving side, and every check comes down to the same two questions: which uid sent this, and does that uid hold the right permission or the right signature?
Answering "who is calling"
- Inside a Binder call,
Binder.getCallingUid()gives the sender's uid; it is set by the driver and cannot be forged.getCallingPid()is available too but a pid is recyclable and a uid is not. packageManager.getPackagesForUid(uid)turns it into package names. A uid maps to one package unless the apps share a user id.PackageManager.hasSigningCertificate, given the package and the expected SHA-256, confirms the package is signed by the key you expect, because a package name proves nothing by itself: anyone can publishcom.android.vendingon a sideloading site.- For a permission check,
checkCallingPermissionandenforceCallingPermissiondo steps 1 and the lookup in one call. Use the calling variants;checkSelfPermissionasks about your own process and always says yes. - If your service needs to call onwards with its own identity rather than the caller's, wrap the call in
Binder.clearCallingIdentity()andrestoreCallingIdentity(token), and be aware you have just made a decision on the caller's behalf.
Activities have weaker tools. callingPackage is set only when the caller used startActivityForResult, referrer is a hint the caller controls, and getLaunchedFromUid() arrived only in Android 14. An exported activity should therefore treat every extra as untrusted rather than trying to identify the sender.
Declaring what is allowed
| Door | Lock | Notes |
|---|---|---|
| Any component | android:exported="false" | The default when no intent filter is declared. Since Android 12 it must be spelled out when a filter is present. |
| Any component | android:permission | The caller must hold this permission. Define your own with <permission>. |
| Custom permission | protectionLevel="signature" | Granted automatically to apps signed with the same key as the definer, denied to everyone else. The standard lock for a suite of apps from one publisher. |
| Custom permission | protectionLevel="knownSigner" (Android 12) | Granted to apps signed with any certificate in a listed set, for partners with different keys. |
| Provider | readPermission, writePermission, <path-permission>, URI grants | Fine-grained by verb and path; grants punch temporary holes. |
| Broadcast | Permission on send, permission on receive, setPackage | Explicit broadcasts also bypass the Android 8 implicit restriction. |
| Bound service | android:permission plus checks inside each method | onBind sees the system's uid, not the client's. |
The traps with names
- Custom permission squatting. The first installed app to define a permission name owns its definition. Android 5 made installation fail when a second app with a different signature redefines it, but a permission that an older app defined as
normalstaysnormal. Prefix names with your package and declare them in every app that uses them. - Implicit PendingIntent hijack. A mutable PendingIntent built on an implicit intent can be filled in by whoever receives it and then fired with your identity; the classic outcome is another app starting your non-exported activity or reading your files through a URI grant. Always name the component, and use
FLAG_IMMUTABLEunless a system feature demands otherwise. - Intent redirection. Pulling an
Intentout of an extra and passing it tostartActivityorsetResultlets the caller reach components only you can reach. Check the component's package first, or strip the intent to what you expect.StrictMode.detectUnsafeIntentLaunch(), from Android 12, flags exactly this in debug builds. - Trusting a package name. Verify the signing certificate of any app you bind to or accept calls from, as the Play Billing library does with the Play Store.
- World-readable files.
MODE_WORLD_READABLEis gone since API 24 andfile://URIs throw since Android 7. Every shared file goes through a provider grant now, and that is the design, not a limitation.
- Google Play Billing checks that the bound service belongs to a package signed with Google's certificate before sending a purchase request; without it a fake store could sell your content.
- Digital Asset Links, the JSON file behind App Links and Trusted Web Activities, is the web-side version of "verify the signing certificate": a domain names the certificate hashes it trusts.
- Credential Manager and Sign in with Google verify the calling app's certificate before releasing a token; that is why a debug build with a different key gets a different (or no) result.
- Signature permission
- A permission that is granted automatically to apps signed with the same key as the app that defined it, and to no one else.
- Digital Asset Links
- A file at
/.well-known/assetlinks.jsonwhere a domain lists the Android apps (by package and certificate hash) it trusts. - Intent redirection
- Forwarding an intent received from an untrusted caller so that it runs with the forwarding app's identity and reach.
Choosing the door
The choice is usually decided by two questions: does the caller need a reply, and how big is the payload?
| Door | Shape | Reply | Payload | Who keeps whom alive | Reach for it when |
|---|---|---|---|---|---|
| Intent to an activity | One shot, user-visible | Optional, via activity result | Small (Bundle, under the Binder limit) | Neither; the system runs the flow | Handing the user to another app: share, pay, pick, view |
| Broadcast | One to many, fire and forget | None | Small | Sender does not know receivers exist | Announcing an event; reacting to system events |
| Content provider | Request and reply over data | Cursor, Bundle, file descriptor | Large: windows and files | Provider process is started on demand; an open cursor ties the two | Exposing tables or files; sharing a file with anyone |
| Bound service, AIDL | Conversation, typed API, callbacks | Synchronous return, oneway plus callback | Small per call; descriptors for bulk | Client keeps the service alive and lends it priority | An SDK or a companion app with a real API |
| Messenger | Serial commands | Via replyTo | Small | Same as any bound service | A small in-house service with a handful of commands |
| Shared memory, fds | Bulk bytes | None by itself | Anything that fits in RAM | Whoever maps the region pays for it | Frames, images, big buffers; always with a control channel beside it |
| Unix socket | Byte stream | Whatever protocol you write | Unlimited stream | Neither | Native code, existing protocols, talking to a daemon |
flowchart TD
S([Two processes need to talk]) --> U{User-visible?}
U -- Yes --> I["Intent to an activity,<br/>result if needed"]
U -- No --> R{Need a reply?}
R -- No --> B["Broadcast, or a oneway AIDL call"]
R -- Yes --> D{Data or API?}
D -- Rows or files --> P["Content provider"]
D -- Methods --> A["Bound service with AIDL"]
P --> X{Bulk bytes?}
A --> X
X -- Yes --> M["add a fd or SharedMemory"]
X -- No --> N["Parcel is fine"]
- Google Play services chose AIDL for everything because its clients need typed calls and callbacks over long sessions.
- The share sheet is an intent because the user is in the loop and the payload is a URI.
- MediaStore is a provider because the shape is tables plus files.
- Camera and video are shared buffers with a Binder control channel, because the payload is measured in megabytes per frame.
Watching it happen
All of this is visible from a shell, which turns "I think the broadcast was delivered" into a fact.
# Who is bound to what, with connection flags and client pids
adb shell dumpsys activity services org.example.counter
# Pending and historical broadcasts, per queue, with receivers and timings
adb shell dumpsys activity broadcasts
# Published providers and which clients hold references to them
adb shell dumpsys activity providers
# Every registered system service name (the ServiceManager table)
adb shell service list
# Drive the doors by hand
adb shell am start -a android.intent.action.VIEW -d "geo:0,0?q=coffee"
adb shell am broadcast -a org.example.SYNC_DONE -n org.example.companion/.SyncDoneReceiver
adb shell content query --uri content://org.example.files/documents --projection _id:name
adb shell cmd package query-activities --brief -a android.intent.action.SEND -t text/plain
# Binder traffic: per-process transaction stats collected by system_server
adb shell dumpsys binder_calls_stats
For timing, Perfetto with the binder tracks enabled draws every transaction as a slice on the calling thread ("binder transaction") with a matching "binder reply" slice on the server thread, so a slow bindService or a chatty SDK is visible as a wall of tiny bars. On a rooted device or emulator, /dev/binderfs/binder_logs/transactions lists every in-flight transaction with its buffer size, which is the fastest way to find out who is eating the 1 MB budget.
- StrictMode in a debug build catches unsafe intent launches and, with
detectAll, Binder calls on the main thread that took too long. - Perfetto's default Android trace config records Binder transactions, which is how startup investigations attribute time to
system_serverrather than to the app.
Mistakes that keep coming back
- A bitmap or a big list in an intent extra or saved state. The 1 MB budget is shared and
TransactionTooLargeExceptionappears only under load. Send a URI or a descriptor. - An implicit intent for a service, or an implicit broadcast to a manifest receiver, that "used to work". Both have been closed for years; make it explicit with
setPackageorsetClassName. - Checking the caller in
onBind. The system callsonBind. Check inside the methods. - Touching UI from an AIDL stub method or a Messenger handler on a background thread. Stubs run on Binder threads; post to the main thread or use a main-thread
Handler. - A synchronous remote call on the main thread that blocks while the other process starts. Bind early, call from a background dispatcher, or use oneway plus a callback.
- Reordering AIDL methods in a new release. Old clients now call the wrong method; append only.
- Building a PendingIntent on an implicit intent, or forwarding a nested intent from an extra. Both hand your identity to a stranger.
- Trusting a package name without checking its certificate.
- Holding a
Cursoropen across a screen's lifetime. If the provider dies, your process goes with it. - A local TCP server with no authentication. Every app with
INTERNETis a client.
Recap
- Every app is its own Linux uid in its own process with a private data directory and an SELinux domain; nothing is shared unless the platform brokers it.
- Binder is the transport under everything: a kernel driver, Parcels, one copy per transaction, a thread pool in the receiver, the caller's uid attached, synchronous unless
oneway. - The Binder buffer is about 1 MB per process, shared by all in-flight transactions. Intents, saved state and AIDL arguments all draw on it.
- Intents are brokered messages: explicit when the component is named, implicit when matched against intent filters. Results, App Links and PendingIntents are variations; Android 11, 12 and 14 tightened visibility, export declarations and mutability.
- Broadcasts fan out with no reply. Manifest receivers lost implicit broadcasts in Android 8; explicit ones still arrive;
onReceiveruns on the main thread with a 10 or 60 second limit. - Content providers put a URI and a fixed verb set in front of data. Cursors ride 2 MB shared-memory windows, files ride descriptors, and an open cursor ties the client's life to the provider's.
- Bound services hand out an
IBinder: a local class in-process, aMessengerfor serial messages, AIDL for a typed API with callbacks. Stubs run on Binder threads; transaction codes follow method order; only a few exception types cross. - Bulk data never goes through a Parcel. File descriptors are duplicated into the receiver;
SharedMemory,CursorWindow, immutable bitmaps andHardwareBufferall use that. - Unix domain sockets give a byte stream with peer credentials; loopback TCP gives neither identity nor isolation.
- RemoteViews, scoped storage, clipboard and Autofill are the same doors in different clothes, always brokered by
system_server. - Security is
getCallingUid, a certificate check, and explicit locks:exported,android:permission, signature permissions, URI grants. Never trust a package name, an implicit PendingIntent or a nested intent. - Choose by shape: user-visible means an intent, event means a broadcast, data means a provider, API means AIDL, bulk means a descriptor alongside one of those.
dumpsys activitywithservices,broadcastsorproviders, plusam,contentand Perfetto's binder slices show every one of these in motion.
Questions
Try answering each one out loud before opening it. Lead with the one-line answer, then the reasoning, then one detail that shows you have used it.
Why does Android need IPC at all, when the apps are on the same device?
Because every app runs as a separate Linux user in a separate process, so the kernel forbids one app from touching another's memory or files, and any cooperation has to go through a channel the platform provides and polices.
- Each package gets a uid from 10000 up, a private data directory with mode 0700, and the
untrusted_appSELinux domain. - The isolation is what makes the identity on every message trustworthy: the kernel stamps the sender's uid on each Binder transaction.
- Even a single app that uses
android:processor anisolatedProcesscrosses the same wall.
Chrome relies on this to run web content in renderers that have no permissions at all and can only reach the browser through Binder and shared memory.
What is Binder, and what does it give that sockets and pipes do not?
Binder is a kernel driver plus a thin library that turns a method call on a proxy object into a transaction delivered to a thread in another process, with the caller's uid attached and the data copied once.
- Objects, not bytes: an
IBinderkeeps its identity as it passes through several processes, with reference counting and death notification. - Built-in identity:
Binder.getCallingUid()is set by the driver and cannot be forged. - A receiver-side thread pool, so a call looks like a function call rather than a message loop.
Nested calls are delivered back to the thread that is already waiting for a reply, which is why a server can call back into a client during a transaction without deadlocking.
Walk through one synchronous Binder call from proxy to stub and back.
The proxy writes the arguments into a Parcel and issues an ioctl; the driver copies the Parcel into the receiver's mapped buffer and wakes a Binder thread there; that thread calls onTransact on the stub, which runs the real method and writes a reply Parcel; the driver copies the reply back and wakes the blocked caller.
- The client thread blocks the whole time; there is no timeout, only
DeadObjectExceptionif the server dies. - Binder references and file descriptors in the Parcel are rewritten by the driver as they cross.
- A
onewaycall skips the reply and returns as soon as it is queued.
The receiver's pool is capped at 16 threads for an app process; when all are busy, incoming calls wait, which is how one slow SDK can stall unrelated callers.
What causes TransactionTooLargeException, and why does it appear only sometimes?
Every Binder transaction into or out of a process shares one buffer of about 1 MB, so a large Parcel fails when the rest of the buffer happens to be in use, which depends on what else the process is doing at that instant.
- Intents, saved instance state, AIDL arguments and replies all draw on the same budget.
- Oneway transactions may use only half of the buffer.
- The usual culprits are bitmaps and long lists in extras.
The fix is never a smaller image; it is a content URI or a file descriptor, since descriptors are duplicated into the receiver and the bytes never enter the Parcel.
Explicit versus implicit intents: how is each resolved, and what changed in recent releases?
An explicit intent names a component and is delivered straight to it; an implicit intent describes an action, data and categories and is matched by PackageManager against installed intent filters, producing one target, a chooser, or ActivityNotFoundException.
- Android 5: service intents must be explicit.
- Android 11: package visibility hides other apps from
resolveActivityandqueryIntentActivitiesunless declared in<queries>;startActivityof an implicit intent still works. - Android 12:
android:exportedmust be declared on any component with an intent filter. - Android 14: implicit intents reach only exported components.
A common regression under Android 14 is an app sending an implicit intent to its own non-exported activity; the fix is setClassName or setPackage.
What is a PendingIntent, and what makes one dangerous?
A PendingIntent is a system-held token wrapping an intent and the creator's identity; whoever holds it can fire it later and the system performs the action as the creator, with the creator's permissions.
- Notifications, widgets, alarms and geofences all fire PendingIntents on the app's behalf.
- Since Android 12 it must be declared
FLAG_IMMUTABLEorFLAG_MUTABLE; immutable is the default choice. - A mutable one built on an implicit intent can be filled in by the holder and pointed at the creator's non-exported components: the hijack.
Android 14 goes further and throws when a mutable PendingIntent names neither a package nor a component.
Why did Android 8 restrict implicit broadcasts, and what still works?
Because a single system event such as a connectivity change woke every app with a matching manifest receiver, so Android 8 stopped delivering implicit broadcasts to manifest receivers of apps targeting API 26, with a short exempt list.
- Still delivered: explicit broadcasts (
setPackageorsetComponent), anything to a context-registered receiver, and exempt actions such asBOOT_COMPLETED,LOCALE_CHANGEDandMY_PACKAGE_REPLACED. - Replacements for the rest:
JobSchedulerandWorkManagerconstraints,ConnectivityManagercallbacks. - Android 14 added the requirement to say
RECEIVER_EXPORTEDorRECEIVER_NOT_EXPORTEDwhen registering.
Firebase Cloud Messaging survived because Play services sends it as an explicit broadcast to your package.
What are the constraints on onReceive, and how do you do real work from a broadcast?
onReceive runs on the main thread of the hosting process, must finish within 10 seconds for a foreground broadcast or 60 for a background one, and the process may exist only for that call, so real work is handed to WorkManager or a foreground service.
goAsync()keeps the process alive afteronReceivereturns, but only untilfinish()or the same timeout.- Starting a thread and returning does not help; the process can be killed as soon as the receiver is done.
- There is no reply channel; if the sender needs an answer this is the wrong mechanism.
The receiver's timeout is an ANR like any other, and background ANRs kill the process silently.
How does a Cursor from another app's provider get its data across, and what is the risk of holding it open?
The provider fills a CursorWindow, a 2 MB shared-memory page, and sends its descriptor; the client reads rows from the mapped window and fetches the next window as it scrolls, so large results never touch the Binder limit.
- After the first lookup through
ActivityManager, calls go directly from client to provider process. - An open cursor holds a stable reference to the provider; if the provider process dies, the system kills the client too.
- A single row larger than the window fails with
SQLiteBlobTooBigException.
This is the reason a crash in the contacts provider can take down a dialer that merely had a cursor open, and why cursors should be read into memory and closed.
How do you share a private file with another app today?
Through a FileProvider: map the directory in XML, get a content:// URI for the file, attach it to the intent with FLAG_GRANT_READ_URI_PERMISSION, and the receiving component may open it until its task ends, with no permanent permission and no exposed path.
file://URIs throwFileUriExposedExceptionsince Android 7; world-readable modes were removed in API 24.- The other side calls
contentResolver.openInputStream(uri), which becomes anopenFilein your provider and a duplicated file descriptor. - For longer access use
grantUriPermission, or persistable grants via the Storage Access Framework.
The provider itself stays exported="false"; the grant is what opens it, and only for that URI and that recipient.
Compare a local Binder, a Messenger and AIDL for a bound service.
A local Binder subclass works only in-process and is just a cast; a Messenger crosses processes with Message objects delivered one at a time on a Handler; AIDL crosses processes with a typed interface whose methods run concurrently on Binder threads.
- Messenger is AIDL underneath, using the framework's
IMessengerinterface, and gives serial delivery for free. - AIDL supports return values,
in/out/inoutparameters,onewayand callback interfaces. - Anything between two apps, or anything with a real API, wants AIDL; a small in-house service with a few commands is fine with Messenger.
Stub.asInterface returns the real object when the binder is local, so the same AIDL service can be used in-process with zero IPC cost.
Which thread runs an AIDL stub method, and why does it matter?
A thread from the service process's Binder pool, chosen by the driver, so methods can run concurrently and never on the main thread unless the caller was in-process.
- Shared state in the service needs synchronisation or thread-safe types.
- UI or lifecycle objects must not be touched directly; post to the main thread.
- The caller's thread is blocked meanwhile, so a slow method is the caller's ANR.
Only a fixed list of exceptions crosses back to the caller; any other exception thrown in the stub crashes the service, and the client sees DeadObjectException.
How do you keep an AIDL interface compatible across versions of the two apps?
Only append methods, never reorder or remove them, because transaction codes are assigned from method order and a mismatch makes the wrong method run silently.
- Add a version query method early so each side can discover the other's capabilities.
- Keep Parcelable layouts backward compatible or version them explicitly; unparcelling a changed layout throws or misreads.
- Prefer
Bundlearguments for optional, evolving data.
The platform's own stable AIDL tooling enforces these rules with hashes; an app-level interface has to enforce them by discipline.
How does big data such as an image or a frame cross processes without hitting the 1 MB limit?
The bytes go into memory both processes can map, and only a file descriptor crosses Binder; the driver duplicates the descriptor into the receiver, which maps the same pages with no copy.
SharedMemory(ashmem or memfd) withsetProtect(PROT_READ)before sharing.ParcelFileDescriptorfor files and pipes;CursorWindowfor query rows; immutable bitmaps are parcelled as ashmem automatically.HardwareBufferandSurfacefor graphics, backed by dma-buf.
SurfaceFlinger never receives a pixel over Binder; a frame is a buffer handle and a fence.
How does a service verify who is calling it, and where must that check not be done?
Inside each interface method, read Binder.getCallingUid(), map it to packages with getPackagesForUid, and confirm the signing certificate with hasSigningCertificate or enforce a permission with enforceCallingPermission; never do it in onBind, which is called by the system.
- The uid comes from the driver and cannot be spoofed; a pid can be recycled.
- A package name proves nothing without its certificate.
checkSelfPermissionasks about your own process and is the wrong call.
Play Billing does exactly this in reverse, verifying that the Play Store it bound to is signed by Google before sending a purchase.
What is a signature permission, and when would you use knownSigner instead?
A signature permission is granted automatically to any app signed with the same key as the app that defined it and denied to everyone else; knownSigner, from Android 12, extends the grant to a listed set of certificates for partners with different keys.
- It is the standard lock between a publisher's own apps.
- Declare the permission in every app that uses it so installation order does not matter.
- Prefix the name with your package to avoid collisions.
Custom permission squatting, where an earlier install defines the name with a weaker level, is why the definition should live in all participants.
Given a requirement, how do you pick between an intent, a broadcast, a provider and AIDL?
If the user is in the loop it is an intent; if it is an announcement with no reply it is a broadcast; if the shape is rows or files it is a provider; if it is an API with calls and callbacks it is a bound service with AIDL; and bulk bytes ride a descriptor alongside whichever of those you chose.
- Providers and AIDL are conversations that start via
ActivityManagerand continue process to process. - Binding lends the client's priority to the service; nothing else does.
- Sockets are for native code and existing protocols, with peer credentials as the identity check.
Google Play services picked AIDL for everything; the share sheet is an intent; MediaStore is a provider; camera frames are shared buffers with a Binder control channel.
How would you debug a bound service that never connects, from the shell?
Run dumpsys activity services for the package to see whether a binding exists and to which process, check logcat for "Unable to start service Intent ... not found" or a package visibility denial, and confirm the intent is explicit and the target is visible in <queries>.
- An implicit service intent throws on Android 5 and later.
- Without a
<queries>entry on Android 11 and later the service resolves to nothing. onServiceDisconnectedfiring immediately means the service process crashed inonBind.
Perfetto's binder slices show the bind transaction and how long the service process took to start, which separates a slow start from a broken one.