Deep Blue Links
Documentation menu

Android

Open your app from its links with verified App Links, resolve each link with the API, and read the install referrer to find the link that led to an install.

This guide uses Kotlin. Replace demo with your app's name and YOUR_API_KEY with its key: the sandbox key in debug builds and the live key in release builds.

1. Add the intent filter

In AndroidManifest.xml, allow the app to reach the API, and add an intent filter with autoVerify to the activity that handles links:

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />

<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTask">
    <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="demo.dev.thedbl.ink" />
    </intent-filter>
</activity>

singleTask hands a link that opens while the app runs to the same activity, through onNewIntent; without it, Android starts a second copy of the activity. Add a <data> element for each custom domain too. Android verifies the hosts against the assetlinks.json Deep Blue Links serves, so your app's package name and the SHA-256 fingerprint of every key that signs it must be in your app's settings. With Play App Signing, that includes the app signing key from the Play Console.

If your app has a URL scheme, add a second filter without autoVerify and with <data android:scheme="myapp" />.

The link arrives as the intent's data, in onCreate when the app starts and in onNewIntent while it runs:

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        handleLink(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        handleLink(intent)
    }

    private fun handleLink(intent: Intent?) {
        val uri = intent?.takeIf { it.action == Intent.ACTION_VIEW }?.data ?: return
        if (uri.scheme == "myapp") {
            router.open("/" + uri.host + (uri.path ?: ""), uri.query)
            return
        }
        lifecycleScope.launch {
            val link = try {
                DeepBlueLinks.resolve(uri.toString())
            } catch (e: IOException) {
                Log.w("DeepBlueLinks", "Couldn't resolve $uri", e)
                null
            }
            val destination = link?.let { Uri.parse(it.longUrl) } ?: uri
            router.open(destination.path ?: "/", destination.query)
        }
    }
}

A scheme URL such as myapp://products/1234 isn't a short link, so it's routed directly: its host is the first path segment. router stands for your app's own navigation. setIntent keeps the latest link as the activity's intent, so a screen rotation doesn't bring back an older one.

POST /api/links/resolve answers 404 for a URL that isn't one of your short links, such as a link's full URL, so the code routes by the URL itself then. It does the same when the call fails: the client reports no network and any other status, such as a 401 for a wrong key, as an IOException, so a failed call never crashes the app. This client needs no library beyond the platform:

DeepBlueLinks.kt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL

data class ResolvedLink(val id: Int, val longUrl: String, val shortUrl: String, val isSandbox: Boolean)

object DeepBlueLinks {
    private const val API_KEY = "YOUR_API_KEY"

    suspend fun resolve(url: String): ResolvedLink? =
        post("https://dev.thedbl.ink/api/links/resolve", JSONObject().put("url", url))

    suspend fun post(endpoint: String, body: JSONObject): ResolvedLink? = withContext(Dispatchers.IO) {
        val connection = URL(endpoint).openConnection() as HttpURLConnection
        try {
            connection.requestMethod = "POST"
            connection.doOutput = true
            connection.setRequestProperty("Api-Key", API_KEY)
            connection.setRequestProperty("Content-Type", "application/json")
            connection.outputStream.use { it.write(body.toString().toByteArray()) }
            when (connection.responseCode) {
                200 -> JSONObject(connection.inputStream.bufferedReader().readText()).let {
                    ResolvedLink(it.getInt("id"), it.getString("longUrl"), it.getString("shortUrl"), it.optBoolean("isSandbox"))
                }
                404 -> null
                else -> throw IOException("Deep Blue Links answered ${connection.responseCode}")
            }
        } finally {
            connection.disconnect()
        }
    }
}

When link.isSandbox is true, load the destination's content from your sandbox backend.

Google Play hands your app the short link through the install referrer. Add the Play Install Referrer library:

build.gradle
dependencies {
    implementation "com.android.installreferrer:installreferrer:2.2"
}

On the first launch, read the referrer, take its link value and resolve it:

InstallReferrer.kt
import android.content.Context
import android.util.Log
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
import java.net.URLDecoder

fun readReferredLink(context: Context, onResult: (String?) -> Unit) {
    val client = InstallReferrerClient.newBuilder(context).build()
    client.startConnection(object : InstallReferrerStateListener {
        override fun onInstallReferrerSetupFinished(responseCode: Int) {
            val link = try {
                if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
                    linkFromReferrer(client.installReferrer.installReferrer)
                } else null
            } catch (e: Exception) {
                Log.w("DeepBlueLinks", "Couldn't read the install referrer", e)
                null
            } finally {
                client.endConnection()
            }
            onResult(link)
        }

        override fun onInstallReferrerServiceDisconnected() {}
    })
}

fun linkFromReferrer(referrer: String?): String? =
    referrer?.split('&')
        ?.map { it.split('=', limit = 2) }
        ?.firstOrNull { it.first() == "link" }
        ?.getOrNull(1)
        ?.let { URLDecoder.decode(it, "UTF-8") }
val prefs = getSharedPreferences("deep_blue_links", MODE_PRIVATE)
if (!prefs.getBoolean("checked_referrer", false)) {
    prefs.edit().putBoolean("checked_referrer", true).apply()
    readReferredLink(this) { link ->
        if (link != null) lifecycleScope.launch {
            val resolved = try {
                DeepBlueLinks.resolve(link)
            } catch (e: IOException) {
                Log.w("DeepBlueLinks", "Couldn't resolve $link", e)
                null
            }
            resolved?.let {
                val destination = Uri.parse(it.longUrl)
                router.open(destination.path ?: "/", destination.query)
            }
        }
    }
}

A failure to read the referrer counts as no link. The referrer stays the same on every launch, so the flag keeps the app from routing to it again. Installs that didn't come from a Deep Blue Links page have no link value.

Keep the flag out of Android's Auto Backup, or a reinstall restores it and the new install's referrer is never read. Exclude the preferences file in both backup rule files your manifest points to (android:fullBackupContent and android:dataExtractionRules):

res/xml/backup_rules.xml
<full-backup-content>
    <exclude domain="sharedpref" path="deep_blue_links.xml" />
</full-backup-content>
res/xml/data_extraction_rules.xml
<data-extraction-rules>
    <cloud-backup>
        <exclude domain="sharedpref" path="deep_blue_links.xml" />
    </cloud-backup>
</data-extraction-rules>

The match is exact, so an Android app that reads the referrer doesn't need check-deferred. An app that can't read it, such as one installed from another store, can still call check-deferred without device.

Test it

  • Install a build signed with a key whose fingerprint is in your app's settings, then check the verification:

    adb shell pm get-app-links com.example.demo

    The host should show verified. After changing your settings, adb shell pm verify-app-links --re-verify com.example.demo checks again.

  • Open a short link from another app, such as a chat, or with adb shell am start -a android.intent.action.VIEW -d "https://demo.dev.thedbl.ink/Xy3kP9aQ".

  • To test the referrer, the app must be installed from Google Play, for example from an internal testing track, through a link's preview page.