Android App Links Guide

Complete step-by-step guide to implement, configure, and test Android App Links in your mobile application

1. Overview & Prerequisites

What are Android App Links?

Android App Links are HTTP URLs that bring users directly to specific content in your Android app. They provide a seamless user experience by opening your app instead of a web browser when users click on links.

Types of Deep Links

Deep Links

Basic URI schemes that can open your app

myapp://product/123
Web Links

HTTP/HTTPS URLs that show intent chooser

https://example.com/product/123
Android App Links

Verified HTTPS URLs that open directly

https://verified.com/product/123
Key Benefits
  • • Direct app opening from web links
  • • Better user engagement and retention
  • • Seamless content sharing between web and app
  • • Enhanced SEO and discoverability
  • • No app chooser dialog for verified domains

Prerequisites

Development
  • • Android Studio
  • • Target SDK 23+
  • • Kotlin or Java knowledge
  • • ADB access for testing
What We Provide
  • • Managed domain and hosting for verification files
  • • Automatic HTTPS with valid SSL certificates
  • • Auto-generated and hosted /.well-known/assetlinks.json
  • • Seamless Android domain verification support

2. Configure Intent Filters

Add Intent Filters to AndroidManifest.xml

Configure your app to handle specific URL patterns by adding intent filters to your target activity.

AndroidManifest.xml
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTop">
    
    <!-- Standard activity intent filter -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    
    <!-- App Links intent filter -->
    <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:scheme="https"
              android:host="yourdomain.com" />
    </intent-filter>
    
    <!-- Multiple path patterns example -->
    <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:scheme="https"
              android:host="yourdomain.com"
              android:pathPrefix="/product" />
    </intent-filter>
</activity>

Intent Filter Parameters Explained

Required Elements

  • android:autoVerify="true" - Enables automatic verification
  • ACTION_VIEW - Standard action for viewing content
  • BROWSABLE - Allows access from browser
  • DEFAULT - Default category for implicit intents

Data Attributes

  • android:scheme - URL scheme (https/http)
  • android:host - Domain name
  • android:pathPrefix - URL path pattern
  • android:path - Exact path match
  • android:pathPattern - Path with wildcards (* and .)
  • android:port - Specific port number

4. Handle Links in Activity

Basic Link Handling (Kotlin)

MainActivity.kt
class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        handleAppLink(intent)
    }
    
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        handleAppLink(intent)
    }
    
    private fun handleAppLink(intent: Intent) {
        val appLinkAction = intent.action
        val appLinkData: Uri? = intent.data
        
        if (Intent.ACTION_VIEW == appLinkAction && appLinkData != null) {
            handleIncomingLink(appLinkData)
        }
    }
    
    private fun handleIncomingLink(uri: Uri) {
        // Extract path and parameters
        val path = uri.path
        val queryParams = uri.queryParameterNames
        
        when {
            path?.startsWith("/product") == true -> {
                val productId = uri.getQueryParameter("id")
                openProduct(productId)
            }
            path?.startsWith("/dashboard") == true -> {
                openDashboard()
            }
            path?.startsWith("/profile") == true -> {
                val userId = uri.getQueryParameter("user")
                openProfile(userId)
            }
            else -> {
                // Handle unknown path - maybe show home screen
                showHome()
            }
        }
    }
    
    private fun openProduct(productId: String?) {
        productId?.let {
            // Navigate to product screen
            val intent = Intent(this, ProductActivity::class.java)
            intent.putExtra("product_id", it)
            startActivity(intent)
        }
    }
    
    private fun openDashboard() {
        // Navigate to dashboard
        val intent = Intent(this, DashboardActivity::class.java)
        startActivity(intent)
    }
    
    private fun openProfile(userId: String?) {
        userId?.let {
            // Navigate to profile screen
            val intent = Intent(this, ProfileActivity::class.java)
            intent.putExtra("user_id", it)
            startActivity(intent)
        }
    }
    
    private fun showHome() {
        // Show default home content
        // Already in MainActivity, just ensure we're showing home fragment
    }
}

Advanced Routing with Navigation Component

With Jetpack Navigation
class MainActivity : AppCompatActivity() {
    private lateinit var navController: NavController
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController
        
        handleAppLink(intent)
    }
    
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        handleAppLink(intent)
    }
    
    private fun handleAppLink(intent: Intent) {
        val uri = intent.data
        uri?.let {
            when (it.path) {
                "/product" -> {
                    val productId = it.getQueryParameter("id")
                    val bundle = Bundle().apply {
                        putString("productId", productId)
                    }
                    navController.navigate(R.id.productFragment, bundle)
                }
                "/dashboard" -> {
                    navController.navigate(R.id.dashboardFragment)
                }
                "/profile" -> {
                    val userId = it.getQueryParameter("user")
                    val bundle = Bundle().apply {
                        putString("userId", userId)
                    }
                    navController.navigate(R.id.profileFragment, bundle)
                }
            }
        }
    }
}

