Android · IPC

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.

Updated 2026-09-10Kotlin · Framework internals
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.

Figure 1. The whole article on one page. Every branch is a section below; fold what you know, open what you do not.

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
Figure 2. Two apps never touch. Every conversation goes down into the kernel, and almost every one is brokered by 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.

In the wild
  • Chrome runs web content in isolatedProcess renderers 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_app is 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

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
Figure 3. One synchronous Binder call. The client thread blocks from step 2 to step 8; a thread from the receiver's pool does the work in between. Steps 1 and 6 are the only lines that look like ordinary code.

What a transaction guarantees

  1. 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.
  2. 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_N in a thread dump; an app process allows up to 16 of them. If none is idle and the pool is full, the call waits.
  3. 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.
  4. 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 same IBinder can be passed around three processes and still be recognised as one object. File descriptors are translated too, by duplicating them into the receiver.
  5. 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 DeadObjectException when the receiver dies.
  6. 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.

Watch out
  • 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.

In the wild
  • Every system service call is a Binder transaction into system_server: startActivity, getSystemService(...).something(), drawing a frame through WindowManager. 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.gms process.
  • Chrome Custom Tabs is an AIDL service inside Chrome; the CustomTabsService a 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/hwbinder and /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
Figure 4. Intent resolution inside 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:

Results, links and tokens

Three variations on the theme cover most cross-app flows:

// 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

ReleaseRuleWhat breaks without it
Android 5Service intents must be explicit.bindService with an implicit intent throws.
Android 7file:// URIs may not leave the app.FileUriExposedException; use a FileProvider content URI.
Android 11Package 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 12Components with an intent filter must declare android:exported; PendingIntents must say FLAG_IMMUTABLE or FLAG_MUTABLE.Install fails; PendingIntent creation throws.
Android 14Implicit 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.
In the wild
  • The share sheet is ACTION_SEND with a MIME type; every app that shows up there declared a matching filter. Direct Share targets on top of it come from ShortcutManager.
  • 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 and google.navigation: intents, which is how a food delivery app hands off a route without embedding a map.
  • The camera via MediaStore.ACTION_IMAGE_CAPTURE is an implicit intent that carries a content URI the camera app writes into, a small IPC round trip that avoids the camera permission entirely.
Watch out

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 holds QUERY_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
Figure 5. A broadcast fans out inside 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:

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()
        }
    }
}
Watch out
  • onReceive runs 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 a WorkManager job 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.
  • LocalBroadcastManager is deprecated and was never IPC; inside one process a SharedFlow or StateFlow from the coroutines article does the same job with types.
In the wild
  • ACTION_BOOT_COMPLETED is 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 to SMS_RECEIVED but 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 onReceive returns, keeping the process alive until finish() 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
Figure 6. ActivityManager is involved only in the introduction. Once the client holds the provider's binder, every call goes process to process.
  1. The first call for an authority asks ActivityManager for it. If the provider's process is not running, the system starts it, and the provider's onCreate runs before the app's own Application.onCreate, which the startup article covers.
  2. The client receives an IContentProvider binder and caches it. Later calls skip system_server entirely.
  3. 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's Cursor reads 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 with SQLiteBlobTooBigException.
  4. 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.

In the wild
  • Contacts, calendar and call log are providers inside system apps; every dialer, messenger and calendar client reads the same tables through ContactsContract and 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.getString is a call() 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_PERMISSION or grantUriPermission is called.
Stable reference
A provider connection whose holder is killed if the provider dies; an open Cursor holds 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
Figure 7. Binding. As with providers, ActivityManager introduces the two sides and then steps out of the way.

Three ways to build the binder

Local Binder subclassMessengerAIDL
Crosses processesNo. The client casts the binder and calls the service object directly.YesYes
ShapeOrdinary Kotlin methodsMessage objects with a what code and a BundleTyped interface methods with arguments and return values
Threading in the serviceCaller's threadOne Handler, so one message at a time, in orderBinder thread pool, concurrent
RepliesReturn valuesSend a Message back via replyToReturn values, out parameters, or a callback interface
Use whenSame process: a music player's UI binding to its own playback serviceSimple commands, no need for concurrency, no build-time interfaceAnything 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:

Watch out
  • onBind is called by ActivityManager, so Binder.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.
In the wild
  • 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 bindService to CustomTabsService, 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: onServiceConnected with the binder, onServiceDisconnected when 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
Figure 8. Shared memory. The pages exist once; both processes map them. The only thing that crosses Binder is a descriptor a few bytes long.
ObjectWhat it isTypical use
ParcelFileDescriptorA Parcelable wrapper around any fdReturning a file from ContentProvider.openFile; passing a socket or pipe end in an AIDL call
ParcelFileDescriptor.createPipe()A pipe, two descriptorsStreaming 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 viewLarge buffers between your app's processes, or into an isolated renderer
CursorWindowA shared-memory page of query rowsEvery Cursor that crosses a process, silently
Bitmap (immutable)Parcelled as an ashmem descriptor rather than inline bytesNotifications and widgets ship images to SystemUI this way
HardwareBufferA GPU-visible buffer backed by dma-bufCamera frames, video decoder output, every window's frames
SurfaceThe producer end of a BufferQueueHanding 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.

