Skip to content

Instantly share code, notes, and snippets.

@ingyesid
Last active September 6, 2015 02:30
Show Gist options
  • Save ingyesid/1bc6b145657b0bf29e21 to your computer and use it in GitHub Desktop.
Save ingyesid/1bc6b145657b0bf29e21 to your computer and use it in GitHub Desktop.
Utils for Android's CustomTabs Support Library
/*
* Copyright 2015 Diego Rossi (@_HellPie)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.hellpie.generic.utils.customtabs;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsCallback;
import android.support.customtabs.CustomTabsClient;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsService;
import android.support.customtabs.CustomTabsServiceConnection;
import android.support.customtabs.CustomTabsSession;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class ChromeTabsUtils {
/**
* All the possible Google Chrome packages known to date (4-Sep-2015)
*/
private static final String CHROME_STABLE = "com.android.chrome";
private static final String CHROME_BETA = "com.android.chrome.beta";
private static final String CHROME_DEV = "com.android.chrome.dev";
private static final String CHROME_GOOGLE = "com.google.android.apps.chrome";
private static final String _FALLBACK = "-1";
/**
* Used as argument in warmup() for CustomTabs, forces asynchronous warmup mode
*/
private static final long WARMUP = 0l;
/**
* A static reference. Saved the first time the class is created non-statically
*/
private static ChromeTabsUtils CTU_THIS = null;
private CustomTabsClient CLIENT;
private CustomTabsSession SESSION;
private CustomTabsCallback CALLBACK = new CallbackHandler();
private CustomTabsServiceConnection CONNECTION = new ConnectionHandler();
private List<CustomTabsCallback> CALLBACKS = new ArrayList<>();
private String BROWSER = null;
private CustomTabsFallback FALLBACK = new WebViewFallback();
/**
* Creates a new ChromeTabUtils object
*
* @param context Needed to use PackageManager
*/
public ChromeTabsUtils(Context context) {
// Find a browser to use that supports CustomTabs APIs if non null context
if(context != null) findBrowser(context);
// Only create a new object if nobody already did it
if(CTU_THIS == null) CTU_THIS = this;
}
/**
* Prepares the CustomTabs client to a possible open() request on a given link.
* Expects the CustomTabs client to pre-resolve DNS of main domain and potential resources
* pre-connecting to the destination including HTTPS/TSL negotiation.
*
* For more informations visit the official CustomTabs documentation at:
* https://developer.chrome.com/multidevice/android/customtabs
*
* @param context Needed to bind the application to the CustomTabs service
* @param URL The URL that may be opened in the near future
* @return Returns itself
*/
public ChromeTabsUtils prepare(Context context, String URL) {
// Break instantly if no package is compatible
if(BROWSER.equals(_FALLBACK) || !isValidBrowser(context, BROWSER)) return this;
// Setup a new session if one wasn't created yet
if(CLIENT == null || SESSION == null) {
CustomTabsClient.bindCustomTabsService(context, BROWSER, CONNECTION);
}
// Transform the URL into a valid URL, no spaces (use literal %20 instead), http(s) protocol
if(URL.contains(" ")) URL = URL.replaceAll(" ", "");
if(!URL.startsWith("http://") || URL.startsWith("https://")) URL = "http://" + URL;
// Warn the client a new link may be clicked
SESSION.mayLaunchUrl(Uri.parse(URL), null, null);
return this;
}
/**
* Launches a CustomTabs (or calls Fallback's fallback function if no browser is compatible)
* with the given URL.
*
* @param context Needed to launch the CustomTab and to bind the app to the CustomTabs service
* @param URL The URL that needs to be opened in the CustomTab
* @return Returns itself
*/
public ChromeTabsUtils open(Activity context, String URL) {
// Call the more complete function existing, it'll setup the builder automatically
return open(context, URL, null);
}
/**
* Launches a CustomTabs (or calls Fallback's fallback function if no browser is compatible)
* with the given URL and CustomTabs Builder for UI customization.
*
* @param context Needed to launch the CustomTab and to bind the app to the CustomTabs service
* @param URL The URL that needs to be opened in the CustomTab
* @return Returns itself
*/
public ChromeTabsUtils open(Activity context, String URL, CustomTabsIntent.Builder builder) {
// Check for browser's existance
if(!isValidBrowser(context, BROWSER)) {
// Check if fallback browser is set as browser and call fallback function
if(BROWSER.equals(_FALLBACK)) {
FALLBACK.onFailedToOpen(context, URL, builder);
}
// Break instantly
return this;
}
// Setup a new session if one wasn't created yet
if(CLIENT == null || SESSION == null) {
CustomTabsClient.bindCustomTabsService(context, BROWSER, CONNECTION);
}
// Create a new Builder and configure basic UI for it if the current builder is null
if(builder == null) {
builder = new CustomTabsIntent.Builder(SESSION);
builder.setShowTitle(true);
}
// Launch the URL using the builder's intent
builder.build().launchUrl(context, Uri.parse(URL));
return this;
}
/**
* Sets a custom CustomTabsFallback object to call in case of missing browser or any other
* generic problem handled by the CustomTabsFallback interface.
*
* @param fallback The fallback object that will be called if something goes wrong
* @return Returns itself
*/
public ChromeTabsUtils setFallback(CustomTabsFallback fallback) {
// Only apply new fallback if fallback is not null
if(fallback != null) {
FALLBACK = fallback;
}
return this;
}
/**
* Returns the static copy of the first ChromeTabUtils created creating a new one if necessary.
*
* @param context Used to create a new ChromeTabsUtils object if not already done
* @return Returns the first ChromeTabUtils created
*/
public static ChromeTabsUtils get(Context context) {
// Only create a new object if nobody already did it
if(CTU_THIS == null) {
CTU_THIS = new ChromeTabsUtils(context);
}
return CTU_THIS;
}
/**
* Finds a valid Application to open CustomTabs with, preferes the default browser or Chrome
* if any of its versions (Stable, Beta, Dev or "system"/local) is present.
*
* @param context Needed to use PackageManager
*/
private void findBrowser(@NonNull Context context) {
if(BROWSER == null) {
// If we do not still have found a browser to open Custom Tabs with
// Test Intent for all the web browsers apps
Intent testIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.com"));
// Get package manager and test which activity by default receives the intent
PackageManager packageManager = context.getPackageManager();
ResolveInfo handlerInfo = packageManager.resolveActivity(testIntent, 0);
String handlerPackage = null; // Stores the default package name if present
// If there is a valid activity, save it's package name for later usage
if(handlerInfo != null) {
handlerPackage = handlerInfo.activityInfo.packageName;
}
// Try to get all the activities supporting the test intent
List<ResolveInfo> allInfos = packageManager.queryIntentActivities(testIntent, 0);
Deque<String> allValidPackages = new ArrayDeque<>(); // Stores all the replying packages
// Loop for each package who replied to the test intent
for(ResolveInfo info : allInfos) {
if(info != null) {
// If the package exists, test if it can open Custom Tabs
Intent supportIntent = new Intent();
supportIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
supportIntent.setPackage(info.activityInfo.packageName);
if(packageManager.resolveService(supportIntent, 0) != null) {
// If it can, save it into the Deque temporarely
allValidPackages.add(info.activityInfo.packageName);
}
}
}
// If we got at least one package replying ...
if(!allValidPackages.isEmpty()) {
if(allValidPackages.contains(handlerPackage)){
// ... and the default one is in the list, use it
BROWSER = handlerPackage;
} else if(allValidPackages.contains(CHROME_STABLE)) {
// ... and Chrome as system app is in the list, use it
BROWSER = CHROME_STABLE;
} else if(allValidPackages.contains(CHROME_GOOGLE)) {
// ... and Chrome as local app is in the list, use it
BROWSER = CHROME_GOOGLE;
} else if(allValidPackages.contains(CHROME_BETA)) {
// ... and Chome Beta is in the list, use it
BROWSER = CHROME_BETA;
} else if(allValidPackages.contains(CHROME_DEV)) {
// ... and Chrome Dev is in the list, use it
BROWSER = CHROME_DEV;
} else {
// ... and there is only one package or it just has not the preferred ones, use
// the first (and only maybe) one
BROWSER = allValidPackages.getFirst();
}
}
} else if(!isValidBrowser(context, BROWSER)) {
// Use WebView as fallback if we have found a browser without Custom Tabs APIs support
BROWSER = _FALLBACK;
}
}
/**
* Given a package name, it tests if the application using that package name can reply to
* CustomTabs initialization intents, assuming API support if true.
*
* @param context Needed to use PackageManager
* @param packageName The Package Name for the Application to check CustomTabs APIs support on
* @return Returns if the given Package supports CustomTabs APIs
*/
private boolean isValidBrowser(@NonNull Context context, @NonNull String packageName) {
// Create a fake intent to test Custom Tabs APIs support on the given package
Intent testIntent = new Intent();
testIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
testIntent.setPackage(packageName);
// If the intent found a service in that package, the package supports Custom Tabs APIs
return context.getPackageManager().resolveService(testIntent, 0) != null;
}
/**
* Handles all the CustomTabsCallback for each CustomTab registered, calling them in a loop
* whenever something happens in a CustomTab. Real handling is left to the developer.
*/
private class CallbackHandler extends CustomTabsCallback {
public CallbackHandler() {
super();
}
@Override
public void onNavigationEvent(int navigationEvent, Bundle extras) {
super.onNavigationEvent(navigationEvent, extras);
// Warn all the callbacks registered to this helper
for(CustomTabsCallback callback : CALLBACKS) {
callback.onNavigationEvent(navigationEvent, extras);
}
}
@Override
public void extraCallback(String callbackName, Bundle args) {
super.extraCallback(callbackName, args);
// Warn all the callbacks registered to this helper
for(CustomTabsCallback callback : CALLBACKS) {
callback.extraCallback(callbackName, args);
}
}
}
/**
* Handles the creation and destruction of CustomTab clients and sessions automatically
*/
private class ConnectionHandler extends CustomTabsServiceConnection {
public ConnectionHandler() {
super();
}
@Override
public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient customTabsClient) {
// Configure the new client and session
CLIENT = customTabsClient;
SESSION = CLIENT.newSession(CALLBACK);
// Warm the client up in the background
CLIENT.warmup(WARMUP);
}
/**
* Called when a connection to the Service has been lost. This typically
* happens when the process hosting the service has crashed or been killed.
* This does <em>not</em> remove the ServiceConnection itself -- this
* binding to the service will remain active, and you will receive a call
* to {@link #onServiceConnected} when the Service is next running.
*
* @param name The concrete component name of the service whose
* connection has been lost.
*/
@Override
public void onServiceDisconnected(ComponentName name) {
// Remove the client: Doc, we lost it.
CLIENT = null;
}
}
/**
* Predefines the structure to use for custom fallback mechanism in case something goes wrong.
*/
public interface CustomTabsFallback {
/**
* Called when no packages were found supporting the CustomTabs API
*/
void onFailedToOpen(Activity context, String URL, CustomTabsIntent.Builder builder);
}
/**
* Default fallback class. Opens a WebView if onValidBrowserNotFound gets called with the URL
* link to point the WebView at.
*/
private class WebViewFallback implements CustomTabsFallback {
/**
* Called when no packages were found supporting the CustomTabs API
*/
@Override
public void onFailedToOpen(Activity context, String URL, CustomTabsIntent.Builder builder) {
// Since Fallback WebView supports CustomTabsIntents, re-route the Intent to it
Intent orig = builder.build().intent;
orig.setData(Uri.parse(URL));
context.startActivity(orig);
}
}
}
/*
* Copyright 2015 Diego Rossi (@_HellPie)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.hellpie.generic.utils.customtabs;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
public class FallbackWebView extends AppCompatActivity {
/**
* Main UI customization flags inserted in Intent by CustomTabs' Intent Builder
*/
public static final String COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR";
public static final String ICON = "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
public static final String SHOW_TITLE = "android.support.customtabs.extra.TITLE_VISIBILITY";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a WebView and assign it a client
WebView webView = new WebView(this);
webView.setWebChromeClient(new WebChromeClient());
// Normap WebView cnfiguration routine
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
// Enable back button functionality
getSupportActionBar().setDisplayShowHomeEnabled(true);
// Set the ActionBar button to back if no close button was set, otherwise, set given bitmap
Bitmap bitmap = getIntent().getParcelableExtra(ICON);
if(bitmap == null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} else {
getSupportActionBar().setIcon(new BitmapDrawable(getResources(), bitmap));
}
// If color is set, set the action bar to that color
int color = getIntent().getIntExtra(COLOR, -1);
if(color != -1) {
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color));
}
// Add the webview to the view
this.addContentView(webView, null);
// Load the URL
webView.loadUrl(getIntent().getData().toString());
// Set Activity title only if title is set to be shown
if(getIntent().getIntExtra(SHOW_TITLE, 1) == 1) {
setTitle(webView.getTitle());
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment