Last active
September 6, 2015 02:30
-
-
Save ingyesid/1bc6b145657b0bf29e21 to your computer and use it in GitHub Desktop.
Utils for Android's CustomTabs Support Library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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