bolts.WebViewAppLinkResolver Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bolts-android Show documentation
Show all versions of bolts-android Show documentation
Bolts is a collection of low-level libraries designed to make developing mobile apps easier.
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
package bolts;
import android.content.Context;
import android.net.Uri;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* A reference implementation for an App Link resolver that uses a hidden
* {@link android.webkit.WebView} to parse the HTML containing App Link metadata.
*/
public class WebViewAppLinkResolver implements AppLinkResolver {
private final Context context;
/**
* Creates a WebViewAppLinkResolver.
*
* @param context the context in which to create the hidden {@link android.webkit.WebView}.
*/
public WebViewAppLinkResolver(Context context) {
this.context = context;
}
private static final String TAG_EXTRACTION_JAVASCRIPT = "javascript:" +
"boltsWebViewAppLinkResolverResult.setValue((function() {" +
" var metaTags = document.getElementsByTagName('meta');" +
" var results = [];" +
" for (var i = 0; i < metaTags.length; i++) {" +
" var property = metaTags[i].getAttribute('property');" +
" if (property && property.substring(0, 'al:'.length) === 'al:') {" +
" var tag = { \"property\": metaTags[i].getAttribute('property') };" +
" if (metaTags[i].hasAttribute('content')) {" +
" tag['content'] = metaTags[i].getAttribute('content');" +
" }" +
" results.push(tag);" +
" }" +
" }" +
" return JSON.stringify(results);" +
"})())";
private static final String PREFER_HEADER = "Prefer-Html-Meta-Tags";
private static final String META_TAG_PREFIX = "al";
private static final String KEY_AL_VALUE = "value";
private static final String KEY_APP_NAME = "app_name";
private static final String KEY_CLASS = "class";
private static final String KEY_PACKAGE = "package";
private static final String KEY_URL = "url";
private static final String KEY_SHOULD_FALLBACK = "should_fallback";
private static final String KEY_WEB_URL = "url";
private static final String KEY_WEB = "web";
private static final String KEY_ANDROID = "android";
@Override
public Task getAppLinkFromUrlInBackground(final Uri url) {
final Capture content = new Capture();
final Capture contentType = new Capture();
return Task.callInBackground(new Callable() {
@Override
public Void call() throws Exception {
URL currentURL = new URL(url.toString());
URLConnection connection = null;
while (currentURL != null) {
// Fetch the content at the given URL.
connection = currentURL.openConnection();
if (connection instanceof HttpURLConnection) {
// Unfortunately, this doesn't actually follow redirects if they go from http->https,
// so we have to do that manually.
((HttpURLConnection) connection).setInstanceFollowRedirects(true);
}
connection.setRequestProperty(PREFER_HEADER, META_TAG_PREFIX);
connection.connect();
if (connection instanceof HttpURLConnection) {
HttpURLConnection httpConnection = (HttpURLConnection) connection;
if (httpConnection.getResponseCode() >= 300 && httpConnection.getResponseCode() < 400) {
currentURL = new URL(httpConnection.getHeaderField("Location"));
httpConnection.disconnect();
} else {
currentURL = null;
}
} else {
currentURL = null;
}
}
try {
content.set(readFromConnection(connection));
contentType.set(connection.getContentType());
} finally {
if (connection instanceof HttpURLConnection) {
((HttpURLConnection) connection).disconnect();
}
}
return null;
}
}).onSuccessTask(new Continuation>() {
@Override
public Task then(Task task) throws Exception {
// Load the content in a WebView and use JavaScript to extract the meta tags.
final Task.TaskCompletionSource tcs = Task.create();
final WebView webView = new WebView(context);
webView.getSettings().setJavaScriptEnabled(true);
webView.setNetworkAvailable(false);
webView.setWebViewClient(new WebViewClient() {
private boolean loaded = false;
private void runJavaScript(WebView view) {
if (!loaded) {
// After the first resource has been loaded (which will be the pre-populated data)
// run the JavaScript meta tag extraction script
loaded = true;
view.loadUrl(TAG_EXTRACTION_JAVASCRIPT);
}
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
runJavaScript(view);
}
@Override
public void onLoadResource(WebView view, String url) {
super.onLoadResource(view, url);
runJavaScript(view);
}
});
// Inject an object that will receive the JSON for the extracted JavaScript tags
webView.addJavascriptInterface(new Object() {
@JavascriptInterface
public void setValue(String value) {
try {
tcs.trySetResult(new JSONArray(value));
} catch (JSONException e) {
tcs.trySetError(e);
}
}
}, "boltsWebViewAppLinkResolverResult");
String inferredContentType = null;
if (contentType.get() != null) {
inferredContentType = contentType.get().split(";")[0];
}
webView.loadDataWithBaseURL(url.toString(),
content.get(),
inferredContentType,
null,
null);
return tcs.getTask();
}
}, Task.UI_THREAD_EXECUTOR).onSuccess(new Continuation() {
@Override
public AppLink then(Task task) throws Exception {
Map alData = parseAlData(task.getResult());
AppLink appLink = makeAppLinkFromAlData(alData, url);
return appLink;
}
});
}
/**
* Builds up a data structure filled with the app link data from the meta tags on a page.
* The structure of this object is a dictionary where each key holds an array of app link
* data dictionaries. Values are stored in a key called "_value".
*/
private static Map parseAlData(JSONArray dataArray) throws JSONException {
HashMap al = new HashMap();
for (int i = 0; i < dataArray.length(); i++) {
JSONObject tag = dataArray.getJSONObject(i);
String name = tag.getString("property");
String[] nameComponents = name.split(":");
if (!nameComponents[0].equals(META_TAG_PREFIX)) {
continue;
}
Map root = al;
for (int j = 1; j < nameComponents.length; j++) {
@SuppressWarnings("unchecked")
List
© 2015 - 2024 Weber Informatics LLC | Privacy Policy