diff --git a/Bolts/src/bolts/HtmlAppLinkResolver.java b/Bolts/src/bolts/HtmlAppLinkResolver.java
new file mode 100644
index 0000000..9855e8f
--- /dev/null
+++ b/Bolts/src/bolts/HtmlAppLinkResolver.java
@@ -0,0 +1,110 @@
+/*
+ * 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.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.text.Editable;
+import android.text.Html;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * A simple implementation for an App Link resolver that parses the HTML containing App Link metadata
+ * using {@link android.text.Html#fromHtml}.
+ */
+public class HtmlAppLinkResolver implements AppLinkResolver {
+ /**
+ * Creates an {@code HtmlAppLinkResolver}.
+ */
+ public HtmlAppLinkResolver() {
+ }
+
+ private static final String META_TAG = "meta";
+ private static final String META_TAG_PREFIX = "al:";
+ private static final String META_ATTR_PROPERTY = "property";
+ private static final String META_ATTR_CONTENT = "content";
+
+ @Override
+ public Task getAppLinkFromUrlInBackground(final Uri url) {
+ return ResolverUtils.fetchUrl(url)
+ .onSuccess(new Continuation() {
+ @Override
+ public JSONArray then(Task task) throws Exception {
+ return parseMetaTags(task.getResult().getContent());
+ }
+ }).onSuccess(new Continuation() {
+ @Override
+ public AppLink then(Task task) throws Exception {
+ return ResolverUtils.parseAppLinkFromAlData(task.getResult(), url);
+ }
+ });
+ }
+
+ private static JSONArray parseMetaTags(String html) throws Exception {
+ final MetaTagHandler metaTagHandler = new MetaTagHandler();
+ Html.fromHtml(html, metaTagHandler, metaTagHandler);
+
+ return metaTagHandler.getJsonArray();
+ }
+
+ private static class MetaTagHandler extends DefaultHandler implements Html.TagHandler, Html.ImageGetter {
+
+ private JSONArray jsonArray = new JSONArray();
+
+ @Override
+ public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
+ xmlReader.setContentHandler(this);
+ }
+
+ @Override
+ public Drawable getDrawable(String source) {
+ return null;
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+ if (META_TAG.equalsIgnoreCase(localName)) {
+ addMetaElement(attributes);
+ }
+ }
+
+ public JSONArray getJsonArray() {
+ return jsonArray;
+ }
+
+ private void addMetaElement(Attributes attributes) throws SAXException {
+ String property = attributes.getValue(META_ATTR_PROPERTY);
+
+ if (property != null && property.startsWith(META_TAG_PREFIX)) {
+ String content = attributes.getValue(META_ATTR_CONTENT);
+
+ JSONObject metaObject = new JSONObject();
+ try {
+ metaObject.put(META_ATTR_PROPERTY, property);
+ if (content != null) {
+ metaObject.put(META_ATTR_CONTENT, content);
+ }
+ } catch (JSONException e) {
+ throw new SAXException(e);
+ }
+
+ jsonArray.put(metaObject);
+ }
+ }
+
+ }
+}
diff --git a/Bolts/src/bolts/MeasurementEvent.java b/Bolts/src/bolts/MeasurementEvent.java
index 6447ef8..6f49956 100644
--- a/Bolts/src/bolts/MeasurementEvent.java
+++ b/Bolts/src/bolts/MeasurementEvent.java
@@ -7,7 +7,6 @@
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
-
package bolts;
import android.content.ComponentName;
diff --git a/Bolts/src/bolts/ResolverUtils.java b/Bolts/src/bolts/ResolverUtils.java
new file mode 100644
index 0000000..e20e899
--- /dev/null
+++ b/Bolts/src/bolts/ResolverUtils.java
@@ -0,0 +1,298 @@
+/*
+ * 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.net.Uri;
+
+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;
+
+public class ResolverUtils {
+
+ 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";
+
+ private ResolverUtils() {
+ }
+
+ /**
+ * Fetch the content from the specified URL.
+ *
+ * @param url the URL to fetch the content from.
+ * @return the page content as a {@code StringEntity}.
+ */
+ public static Task fetchUrl(final Uri url) {
+ return Task.callInBackground(new Callable() {
+ @Override
+ public StringEntity 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 {
+ String content = readFromConnection(connection);
+ String contentType = connection.getContentType();
+ return new StringEntity(content, contentType);
+ } finally {
+ if (connection instanceof HttpURLConnection) {
+ ((HttpURLConnection) connection).disconnect();
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Parse an AppLink from the specified al data specified in a JSON array.
+ *
+ * @param dataArray the al meta tag data.
+ * @param url the URL of the AppLink.
+ * @return the AppLink.
+ * @throws JSONException if there is an error parsing the JSON array.
+ */
+ public static AppLink parseAppLinkFromAlData(JSONArray dataArray, Uri url) throws JSONException {
+ Map alData = parseAlData(dataArray);
+ return makeAppLinkFromAlData(alData, url);
+ }
+
+ /**
+ * 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