In the wild
  • SurfaceFlinger never receives pixels over Binder. Every frame an app draws lands in a HardwareBuffer that the compositor maps; Binder carries the buffer handle and a fence.
  • Camera2 hands the camera HAL a Surface from your preview; frames are written by the ISP into buffers your view already has mapped.
  • MediaCodec exposes decoder output as buffers or a Surface for 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_create is 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 Surface is 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"]
Figure 9. Sockets an app process is already attached to before it has done anything, plus the one it might open itself.
In the wild
  • Zygote takes fork requests from system_server over 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; an adb forward to 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.
Watch out

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.

In the wild
  • Every home-screen widget from a weather card to a calendar agenda is a RemoteViews inflated 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"

  1. 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.
  2. packageManager.getPackagesForUid(uid) turns it into package names. A uid maps to one package unless the apps share a user id.
  3. 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 publish com.android.vending on a sideloading site.
  4. For a permission check, checkCallingPermission and enforceCallingPermission do steps 1 and the lookup in one call. Use the calling variants; checkSelfPermission asks about your own process and always says yes.
  5. If your service needs to call onwards with its own identity rather than the caller's, wrap the call in Binder.clearCallingIdentity() and restoreCallingIdentity(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

DoorLockNotes
Any componentandroid:exported="false"The default when no intent filter is declared. Since Android 12 it must be spelled out when a filter is present.
Any componentandroid:permissionThe caller must hold this permission. Define your own with <permission>.
Custom permissionprotectionLevel="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 permissionprotectionLevel="knownSigner" (Android 12)Granted to apps signed with any certificate in a listed set, for partners with different keys.
ProviderreadPermission, writePermission, <path-permission>, URI grantsFine-grained by verb and path; grants punch temporary holes.
BroadcastPermission on send, permission on receive, setPackageExplicit broadcasts also bypass the Android 8 implicit restriction.
Bound serviceandroid:permission plus checks inside each methodonBind sees the system's uid, not the client's.

The traps with names

In the wild
  • 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.json where 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?

DoorShapeReplyPayloadWho keeps whom aliveReach for it when
Intent to an activityOne shot, user-visibleOptional, via activity resultSmall (Bundle, under the Binder limit)Neither; the system runs the flowHanding the user to another app: share, pay, pick, view
BroadcastOne to many, fire and forgetNoneSmallSender does not know receivers existAnnouncing an event; reacting to system events
Content providerRequest and reply over dataCursor, Bundle, file descriptorLarge: windows and filesProvider process is started on demand; an open cursor ties the twoExposing tables or files; sharing a file with anyone
Bound service, AIDLConversation, typed API, callbacksSynchronous return, oneway plus callbackSmall per call; descriptors for bulkClient keeps the service alive and lends it priorityAn SDK or a companion app with a real API
MessengerSerial commandsVia replyToSmallSame as any bound serviceA small in-house service with a handful of commands
Shared memory, fdsBulk bytesNone by itselfAnything that fits in RAMWhoever maps the region pays for itFrames, images, big buffers; always with a control channel beside it
Unix socketByte streamWhatever protocol you writeUnlimited streamNeitherNative 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"]
Figure 10. A decision path that lands on the right door in most cases. Shared memory is never the whole answer; it rides on one of the others.
In the wild
  • 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.

In the wild
  • 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_server rather than to the app.

Mistakes that keep coming back

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; onReceive runs 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, a Messenger for 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 and HardwareBuffer all 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 activity with services, broadcasts or providers, plus am, content and 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_app SELinux 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:process or an isolatedProcess crosses 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 IBinder keeps 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 DeadObjectException if the server dies.
  • Binder references and file descriptors in the Parcel are rewritten by the driver as they cross.
  • A oneway call 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 resolveActivity and queryIntentActivities unless declared in <queries>; startActivity of an implicit intent still works.
  • Android 12: android:exported must 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_IMMUTABLE or FLAG_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 (setPackage or setComponent), anything to a context-registered receiver, and exempt actions such as BOOT_COMPLETED, LOCALE_CHANGED and MY_PACKAGE_REPLACED.
  • Replacements for the rest: JobScheduler and WorkManager constraints, ConnectivityManager callbacks.
  • Android 14 added the requirement to say RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED when 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 after onReceive returns, but only until finish() 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 throw FileUriExposedException since Android 7; world-readable modes were removed in API 24.
  • The other side calls contentResolver.openInputStream(uri), which becomes an openFile in 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 IMessenger interface, and gives serial delivery for free.
  • AIDL supports return values, in/out/inout parameters, oneway and 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 Bundle arguments 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) with setProtect(PROT_READ) before sharing.
  • ParcelFileDescriptor for files and pipes; CursorWindow for query rows; immutable bitmaps are parcelled as ashmem automatically.
  • HardwareBuffer and Surface for 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.
  • checkSelfPermission asks 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 ActivityManager and 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.
  • onServiceDisconnected firing immediately means the service process crashed in onBind.

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.