How to Test Deep Links on Android: ADB Commands, App Links and Real Devices
Open and test an Android deep link with one adb command, verify App Links, automate with Espresso, and test real-device flows from email, SMS and push.
Originally written by Eric Goebelbecker. Updated October 2026 by Team Mobot.
Quick answer: To test a deep link on Android, connect a device or start an emulator, then run adb shell am start -W -a android.intent.action.VIEW -d "<your-uri>" <your.package.name>. If the right screen opens, the intent filter works. For https App Links, also check verification with adb shell pm get-app-links <your.package.name>.
# 1. Find the device
adb devices
# 2. Open a custom-scheme deep link
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://product/123?ref=email" com.example.app
# 3. Open an https App Link (omit the package to test real system routing)
adb shell am start -W -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://www.example.com/product/123"
# 4. Check App Links verification state (Android 12+)
adb shell pm get-app-links com.example.app
# 5. Re-run verification after changing assetlinks.json (Android 12+)
adb shell pm verify-app-links --re-verify com.example.app
# Several devices attached? Target one with -s
adb -s emulator-5554 shell am start -W -a android.intent.action.VIEW -d "myapp://home"
If your link has more than one query parameter, the & needs extra quoting. See Why does adb cut off my query string? below.
What is a deep link on Android? (custom schemes vs App Links)
A deep link is a URI that opens a specific screen in your app instead of its home screen. Android supports three kinds:
- Custom-scheme deep links use your own scheme, such as
myapp://product/123. Any app can register the same scheme, so the system may show a chooser. - Web links use
httporhttps. On Android 12 and higher, a web link that isn't verified for your app usually opens in the browser. - App Links are verified https links. You add
android:autoVerify="true"to the intent filter and publish/.well-known/assetlinks.jsonon your domain. Once Android verifies the domain, it opens those links in your app with no chooser.
Deferred deep links handle the case where the app isn't installed yet: the user installs it, then lands on the linked content on first open. Android doesn't do this on its own. It usually takes an install-referrer or attribution SDK.
The sample app in this guide uses one custom scheme (dleg) and one web link. Here's the custom-scheme filter:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="www.example.com" />
<data android:scheme="dleg" />
</intent-filter>
Android sends links like dleg://www.example.com to this app.
And here's the web link filter, with autoVerify so it can become a verified App Link:
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="www.example.com" />
<data android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
Android merges multiple <data> elements in one filter. If you put two different scheme-and-host pairs in the same filter, every combination of them matches. Use separate filters for separate URL patterns.
The app reads the link in MainActivity.onCreate(). If the URI has an op parameter, it starts NewActivity. If it has a text parameter, it shows that text on the main screen:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// ... toolbar and navigation setup omitted ...
handleIntent(intent)
}
private fun handleIntent(intent: Intent?) {
if (intent?.action == Intent.ACTION_VIEW) {
intent.data?.let { processDeepLink(it) }
}
}
private fun processDeepLink(appLinkData: Uri) {
val opValue = appLinkData.getQueryParameter("op")
val textValue = appLinkData.getQueryParameter("text")
if (!opValue.isNullOrBlank()) {
startActivity(Intent(this, NewActivity::class.java))
} else if (!textValue.isNullOrBlank()) {
findViewById<TextView>(R.id.textview_first).text = textValue
}
}
To follow along, use a current version of Android Studio and the Android SDK.
How do you open a deep link with ADB?
The Android Debug Bridge (adb) can send a VIEW intent through the activity manager (am), using the adb test syntax from Android's deep-linking guide. It works the same on an emulator and on a USB- or Wi-Fi-connected phone.
- Install your debug build and connect a device. Run
adb devicesand check that the device shows the statedevice, notoffline. - Find the package name. Run
adb shell pm list packages <part-of-name>. For example,adb shell pm list packages example. - Send the intent. Run
adb shell am start -W -a android.intent.action.VIEW -d "<uri>" <package>. - Check the output and the screen. Look for
Status: okand theActivity:line that names the activity that received the intent. Then check that the right screen and content appeared. - Repeat with edge cases. Try missing parameters, bad IDs, the logged-out state, and the app killed vs backgrounded. To test a cold start, add
-S, which force-stops the app before the launch. - Read the logs. Run
adb logcatin a second terminal while you send the link, and look for the activity that received the intent and any errors.adb logcat "*:W"shows only warnings and errors (logcat docs).
adb devices
adb shell pm list packages example
# Send the link and wait for the launch to finish
adb shell am start -W -a android.intent.action.VIEW \
-d "dleg://www.example.com?text=hello" com.example.app
# Same link from a cold start (force-stops the app first)
adb shell am start -W -S -a android.intent.action.VIEW \
-d "dleg://www.example.com?text=hello" com.example.app
# In a second terminal: watch the log while you send links
adb logcat
# Only warnings and errors (quote the * so your shell leaves it alone)
adb logcat "*:W"
Example output. Output from a successful launch looks like this (values vary by device and Android version):
Starting: Intent { act=android.intent.action.VIEW dat=dleg://www.example.com/... pkg=com.example.app }
Status: ok
LaunchState: UNKNOWN (-1)
Activity: com.example.app/.MainActivity
WaitTime: 155
Complete
What do the flags mean?
adb -s <serial>: send the command to one device when several are attached. Get the serial fromadb devices, for exampleemulator-5554. Without it, adb returns "more than one device/emulator".shell am start: run the activity manager on the device and start an activity.-W: wait for the launch to complete before returning, so you getStatus,ActivityandWaitTime.-S: force-stop the app before starting it. Use it to test cold starts.-a android.intent.action.VIEW: the intent action that links use.-c android.intent.category.BROWSABLE: add theBROWSABLEcategory to the intent. Google's App Links guide uses it when checking that a web link opens your app.-d <uri>: the data URI, which is your deep link.<package>(last argument): limits the intent to your app. Leave it off to test real routing. Android then picks the handler the way it would for a user, which is how you find out whether a chooser or the browser appears instead of your app.--debug-link: on Android 17 and higher, prints how the system resolved the URL and which intent filters matched (Android 17+ link diagnostics).
For every option, see the full am start options.
Why does adb cut off my query string?
adb passes your command to the device shell the way ssh does (Android Platform Tools 23 and higher). Your local shell removes one layer of quotes, and the device shell reads what's left. So in this command, the device shell sees a bare & and splits the command there. am start gets only myapp://product?id=123, the package name is lost, and the rest runs as a separate, broken command:
# Wrong: the device shell treats & as "run in background"
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://product?id=123&ref=email" com.example.app
Use either fix:
# Fix 1: quote twice. Your shell removes the double quotes, the device shell sees the single quotes
adb shell am start -W -a android.intent.action.VIEW \
-d "'myapp://product?id=123&ref=email'" com.example.app
# Fix 2: escape the ampersand
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://product?id=123\&ref=email" com.example.app
Spaces cause the same problem. Encode them as %20 or use the double-quoting fix.
Why does adb open the browser or a chooser instead of my app?
- The App Link isn't verified. On Android 12 and higher, an unverified https link opens in the default browser. Check with
pm get-app-links(next section). - The host or path doesn't match. The link's scheme, host and path have to match a
<data>element in your filter.example.comandwww.example.comare different hosts. - The filter is missing the
BROWSABLEorDEFAULTcategory. WithoutBROWSABLE, links tapped in a browser can't reach your app. WithoutDEFAULT, implicit intents can't start the activity. - Another app claims the same custom scheme. Custom schemes aren't verified, so Android shows a chooser. Use App Links for links to your own domain.
- The user picked a different default. Users can change which app opens a domain in system settings.
On Android 17 and higher, --debug-link shows you why. It lists every app that matched the URL, the intent filter attributes that matched, and the App Links verification state:
adb shell am start --debug-link -a android.intent.action.VIEW \
-d "https://www.example.com/product/123"
How do you verify Android App Links?
After you install the app, Android checks https://<your-host>/.well-known/assetlinks.json for every host in an autoVerify filter. On Android 12 and higher you can run that check yourself with Android's App Links verification commands:
# 1. Reset the app's App Links state
adb shell pm set-app-links --package com.example.app 0 all
# 2. Run verification again (the device needs an internet connection)
adb shell pm verify-app-links --re-verify com.example.app
# 3. Wait a few minutes, then read the result
adb shell pm get-app-links com.example.app
The output lists each domain with a state:
com.example.app:
ID: 01234567-89ab-cdef-0123-456789abcdef
Signatures: [***]
Domain verification state:
www.example.com: verified
verified: the domain passed. Links to it open your app.none: nothing has been recorded yet. Wait a few minutes and run verification again.legacy_failureor a number of 1024 or higher: verification failed. Check yourassetlinks.jsonfile.
Common assetlinks.json mistakes:
- Wrong SHA-256 fingerprint. It has to match the certificate that signed the build on the device. If you use Play App Signing, copy the fingerprint from the Play Console, not from your local keystore. If debug and release builds use different keys, list both fingerprints.
- Redirects. The file must load over HTTPS with no 301 or 302 redirects.
- Wrong content type. Serve it as
application/json. - A missing host. Each host in your filters needs its own file.
How do you automate deep link tests with Espresso?
adb is good for checking links by hand while you code, but someone still has to look at the screen. Espresso and AndroidX Test let you check the result in an automated test. The examples below use current AndroidX APIs: ActivityScenario, ActivityScenarioRule and Espresso-Intents' IntentsRule. These examples use ActivityScenario and ActivityScenarioRule from AndroidX Test.
Add the AndroidX Test dependencies to app/build.gradle.kts, using the current versions from the AndroidX Test release notes:
androidTestImplementation("androidx.test:core:<version>")
androidTestImplementation("androidx.test:runner:<version>")
androidTestImplementation("androidx.test.ext:junit:<version>")
androidTestImplementation("androidx.test.espresso:espresso-core:<version>")
androidTestImplementation("androidx.test.espresso:espresso-intents:<version>")
Test that a link starts the right activity (Espresso-Intents)
Espresso-Intents records the intents your app sends. intended() checks that a matching one was sent. This test opens MainActivity with a link that carries op, then checks that the app started NewActivity:
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.intent.rule.IntentsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AppDeepLinkingIntentsTest {
// Calls Intents.init() before each test and Intents.release() after it
@get:Rule
val intentsRule = IntentsRule()
@Test
fun deepLinkWithOpStartsNewActivity() {
val context = ApplicationProvider.getApplicationContext<Context>()
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse("dleg://www.example.com?op=doit"),
context,
MainActivity::class.java
)
ActivityScenario.launch<MainActivity>(intent).use {
intended(hasComponent(NewActivity::class.java.name))
}
}
}
IntentsRule replaces the manual Intents.init() and Intents.release() calls. use { } closes the scenario when the test ends.
Test that a link changes the UI (ActivityScenarioRule)
ActivityScenarioRule launches the activity with your intent before each test and closes it afterwards. Espresso then checks that the text from the link is on screen:
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AppDeepLinkingTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
private val linkIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("dleg://www.example.com?text=foo"),
context,
MainActivity::class.java
)
@get:Rule
val activityRule = ActivityScenarioRule<MainActivity>(linkIntent)
@Test
fun deepLinkTextParamIsShown() {
onView(withId(R.id.textview_first)).check(matches(withText("foo")))
}
}
For the web link, change only the URI, for example Uri.parse("https://www.example.com?text=bar").
What these tests prove, and what they don't. They prove that your activity parses the URI and does the right thing with it. They don't prove that Android sends a real link to your app. Each test names the activity directly, so it skips your manifest's intent filters, App Links verification, the chooser, and the app the user tapped the link in. Cover those with adb (leave off the package name) and with tests on real devices.
Emulator vs real device: what changes when you test deep links?
To open a deep link in the Android emulator, use the same adb command. If more than one device is attached, add -s emulator-5554, using the serial from adb devices. You can also paste the link into a message or note inside the emulator and tap it. Where the emulator and a phone differ:
- adb
am startwith a URI. Emulator: works. Real device: works. - A link tapped in a real email, SMS or chat app. Emulator: only if those apps are installed and signed in, which they usually aren't. Real device: the apps your users actually use.
- A QR code scanned with the camera. Emulator: the camera shows a virtual scene or your computer's webcam, and you can load a QR image into the virtual scene. Real device: the phone's real camera scans a real code.
- A push notification that carries a deep link. Emulator: depends on a Google Play system image and your FCM setup. Real device: real FCM delivery to a real device token.
- Default browser and default-app behavior. Emulator: Google system images, so vendor browsers and OEM settings usually aren't there. Real device: matches the phones your users have.
- Cold start from the lock screen or the notification shade. Emulator: you can approximate it. Real device: shows the real OS behavior on that phone and OS version.
For more detail, see physical robots vs emulators and why test on real devices.
How do you test deep links end to end on real devices?
Most deep link bugs show up outside your app: in the email client, the SMS app, a social app's in-app browser, a QR scan, or a push opened from the lock screen. adb and Espresso skip all of those. To cover them:
- Build a link matrix. Cover each source (email, SMS, QR, push, social in-app browser, web page) against each app state (not installed, killed, backgrounded, already open), login state (logged in, logged out) and OS version.
- Run it on the devices your users have. Use your analytics to pick the makes, models and Android versions. Include at least one device with a vendor browser set as the default.
- Capture evidence. Record video of every run, plus device logs and network logs, so a failure shows exactly where the link broke: wrong screen, lost parameter, login wall, or a 404.
Testing an Android deep link end to end means opening it from where users actually tap it (an email, an SMS, a QR code or a push notification) on a real phone. Mobot testing robots do this on real iOS and Android devices, and a Mobot QA analyst verifies every defect with screenshots from the device and device logs before it reaches your team.
Mobot opens links from the real source app or message, including cold-start and backgrounded states, and scans QR codes with the device's real camera. See how Mobot can test push notifications and deep links on real devices, and what goes into verified defect reports.
To see Mobot test your own app on real devices, start a free trial.
What should a deep link test checklist include?
- The link opens the correct screen, not the home screen.
- Every parameter is parsed, including missing, empty, encoded and invalid values.
- The link works when the app is killed, backgrounded and already open.
- The link works when the app isn't installed (deferred deep link). Reinstall for each run.
- Logged-in and logged-out users each reach the right place, and logged-out users come back to the content after login.
- https links are verified (
pm get-app-linksshowsverified) on every domain and subdomain you use. - The web fallback works when the app isn't installed or the link isn't verified.
- Analytics and attribution parameters reach your analytics.
- The Back button goes somewhere sensible, not out of the app or into a loop.
- Links work from email, SMS, QR codes, push notifications and social in-app browsers.
For a longer list of failure modes, see our deep link testing guide and our roundup of deep link testing tools.
How do you test deep links on iOS?
On iOS, Universal Links are Apple's version of App Links. They depend on the apple-app-site-association file on your domain, the way App Links depend on assetlinks.json. See how to test Universal Links on iOS and deferred deep links on iOS.
FAQ
What is the adb command to open a deep link?
Run adb shell am start -W -a android.intent.action.VIEW -d "<uri>" <package>, for example adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/123" com.example.app. Look for Status: ok and check the screen. Leave off the package name to see which app Android picks on its own.
How do I open a deep link in the Android emulator?
Use the same adb command. If more than one emulator or device is attached, add the serial: adb -s emulator-5554 shell am start -W -a android.intent.action.VIEW -d "<uri>". You can also paste the link into a message or note inside the emulator and tap it, which is closer to what a user does.
Why does my Android deep link open the browser instead of the app?
Usually the https link isn't a verified App Link. On Android 12 and higher, unverified web links open in the default browser. Check with adb shell pm get-app-links <package>. Other causes: the host or path doesn't match your intent filter, the BROWSABLE category is missing, or the user chose a different default app.
How do I check if Android App Links are verified?
On Android 12 and higher, run adb shell pm get-app-links <package>. Each domain shows a state. verified means links open your app. none means verification hasn't finished, so wait and run adb shell pm verify-app-links --re-verify <package>. Other states mean it failed. Check the fingerprint, redirects and content type of assetlinks.json.
Can I test deep links without adb?
Yes. Espresso tests with ActivityScenario check that your activity handles the URI. A simple HTML page with your links, a QR code, or a real email or SMS lets you tap the link the way users do. Those manual checks are the only way to cover the source app, the chooser and App Links routing.
How do I test deferred deep links on Android?
Uninstall the app, tap the link, install the app, then open it and check that it lands on the linked content. Deferred deep links usually depend on an install-referrer or attribution SDK, so follow that SDK's test instructions. Because the first open after install is what matters, reinstall the app before every run.
Can Mobot test deep links on real Android devices?
Yes. Mobot testing robots open links on real iOS and Android devices the way a user does, from the real source app or message: email, SMS, push notifications, and QR codes scanned with the device's real camera. A Mobot QA analyst verifies every defect with screenshots from the device and device logs before it reaches your team.
Test the link where users tap it
Check the command, the verification and the handler in code first. Then test the real path, from the email, SMS, QR code or push, on the phones your users carry.
To see Mobot test your own app on real devices, start a free trial.
Want the full picture?
Start free trial