🔒 Security Considerations

Input Validation

Critical

Always validate and sanitize incoming URL data to prevent security vulnerabilities.

private fun handleIncomingLink(uri: Uri) {
    val path = uri.path
    val productId = uri.getQueryParameter("id")
    
    // ❌ Bad: Direct usage without validation
    // openProduct(productId)
    
    // ✅ Good: Validate input first
    if (path != null && isValidPath(path)) {
        productId?.let { id ->
            if (isValidProductId(id)) {
                openProduct(id)
            } else {
                Log.w("AppLinks", "Invalid product ID: $id")
                showHome()
            }
        }
    } else {
        Log.w("AppLinks", "Invalid or null path: $path")
        showHome()
    }
}

private fun isValidProductId(id: String): Boolean {
    return id.matches(Regex("^[a-zA-Z0-9_-]{1,50}$"))
}

private fun isValidPath(path: String): Boolean {
    val allowedPaths = listOf("/product", "/dashboard", "/profile")
    return allowedPaths.any { path.startsWith(it) }
}

Authentication & Authorization

Important
  • • Always check user authentication before opening sensitive content
  • • Validate user permissions for accessed resources
  • • Don't expose sensitive data through URL parameters
  • • Use secure tokens for authenticated deep links
  • • Implement rate limiting for link processing

Best Security Practices

HTTPS Only

Always use HTTPS for App Links to prevent man-in-the-middle attacks and ensure data integrity.

Certificate Pinning

Consider implementing certificate pinning for critical domains to prevent certificate-based attacks.

Minimal Permissions

Only request necessary permissions and avoid exposing sensitive app functionality through deep links.

Error Handling

Implement proper error handling and logging without exposing sensitive information to attackers.

6. Testing & Debugging

ADB Testing Commands

Test App Link Opening

adb shell am start \
    -W -a android.intent.action.VIEW \
    -d "https://yourdomain.com/product?id=123" \
    com.yourcompany.yourapp

Check Domain Verification Status

adb shell pm get-app-links com.yourcompany.yourapp

Reset Domain Verification

adb shell pm set-app-links --package com.yourcompany.yourapp 0 yourdomain.com

Verification Status Codes

  • verified - Domain verification successful
  • none - No verification attempted
  • ask - User needs to approve
  • denied - User denied permission
  • always_ask - Always show chooser
  • legacy_failure - Verification failed

Debugging Tips

  • • Use Android Studio's logcat to monitor intent data
  • • Test with both debug and release builds
  • • Verify assetlinks.json is accessible in browser
  • • Check network connectivity during verification
  • • Use Google's Digital Asset Links API to validate your file

7. Common Issues & Solutions

❌ App doesn't open when clicking links

  • • Check if assetlinks.json is properly hosted and accessible
  • • Verify SHA256 fingerprint matches your app's signing certificate
  • • Ensure android:autoVerify="true" is set in intent filters
  • • Test domain verification status with ADB commands

⚠️ Verification fails

  • • Verify assetlinks.json returns Content-Type: application/json
  • • Check for any redirects - they're not allowed
  • • Ensure file is served over HTTPS
  • • Validate JSON syntax with online tools

ℹ️ Browser chooser still appears

  • • Domain verification may be in progress (can take up to 20 seconds)
  • • Clear app defaults in Settings → Apps → Your App → Open by default
  • • Reset verification status and wait for re-verification
  • • Check if other apps also handle the same domain

✅ Best Practices

  • • Always test on physical devices, not just emulators
  • • Include both debug and release certificate fingerprints during development
  • • Set launchMode="singleTop" to handle new intents properly
  • • Implement proper fallback handling for unrecognized URLs
  • • Use HTTPS for all domain URLs

📱 Example Android App

Want to see a working implementation? Check out our complete Android App Links example application with full source code.

Android App Links Example

Complete working Android application demonstrating both custom scheme deep links and Android App Links implementation.

Custom scheme deep links (example://link/{id})
Android App Links (https://example.deeptap.io/{path})
Complete assetlinks.json configuration
ADB testing commands and troubleshooting
View on GitHubKotlin • Open Source
Quick Start

1. Clone the repository: git clone https://github.com/TedyHub/android-app-links-example.git

2. Open in Android Studio and run: ./gradlew installDebug

3. Test with ADB: adb shell am start -a android.intent.action.VIEW -d "example://link/123"

🎉 You're All Set!

Your Android app should now handle deep links seamlessly. Test thoroughly across different devices and scenarios.