What these flags actually do
The am ("activity manager") subcommand of adb shell is how you
invoke an Android Intent from your laptop. Every flag below maps onto a
property of the Intent object your activity receives in
onCreate or onNewIntent. There's no magic. Once you've
written the equivalent Kotlin a few times, the flag syntax stops feeling cryptic.
What you're invoking: the action
-a <action> defines what kind of operation you're trying
to perform, not where. For deep links, this is overwhelmingly
android.intent.action.VIEW, which translates to "show this thing to the
user." Sharing is SEND, editing flows are EDIT, telephony
is DIAL or CALL. The most common testing mistake is
omitting the action entirely. The default is MAIN, which doesn't match
deep-link intent filters, so you end up at the app's launcher activity instead of
your handler and you can't figure out why your "deep link test" is broken.
What you're invoking on: the data URI
-d <data URI> is the URL itself, the thing your
<intent-filter> matches against. Always wrap it in double quotes.
The shell otherwise treats &, ?, and #
as control characters and your URL gets truncated silently. If you see only part of
your query string arriving in the app, this is almost always the reason. The
"silently" is the killer, because the command appears to succeed.
Which app gets the intent: targeting flags
Two flags control resolution. -n <package/activity> bypasses
Android's intent resolution entirely and sends the intent to one specific Activity.
That's useful when you want to skip the disambiguation dialog and target a particular
entry point, like testing a deep-link activity that isn't your default launcher. The
format is either relative (com.example.app/.MainActivity) or fully
qualified (com.example.app/com.example.app.MainActivity); either works.
-p <package> is less aggressive: it lets Android resolve the
intent normally, but only among intent filters declared by the named package. This
is the one to remember if you remember nothing else. It's how you verify your app
handles an HTTPS universal link instead of Chrome winning the resolution race
because your App Links aren't auto-verified yet. Use -p for "did my
app handle this correctly," and -n only when you specifically want to
bypass resolution.
Category and extras
Almost all deep links arriving from external sources, like browsers and messaging
apps, carry the BROWSABLE category. If you skip
-c android.intent.category.BROWSABLE during testing, you may match an
intent filter that wouldn't actually match in production: your app appears to
handle the deep link locally but breaks when a real user taps the link in Chrome.
Default to BROWSABLE on; turn it off only if you have a specific reason, like
testing an internal share-target activity that doesn't declare it.
The extras flags pass typed key-value pairs into the intent:
--es for strings, --ei for ints, --ez for
booleans, --ef for floats, --el for longs. These map onto
intent.getStringExtra("key") and friends. Worth knowing: deep-link
query parameters are not automatically passed as extras. They live inside
intent.getData() and your activity extracts them via
intent.getData().getQueryParameter("foo"). If you actually need them
as extras, you have to set them explicitly, either with --es on the
am command line or by reading-then-writing inside the receiving activity.
Process and timing control
-W makes the command synchronously wait for the activity launch to
complete, and prints timing info when it does. It's almost always worth including
while testing. Without it, am returns the moment the launch is
dispatched, not the moment your activity actually appears, so failures look like
successes and you don't see the error log until much later. The cold-start timing
data -W reports is also useful for performance work, though for
serious profiling you'd use other tools.
-S force-kills the app before launching the deep link. The reason to
care: if your app is already running, the deep link arrives via
onNewIntent; with -S it arrives via onCreate.
These are two different code paths in your app, and countless deep-link bugs only
manifest in one or the other. If you're not testing both, you're shipping a bug
you haven't found yet.
-D pauses the app at launch until a debugger attaches. Pair it with
Android Studio's Run → Attach Debugger workflow when you need to step
through your onCreate or onNewIntent handler from the
very first instruction. Mostly useful for chasing a bug that only manifests on
cold start. By the time you'd normally attach the debugger after launch, the
interesting code has already run.
The user profile flag, briefly
--user <id> targets a specific user profile on a multi-user
device. User 0 is the primary owner; 10 and above are
work profiles, secondary users, and the like. Most developers never need this. If
you support enterprise managed profiles, you'll eventually hit a bug that only
reproduces in one specific profile, and this is how you reach it from the command
line.
Why am start lies
The single most expensive thing to learn about am start is that
"success" exit code doesn't mean what you think. The default behaviour is fire-and-forget:
am hands the intent to ActivityManager and returns 0, regardless of whether
an Activity ever actually launched. If your app crashes during onCreate,
if no Activity matches the intent, or if the intent is dispatched to a different app
than the one you expected, am start still prints
Starting: Intent { ... } and exits zero. You see green and move on, then
spend forty minutes wondering why your breakpoint never hit.
Three things help. First, always test with -W. The wait flag turns
am synchronous and prints a small status block:
Status: ok / Activity: com.example.app/.MainActivity / ThisTime: 412 / TotalTime: 412.
If the activity name in the output doesn't match what you expected, you've hit an intent
resolution bug rather than a code bug. Second, run adb logcat -d -t 200 *:E ActivityManager:I
immediately after. ActivityManager logs real-time intent dispatch decisions
and any ActivityNotFoundException shows up there before am
even returns. Third, when chasing a "deep link doesn't trigger anything" bug, install with
adb install -r -t -d on a clean target. Leftover builds from yesterday's
feature branch will absorb your intent silently if their intent-filter happens to match.
Recipes that come up often
Verify a universal link reaches your app, not the browser
adb shell am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://example.com/products/42" -p com.example.app.
Restricting resolution to your package with -p means Android will throw rather
than silently hand it to Chrome, which is exactly the failure mode you want to surface
during testing. If it throws, your autoVerify isn't set up or your
assetlinks.json is missing or stale.
Test the deep link path while the app is already foreground
Omit -S and the intent arrives in your existing process via
onNewIntent. This is the path most apps under-test because the convenient
debugging workflow always starts from a fresh launch. Half of "the app handles the link
correctly the first time but the wrong screen second time" bugs live in
onNewIntent handlers that forgot to call setIntent(intent)
or that read stale Activity state.
Pass a typed payload alongside the URI
--es source notification --ei retry_count 3 --ez from_widget true sets
a string, an int, and a boolean as Intent extras. These travel under
intent.getStringExtra("source") rather than as query parameters, and
they bypass any URL encoding entirely. Useful for reproducing a notification-driven
deep link where the notification was building its intent in Kotlin with typed extras.
A query-string-only reproduction misses the data type and order.
When ADB itself is the problem
Plenty of "deep-link bugs" turn out to be ADB connectivity issues mis-diagnosed. Three
quick checks rule that out. adb devices should list exactly one device
(or one emulator and one device, but you should be targeting one explicitly with
-s). If a device shows as unauthorized, accept the RSA-key
dialog on the device; if it shows as offline, run adb kill-server
then adb start-server. If multiple devices show and your am start
hits the wrong one, you'll get a successful exit but no behaviour on the device you're
staring at, which is embarrassing to discover after an hour of code review.
On wireless ADB (Android 11+), the connection silently drops when the laptop suspends,
when the device reboots, or when either changes network. The behaviour you'll observe
is "command timed out" or "no devices/emulators found." Re-pair with
adb pair host:port and the six-digit code from Settings → Developer
options → Wireless debugging. Wired USB is more reliable for an active debug session;
wireless is convenient for occasional one-off launches.