Retrieving the URL from Custom Tabs in Android

Written by im-joe-king | Published 2020/07/31
Tech Story Tags: tutorial | android | retrieve-url-from-custom-tabs | chrome-custom-tabs | latest-tech-stories | android-app-development | android-app-tutorials | android-apps | web-monetization

TLDR Chrome Custom Tabs is the preferred option if you want to give users access to the web without sending them to another app. The biggest inconvenience that I’ve discovered is that the Custom Tab’s current URL is not sent back to the host application and retrieving it is not the most intuitive process. You can be up to running with a few lines of code. Retrieving the URL is possible with PendingIntent that starts our main activity. You can use a CustomTabs callback to determine when pages start and finish loading but the callback does not contain information about the actual URL.via the TL;DR App

In my previous article I provided an overview of Custom Tabs, or Chrome Custom Tabs, in Android. In short, this component allows your app to seamlessly transition between application and web content and it’s used by several well known applications including Twitter, The Gaurdian, Tumblr, Stack Overflow, and Feedly. They’re safe, simple to use, and their ability to preemptively load URLS makes them incredibly fast compared to alternatives like WebViews. With this said, using Custom Tabs is the preferred option if you want to give users access to the web without sending them to another app. You can be up and running with a few lines of code.
This all sounds pretty convenient and depending on how you plan to use the browser, it might be. The biggest inconvenience that I’ve discovered is that the Custom Tab’s current URL is not sent back to the host application and retrieving it is not the most intuitive process. Apparently this behavior was put in place to safegaurd the user’s privacy and that makes sense…but what if you want to implement a custom share button? Or you want to trigger an action when the user navigates to a specific website? Not all of the use cases for URL tracking are immediately obvious but a lot of people have inquired about this:
It turns out this is possible!

Intents are the Solution

Before I get too ahead of myself, I’ll admit that I haven’t figured out a way to passively track the user’s URL. You can use a CustomTabsCallback to determine when pages start and finish loading but the callback does not contain any information about the actual URL. Nonetheless, if you can get the user to click on an action button or menu option, the URL is yours for the taking!
Using PendingIntent.getActivity
This section of the Chrome Custom Tabs article explains that it’s possible to get the URL from a PendingIntent by calling Intent.getDataString() when the intent is received. We can do that with a PendingIntent that starts our main activity.
Create an action button and add it to your CustomTabsIntent builder:
      val sendLinkIntent = Intent(main,MainActivity::class.java)
        sendLinkIntent.putExtra(Intent.EXTRA_SUBJECT,"This is the link you were exploring")
        val pendingIntent = PendingIntent.getActivity(main,0,sendLinkIntent,PendingIntent.FLAG_UPDATE_CURRENT)
        // Set the action button
        AppCompatResources.getDrawable(main, R.drawable.close_icon)?.let {
            DrawableCompat.setTint(it, Color.WHITE)
            builder.setActionButton(it.toBitmap(),"This is your link",pendingIntent,false)
        }
        // Create the Custom Tabs Intent and launch the URL
        val customTabsIntent: CustomTabsIntent = builder.build()
        customTabsIntent.launchUrl(main, Uri.parse(url))
Then handle the intent when it comes back to your activity. I placed this code in the onCreate() method of my main activity:

// Handle any possible intents **************************************************************************
        // The URL is stored in the intent's data
        val data: Uri? = intent?.data

        // Figure out what to do based on the intent type
        if (intent?.type?.startsWith("image/") == true) {
            // Handle intents with image data ...
            Log.d("Intent",intent?.type.toString())
        } else if (intent?.type == "text/plain") {
            // Handle intents with text ...
            Log.d("Intent.text",intent.extras?.getString("android.intent.extra.TEXT").toString())
        }else{
            if (data != null) {
                Log.d("Intent URL",data.toString())
            }
        }
