Annual Defect Report 2026Download
Blog & Reports
How-to guideMay 15, 2022 · 14 min read · Team Mobot

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.

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 http or https. 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.json on 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.

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.

  1. Install your debug build and connect a device. Run adb devices and check that the device shows the state device, not offline.
  2. Find the package name. Run adb shell pm list packages <part-of-name>. For example, adb shell pm list packages example.
  3. Send the intent. Run adb shell am start -W -a android.intent.action.VIEW -d "<uri>" <package>.
  4. Check the output and the screen. Look for Status: ok and the Activity: line that names the activity that received the intent. Then check that the right screen and content appeared.
  5. 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.
  6. Read the logs. Run adb logcat in 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 from adb devices, for example emulator-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 get Status, Activity and WaitTime.
  • -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 the BROWSABLE category 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.com and www.example.com are different hosts.
  • The filter is missing the BROWSABLE or DEFAULT category. Without BROWSABLE, links tapped in a browser can't reach your app. Without DEFAULT, 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"

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_failure or a number of 1024 or higher: verification failed. Check your assetlinks.json file.

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.

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>")

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.

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.

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 start with 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.

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:

  1. 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.
  2. 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.
  3. 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.

  • 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-links shows verified) 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.

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

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.

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.

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.

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.

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.

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.

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.

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
How-to

See What Your Emulators Are Missing

Get a real, verified defect report from Mobot’s robots and QA analysts — on your app, on real devices.

or talk to sales