Using Custom Tabs with Android 11

Android 11 introduced changes on how apps can interact with other apps that the user has installed on the device. You can read more about those changes on Android documentation.

When an Android app using Custom Tabs targets SDK level 30 or above some changes may be necessary. This article goes over the changes that may be needed for those apps.

In the simplest case, Custom Tabs can be launched with a one-liner like so:

new CustomTabsIntent.Builder().build()
        .launchUrl(this, Uri.parse("https://www.example.com"));

Applications launching applications using this approach, or even adding UI customizations like changing the toolbar color, adding an action button won’t need to do any changes in the application.

Preferring Native Apps

But, if you followed the best practices some changes may be required.

The first relevant best practice is that applications should prefer a native app to handle the intent instead of a Custom Tab if an app that is capable of handling it is installed.

On Android 11 and above

Android 11 introduces a new Intent flag, FLAG_ACTIVITY_REQUIRE_NON_BROWSER, which is the recommended way to try opening a native app, as it doesn’t require the app to declare any package manager queries.

static boolean launchNativeApi30(Context context, Uri uri) {
    Intent nativeAppIntent = new Intent(Intent.ACTION_VIEW, uri)
            .addCategory(Intent.CATEGORY_BROWSABLE)
            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                    Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER);
    try {
        context.startActivity(nativeAppIntent);
        return true;
    } catch (ActivityNotFoundException ex) {
        return false;
    }
}

The solution is to try to launch the Intent and use FLAG_ACTIVITY_REQUIRE_NON_BROWSER to ask Android to avoid browsers when launching.

If a native app that is capable of handling this Intent is not found, an ActivityNotFoundException will be thrown.

Before Android 11

Even though the application may target Android 11, or API level 30, previous Android versions will not understand the FLAG_ACTIVITY_REQUIRE_NON_BROWSER flag, so we need to resort to querying the Package Manager in those cases:

private static boolean launchNativeBeforeApi30(Context context, Uri uri) {
    PackageManager pm = context.getPackageManager();

    // Get all Apps that resolve a generic url
    Intent browserActivityIntent = new Intent()
            .setAction(Intent.ACTION_VIEW)
            .addCategory(Intent.CATEGORY_BROWSABLE)
            .setData(Uri.fromParts("http", "", null));
    Set<String> genericResolvedList = extractPackageNames(
            pm.queryIntentActivities(browserActivityIntent, 0));

    // Get all apps that resolve the specific Url
    Intent specializedActivityIntent = new Intent(Intent.ACTION_VIEW, uri)
            .addCategory(Intent.CATEGORY_BROWSABLE);
    Set<String> resolvedSpecializedList = extractPackageNames(
            pm.queryIntentActivities(specializedActivityIntent, 0));

    // Keep only the Urls that resolve the specific, but not the generic
    // urls.
    resolvedSpecializedList.removeAll(genericResolvedList);

    // If the list is empty, no native app handlers were found.
    if (resolvedSpecializedList.isEmpty()) {
        return false;
    }

    // We found native handlers. Launch the Intent.
    specializedActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(specializedActivityIntent);
    return true;
}

The approach used here is to query the Package Manager for applications that support a generic http intent. Those applications are likely browsers.

Then, query for applications that handle itents for the specific URL we want to launch. This will return both browsers and native applications setup to handle that URL.

Now, remove all browsers found on the first list from the second list, and we’ll be left only with native apps.

If the list is empty, we know there are no native handlers and return false. Otherwise, we launch the intent for the native handler.

Putting it all together

We need to ensure using the right method for each occasion:

static void launchUri(Context context, Uri uri) {
    boolean launched = Build.VERSION.SDK_INT >= 30 ?
            launchNativeApi30(context, uri) :
            launchNativeBeforeApi30(context, uri);

    if (!launched) {
        new CustomTabsIntent.Builder()
                .build()
                .launchUrl(context, uri);
    }
}

Build.VERSION.SDK_INT provides the information we need. If it’s equal or larger than 30, Android knows the FLAG_ACTIVITY_REQUIRE_NON_BROWSER and we can try launching a nativa app with the new approach. Otherwise, we try launching with the old approach.

If launching a native app fails, we then launch a Custom Tabs.

There’s some boilerplate involved in this best practice. We’re working on making this simpler by encapsulating the complexity in a library. Stay tuned for updates to the android-browser-helper support library.

Detecting browsers that support Custom Tabs

Another common pattern is to use the PackageManager to detect which browsers support Custom Tabs on the device. Common use-cases for this are setting the package on the Intent to avoid the app disambiguation dialog or choosing which browser to connect to when connecting to the Custom Tabs service.

When targeting API level 30, developers will need to add a queries section to their Android Manifest, declaring an intent-filter that matches browsers with Custom Tabs support.

<queries>
    <intent>
        <action android:name=
            "android.support.customtabs.action.CustomTabsService" />
    </intent>
</queries>

With the markup in place, the existing code used to query for browsers that support Custom Tabs will work as expected.

Frequently Asked Questions

Q: The code that looks for Custom Tabs providers queries for applications that can handle https:// intents, but the query filter only declares an android.support.customtabs.action.CustomTabsService query. Shouldn’t a query for https:// intents be declared?

A: When declaring a query filter, it will filter the responses to a query to the PackageManager, not the query itself. Since browsers that support Custom Tabs declare handling the CustomTabsService, they won’t be filtered out. Browsers that don’t support Custom Tabs will be filtered out.

Conclusion

Those are all the changes required to adapt an existing Custom Tabs integration to work with Android 11. To learn more about integrating Custom Tabs into an Android app, start with the implementation guide then check out the best practices to learn about building a first-class integration.

Let us know if you have any questions or feedback!