bolts.WebViewAppLinkResolver.java Source code

Java tutorial

Introduction

Here is the source code for bolts.WebViewAppLinkResolver.java

Source

/*
 *  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 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 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<AppLink> getAppLinkFromUrlInBackground(final Uri url) {
        final Capture<String> content = new Capture<String>();
        final Capture<String> contentType = new Capture<String>();
        return Task.callInBackground(new Callable<Void>() {
            @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<Void, Task<JSONArray>>() {
            @Override
            public Task<JSONArray> then(Task<Void> task) throws Exception {
                // Load the content in a WebView and use JavaScript to extract the meta tags.
                final TaskCompletionSource<JSONArray> tcs = new TaskCompletionSource<>();
                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<JSONArray, AppLink>() {
            @Override
            public AppLink then(Task<JSONArray> task) throws Exception {
                Map<String, Object> 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<String, Object> parseAlData(JSONArray dataArray) throws JSONException {
        HashMap<String, Object> al = new HashMap<String, Object>();
        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<String, Object> root = al;
            for (int j = 1; j < nameComponents.length; j++) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> children = (List<Map<String, Object>>) root.get(nameComponents[j]);
                if (children == null) {
                    children = new ArrayList<Map<String, Object>>();
                    root.put(nameComponents[j], children);
                }
                Map<String, Object> child = children.size() > 0 ? children.get(children.size() - 1) : null;
                if (child == null || j == nameComponents.length - 1) {
                    child = new HashMap<String, Object>();
                    children.add(child);
                }
                root = child;
            }
            if (tag.has("content")) {
                if (tag.isNull("content")) {
                    root.put(KEY_AL_VALUE, null);
                } else {
                    root.put(KEY_AL_VALUE, tag.getString("content"));
                }
            }
        }
        return al;
    }

    @SuppressWarnings("unchecked")
    private static List<Map<String, Object>> getAlList(Map<String, Object> map, String key) {
        List<Map<String, Object>> result = (List<Map<String, Object>>) map.get(key);
        if (result == null) {
            return Collections.emptyList();
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    private static AppLink makeAppLinkFromAlData(Map<String, Object> appLinkDict, Uri destination) {
        List<AppLink.Target> targets = new ArrayList<AppLink.Target>();
        List<Map<String, Object>> platformMapList = (List<Map<String, Object>>) appLinkDict.get(KEY_ANDROID);
        if (platformMapList == null) {
            platformMapList = Collections.emptyList();
        }
        for (Map<String, Object> platformMap : platformMapList) {
            // The schema requires a single url/package/app name/class, but we could find multiple
            // of them. We'll make a best effort to interpret this data.
            List<Map<String, Object>> urls = getAlList(platformMap, KEY_URL);
            List<Map<String, Object>> packages = getAlList(platformMap, KEY_PACKAGE);
            List<Map<String, Object>> classes = getAlList(platformMap, KEY_CLASS);
            List<Map<String, Object>> appNames = getAlList(platformMap, KEY_APP_NAME);

            int maxCount = Math.max(urls.size(),
                    Math.max(packages.size(), Math.max(classes.size(), appNames.size())));
            for (int i = 0; i < maxCount; i++) {
                String urlString = (String) (urls.size() > i ? urls.get(i).get(KEY_AL_VALUE) : null);
                Uri url = tryCreateUrl(urlString);
                String packageName = (String) (packages.size() > i ? packages.get(i).get(KEY_AL_VALUE) : null);
                String className = (String) (classes.size() > i ? classes.get(i).get(KEY_AL_VALUE) : null);
                String appName = (String) (appNames.size() > i ? appNames.get(i).get(KEY_AL_VALUE) : null);
                AppLink.Target target = new AppLink.Target(packageName, className, url, appName);
                targets.add(target);
            }
        }

        Uri webUrl = destination;
        List<Map<String, Object>> webMapList = (List<Map<String, Object>>) appLinkDict.get(KEY_WEB);
        if (webMapList != null && webMapList.size() > 0) {
            Map<String, Object> webMap = webMapList.get(0);
            List<Map<String, Object>> urls = (List<Map<String, Object>>) webMap.get(KEY_WEB_URL);
            List<Map<String, Object>> shouldFallbacks = (List<Map<String, Object>>) webMap.get(KEY_SHOULD_FALLBACK);
            if (shouldFallbacks != null && shouldFallbacks.size() > 0) {
                String shouldFallbackString = (String) shouldFallbacks.get(0).get(KEY_AL_VALUE);
                if (Arrays.asList("no", "false", "0").contains(shouldFallbackString.toLowerCase())) {
                    webUrl = null;
                }
            }
            if (webUrl != null && urls != null && urls.size() > 0) {
                String webUrlString = (String) urls.get(0).get(KEY_AL_VALUE);
                webUrl = tryCreateUrl(webUrlString);
            }
        }
        return new AppLink(destination, targets, webUrl);
    }

    private static Uri tryCreateUrl(String urlString) {
        if (urlString == null) {
            return null;
        }
        return Uri.parse(urlString);
    }

    /**
     * Gets a string with the proper encoding (including using the charset specified in the MIME type
     * of the request) from a URLConnection.
     */
    private static String readFromConnection(URLConnection connection) throws IOException {
        InputStream stream;
        if (connection instanceof HttpURLConnection) {
            HttpURLConnection httpConnection = (HttpURLConnection) connection;
            try {
                stream = connection.getInputStream();
            } catch (Exception e) {
                stream = httpConnection.getErrorStream();
            }
        } else {
            stream = connection.getInputStream();
        }
        try {
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int read = 0;
            while ((read = stream.read(buffer)) != -1) {
                output.write(buffer, 0, read);
            }
            String charset = connection.getContentEncoding();
            if (charset == null) {
                String mimeType = connection.getContentType();
                String[] parts = mimeType.split(";");
                for (String part : parts) {
                    part = part.trim();
                    if (part.startsWith("charset=")) {
                        charset = part.substring("charset=".length());
                        break;
                    }
                }
                if (charset == null) {
                    charset = "UTF-8";
                }
            }
            return new String(output.toByteArray(), charset);
        } finally {
            stream.close();
        }
    }
}