Demonstration from my app Rabbit Hole
As you can see in this video, the build above will send the user back to the main activity along with the URL that they were last viewing. This could work well if you want to share the link or save it for later.
My issue with this technique is that the redirection may come as an unwanted interruption to the user’s casual browsing. Why shouldn’t the user be able to save a link and continue their adventure across the web?
Using PendingIntent.getBroadcast()
Grabbing the current URL and remaining in the Custom Tab browser can be accomplished with a BroadcastReceiver. If you don’t have much experience with these, check out this guide before continuing on. Otherwise, let’s begin.
Extend the BroadcastReceiver class and place your code to handle the incoming intent inside the onRecieve() method:

class DigBroadcastReceiver() : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val uri: Uri? = intent.data
        if (uri != null) {
            Log.d("Broadcast URL",uri.toString())
            var toast = Toast.makeText(context, uri.toString(), Toast.LENGTH_SHORT)
            val view = toast.view
            view.background.setColorFilter(ContextCompat.getColor(context,R.color.red), PorterDuff.Mode.SRC_IN)
            val text = view.findViewById(android.R.id.message) as TextView
            text.setTextColor(ContextCompat.getColor(context, R.color.common_google_signin_btn_text_dark))
            text.textAlignment = View.TEXT_ALIGNMENT_CENTER
            toast.setGravity(Gravity.BOTTOM,0,200)
            toast.show()
        }
    }
}
List the new BroadcastReceiver in your manifest file. This gets nested one layer under the<application> tag and the “name” attribute is the file name including all folders after your package name. Here’s my project structure:
And here’s the receiver in the manifest file:
<application>
  <receiver
    android:name=".ui.dig.DigBroadcastReceiver"
    android:enabled="true" />
</application>
Once you have that completed, create the PendingIntent that will send out the broadcast and add the associated action button to your CustomTabsIntent.Builder:

        val sendLinkIntent = Intent(main,DigBroadcastReceiver()::class.java)
        sendLinkIntent.putExtra(Intent.EXTRA_SUBJECT,"This is the link you were exploring")
        val pendingIntent = PendingIntent.getBroadcast(main,0,sendLinkIntent,PendingIntent.FLAG_UPDATE_CURRENT)
        // Set the action button
        AppCompatResources.getDrawable(main, R.drawable.close_icon)?.let {
            DrawableCompat.setTint(it, Color.WHITE)
            builder.setActionButton(it.toBitmap(),"Add this link to your dig",pendingIntent,false)
        }
        val customTabsIntent: CustomTabsIntent = builder.build()
        customTabsIntent.launchUrl(main, Uri.parse(url))
Now when you press the action button within the Custom Tab, the current URL is retrieved and displayed in a Toast. This is much less disturbing to the overall user experience but quite frankly, not too useful to us as the developer. Since the Broadcast Receiver we created doesn’t know about the main activity, everything that happens within the receiver is isolated (for instance, all of the code to generate and customize the Toast is in the Broadcast Receiver class above).
If we want to actually utilize the URL data, we can make the BroadcastReceiver aware of the MainActivity by nesting the new class inside a fragment or activity. The <receiver> tag in the manifest file will need to be updated accordingly (see below). I’ve provided both situations since some people may want to arrange their classes differently.

       <receiver
    android:name=".ui.dig.DigTabs$DigBroadcastReceiver"
    android:enabled="true" />

In Conclusion

Extracting the current URL from an intent sent by the Custom Tabs turns out to be pretty straightforward if you don’t mind making users click a toolbar button. There still doesn’t seem to be a way to passively retrieve this data but the yet-unused extras argument in the CustomTabsCallback class may change that. Until the, an extra click here and there will have to suffice.
The next topic I’ll be looking into related to Custom Tabs is whether or not it’s possible to suppress Intents from being fulfilled by third party apps. In my situation, navigating between normal web pages works fine as you can see above. But when I navigate to a web page that I have set to default open in another app (like a *.wikipedia.org page set to default open in the Wikipedia app), I leave Custom Tabs, never to return.
I’ll do some investigating and update you soon!
Check out the full series on Custom Tabs:

Published by HackerNoon on 2020/07/31