All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bolts.WebViewAppLinkResolver Maven / Gradle / Ivy

Go to download

Bolts is a collection of low-level libraries designed to make developing mobile apps easier.

There is a newer version: 1.4.0
Show newest version
/*
 *  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> children =
                (List>) root.get(nameComponents[j]);
        if (children == null) {
          children = new ArrayList>();
          root.put(nameComponents[j], children);
        }
        Map child = children.size() > 0 ? children.get(children.size() - 1) : null;
        if (child == null || j == nameComponents.length - 1) {
          child = new HashMap();
          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> getAlList(Map map, String key) {
    List> result = (List>) map.get(key);
    if (result == null) {
      return Collections.emptyList();
    }
    return result;
  }

  @SuppressWarnings("unchecked")
  private static AppLink makeAppLinkFromAlData(Map appLinkDict, Uri destination) {
    List targets = new ArrayList();
    List> platformMapList =
            (List>) appLinkDict.get(KEY_ANDROID);
    if (platformMapList == null) {
      platformMapList = Collections.emptyList();
    }
    for (Map 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> urls = getAlList(platformMap, KEY_URL);
      List> packages = getAlList(platformMap, KEY_PACKAGE);
      List> classes = getAlList(platformMap, KEY_CLASS);
      List> 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> webMapList = (List>) appLinkDict.get(KEY_WEB);
    if (webMapList != null && webMapList.size() > 0) {
      Map webMap = webMapList.get(0);
      List> urls = (List>) webMap.get(KEY_WEB_URL);
      List> shouldFallbacks =
              (List>) 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();
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy