Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Chrome custom tabs fallback

I am implementing fallback chrome custom tabs

I was referring to few links it has some custom fallback implementation. I didn't get why it is required.

I did following for handling fallback and is working fine.

  try {
        CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
        builder.setToolbarColor(ContextCompat.getColor(context, R.color.appthemecolor));
        CustomTabsIntent customTabsIntent = builder.build();
        customTabsIntent.launchUrl(context, Uri.parse(url));
    } catch (ActivityNotFoundException e) {
        e.printStackTrace();
        Intent intent = new Intent(context, WebviewActivity.class);
        intent.putExtra(WebviewActivity.EXTRA_URL, url);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        context.startActivity(intent);
    }

Any idea why such complex implementation is required for handling fallback?

using following version of support librarycompile 'com.android.support:customtabs:25.3.1'

like image 737
amodkanthe Avatar asked May 09 '17 07:05

amodkanthe


1 Answers

If you see through the source code of CustomTabsIntent, it's nothing more than just a helper to create a normal implicit intent to open a URL using Intent.ACTION_VIEW. It helps you to add extra data to the intent with the specific keys recognized by Chrome which later be used by Chrome to present a customized UI.

Here is the explanation from the official page:

Custom Tabs uses an ACTION_VIEW Intent with key Extras to customize the UI. This means that by default the page willopen in the system browser, or the user's default browser.

If the user has Chrome installed and it is the default browser, it will automatically pick up the EXTRAS and present a customized UI. It is also possible for another browser to use the Intent extras to provide a similar customized interface.

For the solution on your link, the source code taken from here. As you can see from CustomTabActivityHelper#openCustomTab, first it will look for the app that support Custom Tabs. If it is available, then start the implicit intent described by CustomTabsIntent. If not, open the WebViewActivity.

How to find out if any app support Custom Tabs? You can se it on CustomTabsHelper.getPackageNameToUse. At first, it will resolve all apps that can open a URL using Intent.ACTION_VIEW. Then, it will check if that apps support Custom Tabs.

Then,

  • If no apps available, return null
  • If only 1 app available, return it.
  • If more than 1 app available and 1 of it is the default app, return it.
  • If more than 1 app available and 1 of it is Chrome, return it.
  • Else, return null
  • (If more than 1 app available, you can put a logic to ask a user to choose any browser they want)

Now, how about your solution?

If we use your solution, WebviewActivity will be opened if no apps can handle implicit intent created by CustomTabsIntent, in this case no browser installed? What happened if we have browsers and none of it support Custom Tabs? Your app will still ask to open the link in the browser and not in WebViewActivity.

Remember, CustomTabsIntent is just a helper to create a normal implicit intent to open a URL using Intent.ACTION_VIEW with various EXTRA data to customized the UI. How to customized the UI is handled by the browsers. Basically, I think we can create and start the intent to open the browser with customized UI by yourself without CustomTabsIntent. I never try this one though.

If you want the link to be opened in any browser, no matter the browser support Custom Tabs or not, and the link to be opened in WebViewActivity if no apps available, your solution solve it even though it's not the best solution I guess.

But, if you want the link to be opened in the browser that support Custom Tabs, and the link to be opened in WebViewActivity if no apps that support Custom Tabs available, his solution is the right one.

But, if what you want is just providing fallback mechanism, it should not be that complicated. Here is the simpler code:

public class CustomTabs {

    private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";

    private static final String STABLE_PACKAGE = "com.android.chrome";
    private static final String BETA_PACKAGE = "com.chrome.beta";
    private static final String DEV_PACKAGE = "com.chrome.dev";
    private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";

    public static void openTab(Context context, String url) {
        CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();

        /* do some UI customization here */

        CustomTabsIntent customTabsIntent = builder.build();

        String packageName = getPackageNameToUse(context);

        if (packageName == null) {
            Intent intent = new Intent(context, WebviewActivity.class);
            intent.putExtra(WebviewActivity.EXTRA_URL, url);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);

            context.startActivity(intent);
        } else {
            customTabsIntent.intent.setPackage(packageName);
            customTabsIntent.launchUrl(context, Uri.parse(url));
        }
    }

    private static String getPackageNameToUse(Context context) {
        String packageNameToUse = null;

        PackageManager pm = context.getPackageManager();

        Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));

        ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);

        String defaultViewHandlerPackageName = null;
        if (defaultViewHandlerInfo != null) {
            defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
        }

        List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);

        List<String> packagesSupportingCustomTabs = new ArrayList<>();
        for (ResolveInfo info : resolvedActivityList) {
            Intent serviceIntent = new Intent();
            serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
            serviceIntent.setPackage(info.activityInfo.packageName);
            if (pm.resolveService(serviceIntent, 0) != null) {
                packagesSupportingCustomTabs.add(info.activityInfo.packageName);
            }
        }

        if (packagesSupportingCustomTabs.isEmpty()) {
            packageNameToUse = null;
        } else if (packagesSupportingCustomTabs.size() == 1) {
            packageNameToUse = packagesSupportingCustomTabs.get(0);
        } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
                && !hasSpecializedHandlerIntents(context, activityIntent)
                && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
            packageNameToUse = defaultViewHandlerPackageName;
        } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
            packageNameToUse = STABLE_PACKAGE;
        } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
            packageNameToUse = BETA_PACKAGE;
        } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
            packageNameToUse = DEV_PACKAGE;
        } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
            packageNameToUse = LOCAL_PACKAGE;
        }
        return packageNameToUse;
    }

    private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
        try {
            PackageManager pm = context.getPackageManager();
            List<ResolveInfo> handlers = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);

            if (handlers == null || handlers.size() == 0) {
                return false;
            }

            for (ResolveInfo resolveInfo : handlers) {
                IntentFilter filter = resolveInfo.filter;
                if (filter == null) continue;
                if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
                if (resolveInfo.activityInfo == null) continue;
                return true;
            }

        } catch (RuntimeException e) {
            Log.e("LOG", "Runtime exception while getting specialized handlers");
        }

        return false;
    }
}
like image 113
marcelljee Avatar answered Oct 13 '22 19:10

marcelljee