Implement IntentFilter verification service.

This commit adds a verifier that verifies a host delegates permission for
an app to handle Url for the host using the Statement protocol.

- Implements the Statement protocol
-- The protocol defines a file format that represents statements.
-- The protocol defines where each asset type should put their statement
declaration. For web asset, the statement file should be hosted at
<scheme>://<host>:<port>/.well-known/associations.json.

- Implements IntentFilterVerificationReceiver, an interface between
StatementService and PackageManager. PackageManager will send a
broadcast with action Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION.
The service will process the request and returns the results by calling
PackageManager.verifyIntentFilter().

To verify an IntentFilter like this defined in Android app com.test.app
<intent-filter>
  <data android:scheme="https" />
  <data android:host="www.test.com" />
  <data android:pathPattern=".*"/>
</intent-filter>

The service will try to retrieve the statement file from
https://www.test.com:443/.well-known/associations.json and try to find
a JSON object equivalent to
{'relation': ['delegate_permission/common.handle_all_urls'],
 'target': {'namespace': 'android_app',
            'package_name': 'com.test.app',
            'sha256_cert_fingerprints': [APP_CERT_FP]}}
The entry should have the correct relation, package name, and
certificate sha256 fingerprint.

Because this implementation will send a HTTP request for each host
specified in the intent-filter in AndroidManifest.xml, to avoid overwhelming
the network at app install time, we limit the maximum number of hosts we will
verify for a single app to 10. Any app with more than 10 hosts in the
autoVerify=true intent-filter won't be auto verified.

Change-Id: I787c9d176e4110aa441eb5fe4fa9651a071c6610
diff --git a/packages/StatementService/src/com/android/statementservice/DirectStatementService.java b/packages/StatementService/src/com/android/statementservice/DirectStatementService.java
new file mode 100644
index 0000000..449738e
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/DirectStatementService.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice;
+
+import android.app.Service;
+import android.content.Intent;
+import android.net.http.HttpResponseCache;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.android.statementservice.retriever.AbstractAsset;
+import com.android.statementservice.retriever.AbstractAssetMatcher;
+import com.android.statementservice.retriever.AbstractStatementRetriever;
+import com.android.statementservice.retriever.AbstractStatementRetriever.Result;
+import com.android.statementservice.retriever.AssociationServiceException;
+import com.android.statementservice.retriever.Relation;
+import com.android.statementservice.retriever.Statement;
+
+import org.json.JSONException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/**
+ * Handles com.android.statementservice.service.CHECK_ALL_ACTION intents.
+ */
+public final class DirectStatementService extends Service {
+    private static final String TAG = DirectStatementService.class.getSimpleName();
+
+    /**
+     * Returns true if every asset in {@code SOURCE_ASSET_DESCRIPTORS} is associated with {@code
+     * EXTRA_TARGET_ASSET_DESCRIPTOR} for {@code EXTRA_RELATION} relation.
+     *
+     * <p>Takes parameter {@code EXTRA_RELATION}, {@code SOURCE_ASSET_DESCRIPTORS}, {@code
+     * EXTRA_TARGET_ASSET_DESCRIPTOR}, and {@code EXTRA_RESULT_RECEIVER}.
+     */
+    public static final String CHECK_ALL_ACTION =
+            "com.android.statementservice.service.CHECK_ALL_ACTION";
+
+    /**
+     * Parameter for {@link #CHECK_ALL_ACTION}.
+     *
+     * <p>A relation string.
+     */
+    public static final String EXTRA_RELATION =
+            "com.android.statementservice.service.RELATION";
+
+    /**
+     * Parameter for {@link #CHECK_ALL_ACTION}.
+     *
+     * <p>An array of asset descriptors in JSON.
+     */
+    public static final String EXTRA_SOURCE_ASSET_DESCRIPTORS =
+            "com.android.statementservice.service.SOURCE_ASSET_DESCRIPTORS";
+
+    /**
+     * Parameter for {@link #CHECK_ALL_ACTION}.
+     *
+     * <p>An asset descriptor in JSON.
+     */
+    public static final String EXTRA_TARGET_ASSET_DESCRIPTOR =
+            "com.android.statementservice.service.TARGET_ASSET_DESCRIPTOR";
+
+    /**
+     * Parameter for {@link #CHECK_ALL_ACTION}.
+     *
+     * <p>A {@code ResultReceiver} instance that will be used to return the result. If the request
+     * failed, return {@link #RESULT_FAIL} and an empty {@link android.os.Bundle}. Otherwise, return
+     * {@link #RESULT_SUCCESS} and a {@link android.os.Bundle} with the result stored in {@link
+     * #IS_ASSOCIATED}.
+     */
+    public static final String EXTRA_RESULT_RECEIVER =
+            "com.android.statementservice.service.RESULT_RECEIVER";
+
+    /**
+     * A boolean bundle entry that stores the result of {@link #CHECK_ALL_ACTION}.
+     * This is set only if the service returns with {@code RESULT_SUCCESS}.
+     * {@code IS_ASSOCIATED} is true if and only if {@code FAILED_SOURCES} is empty.
+     */
+    public static final String IS_ASSOCIATED = "is_associated";
+
+    /**
+     * A String ArrayList bundle entry that stores sources that can't be verified.
+     */
+    public static final String FAILED_SOURCES = "failed_sources";
+
+    /**
+     * Returned by the service if the request is successfully processed. The caller should check
+     * the {@code IS_ASSOCIATED} field to determine if the association exists or not.
+     */
+    public static final int RESULT_SUCCESS = 0;
+
+    /**
+     * Returned by the service if the request failed. The request will fail if, for example, the
+     * input is not well formed, or the network is not available.
+     */
+    public static final int RESULT_FAIL = 1;
+
+    private static final long HTTP_CACHE_SIZE_IN_BYTES = 1 * 1024 * 1024;  // 1 MBytes
+    private static final String CACHE_FILENAME = "request_cache";
+
+    private AbstractStatementRetriever mStatementRetriever;
+    private Handler mHandler;
+    private HandlerThread mThread;
+    private HttpResponseCache mHttpResponseCache;
+
+    @Override
+    public void onCreate() {
+        mThread = new HandlerThread("DirectStatementService thread",
+                android.os.Process.THREAD_PRIORITY_BACKGROUND);
+        mThread.start();
+        onCreate(AbstractStatementRetriever.createDirectRetriever(this), mThread.getLooper(),
+                getCacheDir());
+    }
+
+    /**
+     * Creates a DirectStatementService with the dependencies passed in for easy testing.
+     */
+    public void onCreate(AbstractStatementRetriever statementRetriever, Looper looper,
+                         File cacheDir) {
+        super.onCreate();
+        mStatementRetriever = statementRetriever;
+        mHandler = new Handler(looper);
+
+        try {
+            File httpCacheDir = new File(cacheDir, CACHE_FILENAME);
+            mHttpResponseCache = HttpResponseCache.install(httpCacheDir, HTTP_CACHE_SIZE_IN_BYTES);
+        } catch (IOException e) {
+            Log.i(TAG, "HTTPS response cache installation failed:" + e);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mThread != null) {
+            mThread.quit();
+        }
+
+        try {
+            if (mHttpResponseCache != null) {
+                mHttpResponseCache.delete();
+            }
+        } catch (IOException e) {
+            Log.i(TAG, "HTTP(S) response cache deletion failed:" + e);
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        super.onStartCommand(intent, flags, startId);
+
+        if (intent == null) {
+            Log.e(TAG, "onStartCommand called with null intent");
+            return START_STICKY;
+        }
+
+        if (intent.getAction().equals(CHECK_ALL_ACTION)) {
+
+            Bundle extras = intent.getExtras();
+            List<String> sources = extras.getStringArrayList(EXTRA_SOURCE_ASSET_DESCRIPTORS);
+            String target = extras.getString(EXTRA_TARGET_ASSET_DESCRIPTOR);
+            String relation = extras.getString(EXTRA_RELATION);
+            ResultReceiver resultReceiver = extras.getParcelable(EXTRA_RESULT_RECEIVER);
+
+            if (resultReceiver == null) {
+                Log.e(TAG, " Intent does not have extra " + EXTRA_RESULT_RECEIVER);
+                return START_STICKY;
+            }
+            if (sources == null) {
+                Log.e(TAG, " Intent does not have extra " + EXTRA_SOURCE_ASSET_DESCRIPTORS);
+                resultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+                return START_STICKY;
+            }
+            if (target == null) {
+                Log.e(TAG, " Intent does not have extra " + EXTRA_TARGET_ASSET_DESCRIPTOR);
+                resultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+                return START_STICKY;
+            }
+            if (relation == null) {
+                Log.e(TAG, " Intent does not have extra " + EXTRA_RELATION);
+                resultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+                return START_STICKY;
+            }
+
+            mHandler.post(new ExceptionLoggingFutureTask<Void>(
+                    new IsAssociatedCallable(sources, target, relation, resultReceiver), TAG));
+        } else {
+            Log.e(TAG, "onStartCommand called with unsupported action: " + intent.getAction());
+        }
+        return START_STICKY;
+    }
+
+    private class IsAssociatedCallable implements Callable<Void> {
+
+        private List<String> mSources;
+        private String mTarget;
+        private String mRelation;
+        private ResultReceiver mResultReceiver;
+
+        public IsAssociatedCallable(List<String> sources, String target, String relation,
+                ResultReceiver resultReceiver) {
+            mSources = sources;
+            mTarget = target;
+            mRelation = relation;
+            mResultReceiver = resultReceiver;
+        }
+
+        private boolean verifyOneSource(AbstractAsset source, AbstractAssetMatcher target,
+                Relation relation) throws AssociationServiceException {
+            Result statements = mStatementRetriever.retrieveStatements(source);
+            for (Statement statement : statements.getStatements()) {
+                if (relation.matches(statement.getRelation())
+                        && target.matches(statement.getTarget())) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public Void call() {
+            Bundle result = new Bundle();
+            ArrayList<String> failedSources = new ArrayList<String>();
+            AbstractAssetMatcher target;
+            Relation relation;
+            try {
+                target = AbstractAssetMatcher.createMatcher(mTarget);
+                relation = Relation.create(mRelation);
+            } catch (AssociationServiceException | JSONException e) {
+                Log.e(TAG, "isAssociatedCallable failed with exception", e);
+                mResultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+                return null;
+            }
+
+            boolean allSourcesVerified = true;
+            for (String sourceString : mSources) {
+                AbstractAsset source;
+                try {
+                    source = AbstractAsset.create(sourceString);
+                } catch (AssociationServiceException e) {
+                    mResultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+                    return null;
+                }
+
+                try {
+                    if (!verifyOneSource(source, target, relation)) {
+                        failedSources.add(source.toJson());
+                        allSourcesVerified = false;
+                    }
+                } catch (AssociationServiceException e) {
+                    failedSources.add(source.toJson());
+                    allSourcesVerified = false;
+                }
+            }
+
+            result.putBoolean(IS_ASSOCIATED, allSourcesVerified);
+            result.putStringArrayList(FAILED_SOURCES, failedSources);
+            mResultReceiver.send(RESULT_SUCCESS, result);
+            return null;
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java b/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java
new file mode 100644
index 0000000..20c7f97
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice;
+
+import android.util.Log;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * {@link FutureTask} that logs unhandled exceptions.
+ */
+final class ExceptionLoggingFutureTask<V> extends FutureTask<V> {
+
+    private final String mTag;
+
+    public ExceptionLoggingFutureTask(Callable<V> callable, String tag) {
+        super(callable);
+        mTag = tag;
+    }
+
+    @Override
+    protected void done() {
+        try {
+            get();
+        } catch (ExecutionException | InterruptedException e) {
+            Log.e(mTag, "Uncaught exception.", e);
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java b/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java
new file mode 100644
index 0000000..712347a
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.util.Patterns;
+
+import com.android.statementservice.retriever.Utils;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Receives {@link Intent#ACTION_INTENT_FILTER_NEEDS_VERIFICATION} broadcast and calls
+ * {@link DirectStatementService} to verify the request. Calls
+ * {@link PackageManager#verifyIntentFilter} to notify {@link PackageManager} the result of the
+ * verification.
+ *
+ * This implementation of the API will send a HTTP request for each host specified in the query.
+ * To avoid overwhelming the network at app install time, {@code MAX_HOSTS_PER_REQUEST} limits
+ * the maximum number of hosts in a query. If a query contains more than
+ * {@code MAX_HOSTS_PER_REQUEST} hosts, it will fail immediately without making any HTTP request
+ * and call {@link PackageManager#verifyIntentFilter} with
+ * {@link PackageManager#INTENT_FILTER_VERIFICATION_FAILURE}.
+ */
+public final class IntentFilterVerificationReceiver extends BroadcastReceiver {
+    private static final String TAG = IntentFilterVerificationReceiver.class.getSimpleName();
+
+    private static final Integer MAX_HOSTS_PER_REQUEST = 10;
+
+    private static final String HANDLE_ALL_URLS_RELATION
+            = "delegate_permission/common.handle_all_urls";
+
+    private static final String ANDROID_ASSET_FORMAT = "{\"namespace\": \"android_app\", "
+            + "\"package_name\": \"%s\", \"sha256_cert_fingerprints\": [\"%s\"]}";
+    private static final String WEB_ASSET_FORMAT = "{\"namespace\": \"web\", \"site\": \"%s\"}";
+    private static final Pattern ANDROID_PACKAGE_NAME_PATTERN =
+            Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*$");
+    private static final String TOO_MANY_HOSTS_FORMAT =
+            "Request contains %d hosts which is more than the allowed %d.";
+
+    private static void sendErrorToPackageManager(PackageManager packageManager,
+            int verificationId) {
+        packageManager.verifyIntentFilter(verificationId,
+                PackageManager.INTENT_FILTER_VERIFICATION_FAILURE,
+                Collections.<String>emptyList());
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final String action = intent.getAction();
+        if (Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION.equals(action)) {
+            Bundle inputExtras = intent.getExtras();
+            if (inputExtras != null) {
+                Intent serviceIntent = new Intent(context, DirectStatementService.class);
+                serviceIntent.setAction(DirectStatementService.CHECK_ALL_ACTION);
+
+                int verificationId = inputExtras.getInt(
+                        PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID);
+                String scheme = inputExtras.getString(
+                        PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME);
+                String hosts = inputExtras.getString(
+                        PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS);
+                String packageName = inputExtras.getString(
+                        PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME);
+
+                Log.i(TAG, "Verify IntentFilter for " + hosts);
+
+                Bundle extras = new Bundle();
+                extras.putString(DirectStatementService.EXTRA_RELATION, HANDLE_ALL_URLS_RELATION);
+
+                String[] hostList = hosts.split(" ");
+                if (hostList.length > MAX_HOSTS_PER_REQUEST) {
+                    Log.w(TAG, String.format(TOO_MANY_HOSTS_FORMAT,
+                            hostList.length, MAX_HOSTS_PER_REQUEST));
+                    sendErrorToPackageManager(context.getPackageManager(), verificationId);
+                    return;
+                }
+
+                try {
+                    ArrayList<String> sourceAssets = new ArrayList<String>();
+                    for (String host : hostList) {
+                        sourceAssets.add(createWebAssetString(scheme, host));
+                    }
+                    extras.putStringArrayList(DirectStatementService.EXTRA_SOURCE_ASSET_DESCRIPTORS,
+                            sourceAssets);
+                } catch (MalformedURLException e) {
+                    Log.w(TAG, "Error when processing input host: " + e.getMessage());
+                    sendErrorToPackageManager(context.getPackageManager(), verificationId);
+                    return;
+                }
+                try {
+                    extras.putString(DirectStatementService.EXTRA_TARGET_ASSET_DESCRIPTOR,
+                            createAndroidAssetString(context, packageName));
+                } catch (NameNotFoundException e) {
+                    Log.w(TAG, "Error when processing input Android package: " + e.getMessage());
+                    sendErrorToPackageManager(context.getPackageManager(), verificationId);
+                    return;
+                }
+                extras.putParcelable(DirectStatementService.EXTRA_RESULT_RECEIVER,
+                        new IsAssociatedResultReceiver(
+                                new Handler(), context.getPackageManager(), verificationId));
+
+                serviceIntent.putExtras(extras);
+                context.startService(serviceIntent);
+            }
+        } else {
+            Log.w(TAG, "Intent action not supported: " + action);
+        }
+    }
+
+    private String createAndroidAssetString(Context context, String packageName)
+            throws NameNotFoundException {
+        if (!ANDROID_PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
+            throw new NameNotFoundException("Input package name is not valid.");
+        }
+
+        List<String> certFingerprints =
+                Utils.getCertFingerprintsFromPackageManager(packageName, context);
+
+        return String.format(ANDROID_ASSET_FORMAT, packageName,
+                Utils.joinStrings("\", \"", certFingerprints));
+    }
+
+    private String createWebAssetString(String scheme, String host) throws MalformedURLException {
+        if (!Patterns.DOMAIN_NAME.matcher(host).matches()) {
+            throw new MalformedURLException("Input host is not valid.");
+        }
+        if (!scheme.equals("http") && !scheme.equals("https")) {
+            throw new MalformedURLException("Input scheme is not valid.");
+        }
+
+        return String.format(WEB_ASSET_FORMAT, new URL(scheme, host, "").toString());
+    }
+
+    /**
+     * Receives the result of {@code StatementService.CHECK_ACTION} from
+     * {@link DirectStatementService} and passes it back to {@link PackageManager}.
+     */
+    private static class IsAssociatedResultReceiver extends ResultReceiver {
+
+        private final int mVerificationId;
+        private final PackageManager mPackageManager;
+
+        public IsAssociatedResultReceiver(Handler handler, PackageManager packageManager,
+                int verificationId) {
+            super(handler);
+            mVerificationId = verificationId;
+            mPackageManager = packageManager;
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            if (resultCode == DirectStatementService.RESULT_SUCCESS) {
+                if (resultData.getBoolean(DirectStatementService.IS_ASSOCIATED)) {
+                    mPackageManager.verifyIntentFilter(mVerificationId,
+                            PackageManager.INTENT_FILTER_VERIFICATION_SUCCESS,
+                            Collections.<String>emptyList());
+                } else {
+                    mPackageManager.verifyIntentFilter(mVerificationId,
+                            PackageManager.INTENT_FILTER_VERIFICATION_FAILURE,
+                            resultData.getStringArrayList(DirectStatementService.FAILED_SOURCES));
+                }
+            } else {
+                sendErrorToPackageManager(mPackageManager, mVerificationId);
+            }
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
new file mode 100644
index 0000000..e71cf54
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+/**
+ * A handle representing the identity and address of some digital asset. An asset is an online
+ * entity that typically provides some service or content. Examples of assets are websites, Android
+ * apps, Twitter feeds, and Plus Pages.
+ *
+ * <p> Asset can be represented by a JSON string. For example, the web site https://www.google.com
+ * can be represented by
+ * <pre>
+ * {"namespace": "web", "site": "https://www.google.com"}
+ * </pre>
+ *
+ * <p> The Android app with package name com.google.test that is signed by a certificate with sha256
+ * fingerprint 11:22:33 can be represented by
+ * <pre>
+ * {"namespace": "android_app",
+ *  "package_name": "com.google.test",
+ *  "sha256_cert_fingerprints": ["11:22:33"]}
+ * </pre>
+ *
+ * <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
+ * {@code keytool -list -printcert -jarfile signed_app.apk}
+ */
+public abstract class AbstractAsset {
+
+    /**
+     * Returns a JSON string representation of this asset. The strings returned by this function are
+     * normalized -- they can be used for equality testing.
+     */
+    public abstract String toJson();
+
+    /**
+     * Returns a key that can be used by {@link AbstractAssetMatcher} to lookup the asset.
+     *
+     * <p> An asset will match an {@code AssetMatcher} only if the value of this method is equal to
+     * {@code AssetMatcher.getMatchedLookupKey()}.
+     */
+    public abstract int lookupKey();
+
+    /**
+     * Creates a new Asset from its JSON string representation.
+     *
+     * @throws AssociationServiceException if the assetJson is not well formatted.
+     */
+    public static AbstractAsset create(String assetJson)
+            throws AssociationServiceException {
+        return AssetFactory.create(assetJson);
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAssetMatcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAssetMatcher.java
new file mode 100644
index 0000000..c35553f
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAssetMatcher.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import org.json.JSONException;
+
+/**
+ * An asset matcher that can match asset with the given query.
+ */
+public abstract class AbstractAssetMatcher {
+
+    /**
+     * Returns true if this AssetMatcher matches the asset.
+     */
+    public abstract boolean matches(AbstractAsset asset);
+
+    /**
+     * This AssetMatcher will only match Asset with {@code lookupKey()} equal to the value returned
+     * by this method.
+     */
+    public abstract int getMatchedLookupKey();
+
+    /**
+     * Creates a new AssetMatcher from its JSON string representation.
+     *
+     * <p> For web namespace, {@code query} will match assets that have the same 'site' field.
+     *
+     * <p> For Android namespace, {@code query} will match assets that have the same
+     * 'package_name' field and have at least one common certificate fingerprint in
+     * 'sha256_cert_fingerprints' field.
+     */
+    public static AbstractAssetMatcher createMatcher(String query)
+            throws AssociationServiceException, JSONException {
+        return AssetMatcherFactory.create(query);
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java
new file mode 100644
index 0000000..fb30bc1
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import android.content.Context;
+import android.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Retrieves the statements made by assets. This class is the entry point of the package.
+ * <p>
+ * An asset is an identifiable and addressable online entity that typically
+ * provides some service or content. Examples of assets are websites, Android
+ * apps, Twitter feeds, and Plus Pages.
+ * <p>
+ * Ownership of an asset is defined by being able to control it and speak for it.
+ * An asset owner may establish a relationship between the asset and another
+ * asset by making a statement about an intended relationship between the two.
+ * An example of a relationship is permission delegation. For example, the owner
+ * of a website (the webmaster) may delegate the ability the handle URLs to a
+ * particular mobile app. Relationships are considered public information.
+ * <p>
+ * A particular kind of relationship (like permission delegation) defines a binary
+ * relation on assets. The relation is not symmetric or transitive, nor is it
+ * antisymmetric or anti-transitive.
+ * <p>
+ * A statement S(r, a, b) is an assertion that the relation r holds for the
+ * ordered pair of assets (a, b). For example, taking r = "delegates permission
+ * to view user's location", a = New York Times mobile app,
+ * b = nytimes.com website, S(r, a, b) would be an assertion that "the New York
+ * Times mobile app delegates its ability to use the user's location to the
+ * nytimes.com website".
+ * <p>
+ * A statement S(r, a, b) is considered <b>reliable</b> if we have confidence that
+ * the statement is true; the exact criterion depends on the kind of statement,
+ * since some kinds of statements may be true on their face whereas others may
+ * require multiple parties to agree.
+ * <p>
+ * For example, to get the statements made by www.example.com use:
+ * <pre>
+ * result = retrieveStatements(AssetFactory.create(
+ *     "{\"namespace\": \"web\", \"site\": \"https://www.google.com\"}"))
+ * </pre>
+ * {@code result} will contain the statements and the expiration time of this result. The statements
+ * are considered reliable until the expiration time.
+ */
+public abstract class AbstractStatementRetriever {
+
+    /**
+     * Returns the statements made by the {@code source} asset with ttl.
+     *
+     * @throws AssociationServiceException if the asset namespace is not supported.
+     */
+    public abstract Result retrieveStatements(AbstractAsset source)
+            throws AssociationServiceException;
+
+    /**
+     * The retrieved statements and the expiration date.
+     */
+    public interface Result {
+
+        /**
+         * @return the retrieved statements.
+         */
+        @NonNull
+        public List<Statement> getStatements();
+
+        /**
+         * @return the expiration time in millisecond.
+         */
+        public long getExpireMillis();
+    }
+
+    /**
+     * Creates a new StatementRetriever that directly retrieves statements from the asset.
+     *
+     * <p> For web assets, {@link AbstractStatementRetriever} will try to retrieve the statement
+     * file from URL: {@code [webAsset.site]/.well-known/associations.json"} where {@code
+     * [webAsset.site]} is in the form {@code http{s}://[hostname]:[optional_port]}. The file
+     * should contain one JSON array of statements.
+     *
+     * <p> For Android assets, {@link AbstractStatementRetriever} will try to retrieve the statement
+     * from the AndroidManifest.xml. The developer should add a {@code meta-data} tag under
+     * {@code application} tag where attribute {@code android:name} equals "associated_assets"
+     * and {@code android:recourse} points to a string array resource. Each entry in the string
+     * array should contain exactly one statement in JSON format. Note that this implementation
+     * can only return statements made by installed apps.
+     */
+    public static AbstractStatementRetriever createDirectRetriever(Context context) {
+        return new DirectStatementRetriever(new URLFetcher(),
+                new AndroidPackageInfoFetcher(context));
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java
new file mode 100644
index 0000000..0c96038
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Immutable value type that names an Android app asset.
+ *
+ * <p>An Android app can be named by its package name and certificate fingerprints using this JSON
+ * string: { "namespace": "android_app", "package_name": "[Java package name]",
+ * "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] }
+ *
+ * <p>For example, { "namespace": "android_app", "package_name": "com.test.mytestapp",
+ * "sha256_cert_fingerprints": ["24:D9:B4:57:A6:42:FB:E6:E5:B8:D6:9E:7B:2D:C2:D1:CB:D1:77:17:1D:7F:D4:A9:16:10:11:AB:92:B9:8F:3F"]
+ * }
+ *
+ * <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
+ * {@code keytool -list -printcert -jarfile signed_app.apk}
+ *
+ * <p>Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...)
+ * representing the certificate SHA-256 fingerprint.
+ */
+/* package private */ final class AndroidAppAsset extends AbstractAsset {
+
+    private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
+    private static final String MISSING_APPCERTS_FORMAT_STRING =
+            "Expected %s to be non-empty array.";
+    private static final String APPCERT_NOT_STRING_FORMAT_STRING = "Expected all %s to be strings.";
+
+    private final List<String> mCertFingerprints;
+    private final String mPackageName;
+
+    public List<String> getCertFingerprints() {
+        return Collections.unmodifiableList(mCertFingerprints);
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    @Override
+    public String toJson() {
+        AssetJsonWriter writer = new AssetJsonWriter();
+
+        writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_ANDROID_APP);
+        writer.writeFieldLower(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName);
+        writer.writeArrayUpper(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints);
+
+        return writer.closeAndGetString();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder asset = new StringBuilder();
+        asset.append("AndroidAppAsset: ");
+        asset.append(toJson());
+        return asset.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof AndroidAppAsset)) {
+            return false;
+        }
+
+        return ((AndroidAppAsset) o).toJson().equals(toJson());
+    }
+
+    @Override
+    public int hashCode() {
+        return toJson().hashCode();
+    }
+
+    @Override
+    public int lookupKey() {
+        return getPackageName().hashCode();
+    }
+
+    /**
+     * Checks that the input is a valid Android app asset.
+     *
+     * @param asset a JSONObject that has "namespace", "package_name", and
+     *              "sha256_cert_fingerprints" fields.
+     * @throws AssociationServiceException if the asset is not well formatted.
+     */
+    public static AndroidAppAsset create(JSONObject asset)
+            throws AssociationServiceException {
+        String packageName = asset.optString(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME);
+        if (packageName.equals("")) {
+            throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
+                    Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME));
+        }
+
+        JSONArray certArray = asset.optJSONArray(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS);
+        if (certArray == null || certArray.length() == 0) {
+            throw new AssociationServiceException(
+                    String.format(MISSING_APPCERTS_FORMAT_STRING,
+                            Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
+        }
+        List<String> certFingerprints = new ArrayList<>(certArray.length());
+        for (int i = 0; i < certArray.length(); i++) {
+            try {
+                certFingerprints.add(certArray.getString(i));
+            } catch (JSONException e) {
+                throw new AssociationServiceException(
+                        String.format(APPCERT_NOT_STRING_FORMAT_STRING,
+                                Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
+            }
+        }
+
+        return new AndroidAppAsset(packageName, certFingerprints);
+    }
+
+    /**
+     * Creates a new AndroidAppAsset.
+     *
+     * @param packageName the package name of the Android app.
+     * @param certFingerprints at least one of the Android app signing certificate sha-256
+     *                         fingerprint.
+     */
+    public static AndroidAppAsset create(String packageName, List<String> certFingerprints) {
+        if (packageName == null || packageName.equals("")) {
+            throw new AssertionError("Expected packageName to be set.");
+        }
+        if (certFingerprints == null || certFingerprints.size() == 0) {
+            throw new AssertionError("Expected certFingerprints to be set.");
+        }
+        List<String> lowerFps = new ArrayList<String>(certFingerprints.size());
+        for (String fp : certFingerprints) {
+            lowerFps.add(fp.toUpperCase(Locale.US));
+        }
+        return new AndroidAppAsset(packageName, lowerFps);
+    }
+
+    private AndroidAppAsset(String packageName, List<String> certFingerprints) {
+        if (packageName.equals("")) {
+            mPackageName = null;
+        } else {
+            mPackageName = packageName;
+        }
+
+        if (certFingerprints == null || certFingerprints.size() == 0) {
+            mCertFingerprints = null;
+        } else {
+            mCertFingerprints = Collections.unmodifiableList(sortAndDeDuplicate(certFingerprints));
+        }
+    }
+
+    /**
+     * Returns an ASCII-sorted copy of the list of certs with all duplicates removed.
+     */
+    private List<String> sortAndDeDuplicate(List<String> certs) {
+        if (certs.size() <= 1) {
+            return certs;
+        }
+        HashSet<String> set = new HashSet<>(certs);
+        List<String> result = new ArrayList<>(set);
+        Collections.sort(result);
+        return result;
+    }
+
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java
new file mode 100644
index 0000000..8a9d838
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Match assets that have the same 'package_name' field and have at least one common certificate
+ * fingerprint in 'sha256_cert_fingerprints' field.
+ */
+/* package private */ final class AndroidAppAssetMatcher extends AbstractAssetMatcher {
+
+    private final AndroidAppAsset mQuery;
+
+    public AndroidAppAssetMatcher(AndroidAppAsset query) {
+        mQuery = query;
+    }
+
+    @Override
+    public boolean matches(AbstractAsset asset) {
+        if (asset instanceof AndroidAppAsset) {
+            AndroidAppAsset androidAppAsset = (AndroidAppAsset) asset;
+            if (!androidAppAsset.getPackageName().equals(mQuery.getPackageName())) {
+                return false;
+            }
+
+            Set<String> certs = new HashSet<String>(mQuery.getCertFingerprints());
+            for (String cert : androidAppAsset.getCertFingerprints()) {
+                if (certs.contains(cert)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public int getMatchedLookupKey() {
+        return mQuery.lookupKey();
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java
new file mode 100644
index 0000000..1000c4c
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources.NotFoundException;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Class that provides information about an android app from {@link PackageManager}.
+ *
+ * Visible for testing.
+ *
+ * @hide
+ */
+public class AndroidPackageInfoFetcher {
+
+    /**
+     * The name of the metadata tag in AndroidManifest.xml that stores the associated asset array
+     * ID. The metadata tag should use the android:resource attribute to point to an array resource
+     * that contains the associated assets.
+     */
+    private static final String ASSOCIATED_ASSETS_KEY = "associated_assets";
+
+    private Context mContext;
+
+    public AndroidPackageInfoFetcher(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Returns the Sha-256 fingerprints of all certificates from the specified package as a list of
+     * upper case HEX Strings with bytes separated by colons. Given an app {@link
+     * android.content.pm.Signature}, the fingerprint can be computed as {@link
+     * Utils#computeNormalizedSha256Fingerprint} {@code(signature.toByteArray())}.
+     *
+     * <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using: {@code
+     * keytool -list -printcert -jarfile signed_app.apk}
+     *
+     * <p>Example: "10:39:38:EE:45:37:E5:9E:8E:E7:92:F6:54:50:4F:B8:34:6F:C6:B3:46:D0:BB:C4:41:5F:C3:39:FC:FC:8E:C1"
+     *
+     * @throws NameNotFoundException if an app with packageName is not installed on the device.
+     */
+    public List<String> getCertFingerprints(String packageName) throws NameNotFoundException {
+        return Utils.getCertFingerprintsFromPackageManager(packageName, mContext);
+    }
+
+    /**
+     * Returns all statements that the specified package makes in its AndroidManifest.xml.
+     *
+     * @throws NameNotFoundException if the app is not installed on the device.
+     */
+    public List<String> getStatements(String packageName) throws NameNotFoundException {
+        PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(
+                packageName, PackageManager.GET_META_DATA);
+        ApplicationInfo appInfo = packageInfo.applicationInfo;
+        if (appInfo.metaData == null) {
+            return Collections.<String>emptyList();
+        }
+        int tokenResourceId = appInfo.metaData.getInt(ASSOCIATED_ASSETS_KEY);
+        if (tokenResourceId == 0) {
+            return Collections.<String>emptyList();
+        }
+        try {
+            return Arrays.asList(
+                    mContext.getPackageManager().getResourcesForApplication(packageName)
+                    .getStringArray(tokenResourceId));
+        } catch (NotFoundException e) {
+            return Collections.<String>emptyList();
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java
new file mode 100644
index 0000000..762365e
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Factory to create asset from JSON string.
+ */
+/* package private */ final class AssetFactory {
+
+    private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string.";
+
+    private AssetFactory() {}
+
+    /**
+     * Creates a new Asset object from its JSON string representation.
+     *
+     * @throws AssociationServiceException if the assetJson is not well formatted.
+     */
+    public static AbstractAsset create(String assetJson) throws AssociationServiceException {
+        try {
+            return create(new JSONObject(assetJson));
+        } catch (JSONException e) {
+            throw new AssociationServiceException(
+                    "Input is not a well formatted asset descriptor.");
+        }
+    }
+
+    /**
+     * Checks that the input is a valid asset with purposes.
+     *
+     * @throws AssociationServiceException if the asset is not well formatted.
+     */
+    private static AbstractAsset create(JSONObject asset)
+            throws AssociationServiceException {
+        String namespace = asset.optString(Utils.NAMESPACE_FIELD, null);
+        if (namespace == null) {
+            throw new AssociationServiceException(String.format(
+                    FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD));
+        }
+
+        if (namespace.equals(Utils.NAMESPACE_WEB)) {
+            return WebAsset.create(asset);
+        } else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) {
+            return AndroidAppAsset.create(asset);
+        } else {
+            throw new AssociationServiceException("Namespace " + namespace + " is not supported.");
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssetJsonWriter.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetJsonWriter.java
new file mode 100644
index 0000000..080e45a
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetJsonWriter.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import android.util.JsonWriter;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Creates a Json string where the order of the fields can be specified.
+ */
+/* package private */ final class AssetJsonWriter {
+
+    private StringWriter mStringWriter = new StringWriter();
+    private JsonWriter mWriter;
+    private boolean mClosed = false;
+
+    public AssetJsonWriter() {
+        mWriter = new JsonWriter(mStringWriter);
+        try {
+            mWriter.beginObject();
+        } catch (IOException e) {
+            throw new AssertionError("Unreachable exception.");
+        }
+    }
+
+    /**
+     * Appends a field to the output, putting both the key and value in lowercase. Null values are
+     * not written.
+     */
+    public void writeFieldLower(String key, String value) {
+        if (mClosed) {
+            throw new IllegalArgumentException(
+                    "Cannot write to an object that has already been closed.");
+        }
+
+        if (value != null) {
+            try {
+                mWriter.name(key.toLowerCase(Locale.US));
+                mWriter.value(value.toLowerCase(Locale.US));
+            } catch (IOException e) {
+                throw new AssertionError("Unreachable exception.");
+            }
+        }
+    }
+
+    /**
+     * Appends an array to the output, putting both the key and values in lowercase. If {@code
+     * values} is null, this field will not be written. Individual values in the list must not be
+     * null.
+     */
+    public void writeArrayUpper(String key, List<String> values) {
+        if (mClosed) {
+            throw new IllegalArgumentException(
+                    "Cannot write to an object that has already been closed.");
+        }
+
+        if (values != null) {
+            try {
+                mWriter.name(key.toLowerCase(Locale.US));
+                mWriter.beginArray();
+                for (String value : values) {
+                    mWriter.value(value.toUpperCase(Locale.US));
+                }
+                mWriter.endArray();
+            } catch (IOException e) {
+                throw new AssertionError("Unreachable exception.");
+            }
+        }
+    }
+
+    /**
+     * Returns the string representation of the constructed json. After calling this method, {@link
+     * #writeFieldLower} can no longer be called.
+     */
+    public String closeAndGetString() {
+        if (!mClosed) {
+            try {
+                mWriter.endObject();
+            } catch (IOException e) {
+                throw new AssertionError("Unreachable exception.");
+            }
+            mClosed = true;
+        }
+        return mStringWriter.toString();
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java
new file mode 100644
index 0000000..1a50757
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Factory to create asset matcher from JSON string.
+ */
+/* package private */ final class AssetMatcherFactory {
+
+    private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string.";
+    private static final String NAMESPACE_NOT_SUPPORTED_STRING = "Namespace %s is not supported.";
+
+    public static AbstractAssetMatcher create(String query) throws AssociationServiceException,
+            JSONException {
+        JSONObject queryObject = new JSONObject(query);
+
+        String namespace = queryObject.optString(Utils.NAMESPACE_FIELD, null);
+        if (namespace == null) {
+            throw new AssociationServiceException(String.format(
+                    FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD));
+        }
+
+        if (namespace.equals(Utils.NAMESPACE_WEB)) {
+            return new WebAssetMatcher(WebAsset.create(queryObject));
+        } else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) {
+            return new AndroidAppAssetMatcher(AndroidAppAsset.create(queryObject));
+        } else {
+            throw new AssociationServiceException(
+                    String.format(NAMESPACE_NOT_SUPPORTED_STRING, namespace));
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssociationServiceException.java b/packages/StatementService/src/com/android/statementservice/retriever/AssociationServiceException.java
new file mode 100644
index 0000000..d6e49c2
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssociationServiceException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+/**
+ * Thrown when an error occurs in the Association Service.
+ */
+public class AssociationServiceException extends Exception {
+
+    public AssociationServiceException(String msg) {
+        super(msg);
+    }
+
+    public AssociationServiceException(String msg, Exception e) {
+        super(msg, e);
+    }
+
+    public AssociationServiceException(Exception e) {
+        super(e);
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java b/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java
new file mode 100644
index 0000000..3ad71c4
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
+
+import org.json.JSONException;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An implementation of {@link AbstractStatementRetriever} that directly retrieves statements from
+ * the asset.
+ */
+/* package private */ final class DirectStatementRetriever extends AbstractStatementRetriever {
+
+    private static final long DO_NOT_CACHE_RESULT = 0L;
+    private static final int HTTP_CONNECTION_TIMEOUT_MILLIS = 5000;
+    private static final long HTTP_CONTENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
+    private static final int MAX_INCLUDE_LEVEL = 1;
+    private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/associations.json";
+
+    private final URLFetcher mUrlFetcher;
+    private final AndroidPackageInfoFetcher mAndroidFetcher;
+
+    /**
+     * An immutable value type representing the retrieved statements and the expiration date.
+     */
+    public static class Result implements AbstractStatementRetriever.Result {
+
+        private final List<Statement> mStatements;
+        private final Long mExpireMillis;
+
+        @Override
+        public List<Statement> getStatements() {
+            return mStatements;
+        }
+
+        @Override
+        public long getExpireMillis() {
+            return mExpireMillis;
+        }
+
+        private Result(List<Statement> statements, Long expireMillis) {
+            mStatements = statements;
+            mExpireMillis = expireMillis;
+        }
+
+        public static Result create(List<Statement> statements, Long expireMillis) {
+            return new Result(statements, expireMillis);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder result = new StringBuilder();
+            result.append("Result: ");
+            result.append(mStatements.toString());
+            result.append(", mExpireMillis=");
+            result.append(mExpireMillis);
+            return result.toString();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            Result result = (Result) o;
+
+            if (!mExpireMillis.equals(result.mExpireMillis)) {
+                return false;
+            }
+            if (!mStatements.equals(result.mStatements)) {
+                return false;
+            }
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = mStatements.hashCode();
+            result = 31 * result + mExpireMillis.hashCode();
+            return result;
+        }
+    }
+
+    public DirectStatementRetriever(URLFetcher urlFetcher,
+                                    AndroidPackageInfoFetcher androidFetcher) {
+        this.mUrlFetcher = urlFetcher;
+        this.mAndroidFetcher = androidFetcher;
+    }
+
+    @Override
+    public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {
+        if (source instanceof AndroidAppAsset) {
+            return retrieveFromAndroid((AndroidAppAsset) source);
+        } else if (source instanceof WebAsset) {
+            return retrieveFromWeb((WebAsset) source);
+        } else {
+            throw new AssociationServiceException("Namespace is not supported.");
+        }
+    }
+
+    private String computeAssociationJsonUrl(WebAsset asset) {
+        try {
+            return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),
+                    WELL_KNOWN_STATEMENT_PATH)
+                    .toExternalForm();
+        } catch (MalformedURLException e) {
+            throw new AssertionError("Invalid domain name in database.");
+        }
+    }
+
+    private Result retrieveStatementFromUrl(String url, int maxIncludeLevel, AbstractAsset source)
+            throws AssociationServiceException {
+        List<Statement> statements = new ArrayList<Statement>();
+        if (maxIncludeLevel < 0) {
+            return Result.create(statements, DO_NOT_CACHE_RESULT);
+        }
+
+        WebContent webContent;
+        try {
+            webContent = mUrlFetcher.getWebContentFromUrl(new URL(url),
+                    HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS);
+        } catch (IOException e) {
+            return Result.create(statements, DO_NOT_CACHE_RESULT);
+        }
+
+        try {
+            ParsedStatement result = StatementParser
+                    .parseStatementList(webContent.getContent(), source);
+            statements.addAll(result.getStatements());
+            for (String delegate : result.getDelegates()) {
+                statements.addAll(
+                        retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
+                                .getStatements());
+            }
+            return Result.create(statements, webContent.getExpireTimeMillis());
+        } catch (JSONException e) {
+            return Result.create(statements, DO_NOT_CACHE_RESULT);
+        }
+    }
+
+    private Result retrieveFromWeb(WebAsset asset)
+            throws AssociationServiceException {
+        return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);
+    }
+
+    private Result retrieveFromAndroid(AndroidAppAsset asset) throws AssociationServiceException {
+        try {
+            List<String> delegates = new ArrayList<String>();
+            List<Statement> statements = new ArrayList<Statement>();
+
+            List<String> certFps = mAndroidFetcher.getCertFingerprints(asset.getPackageName());
+            if (!Utils.hasCommonString(certFps, asset.getCertFingerprints())) {
+                throw new AssociationServiceException(
+                        "Specified certs don't match the installed app.");
+            }
+
+            AndroidAppAsset actualSource = AndroidAppAsset.create(asset.getPackageName(), certFps);
+            for (String statementJson : mAndroidFetcher.getStatements(asset.getPackageName())) {
+                ParsedStatement result =
+                        StatementParser.parseStatement(statementJson, actualSource);
+                statements.addAll(result.getStatements());
+                delegates.addAll(result.getDelegates());
+            }
+
+            for (String delegate : delegates) {
+                statements.addAll(retrieveStatementFromUrl(delegate, MAX_INCLUDE_LEVEL,
+                        actualSource).getStatements());
+            }
+
+            return Result.create(statements, DO_NOT_CACHE_RESULT);
+        } catch (JSONException | NameNotFoundException e) {
+            Log.w(DirectStatementRetriever.class.getSimpleName(), e);
+            return Result.create(Collections.<Statement>emptyList(), DO_NOT_CACHE_RESULT);
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java b/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java
new file mode 100644
index 0000000..9446e66
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import java.util.List;
+
+/**
+ * A class that stores a list of statement and/or a list of delegate url.
+ */
+/* package private */ final class ParsedStatement {
+
+    private final List<Statement> mStatements;
+    private final List<String> mDelegates;
+
+    public ParsedStatement(List<Statement> statements, List<String> delegates) {
+        this.mStatements = statements;
+        this.mDelegates = delegates;
+    }
+
+    public List<Statement> getStatements() {
+        return mStatements;
+    }
+
+    public List<String> getDelegates() {
+        return mDelegates;
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Relation.java b/packages/StatementService/src/com/android/statementservice/retriever/Relation.java
new file mode 100644
index 0000000..91218c6
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/Relation.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import android.annotation.NonNull;
+
+import java.util.regex.Pattern;
+
+/**
+ * An immutable value type representing a statement relation with "kind" and "detail".
+ *
+ * <p> The set of kinds is enumerated by the API: <ul> <li> <b>delegate_permission</b>: The detail
+ * field specifies which permission to delegate. A statement involving this relation does not
+ * constitute a requirement to do the delegation, just a permission to do so. </ul>
+ *
+ * <p> We may add other kinds in the future.
+ *
+ * <p> The detail field is a lowercase alphanumeric string with underscores and periods allowed
+ * (matching the regex [a-z0-9_.]+), but otherwise unstructured. It is also possible to specify '*'
+ * (the wildcard character) as the detail if the relation applies to any detail in the specified
+ * kind.
+ */
+public final class Relation {
+
+    private static final Pattern KIND_PATTERN = Pattern.compile("^[a-z0-9_.]+$");
+    private static final Pattern DETAIL_PATTERN = Pattern.compile("^([a-z0-9_.]+|[*])$");
+
+    private static final String MATCH_ALL_DETAILS = "*";
+
+    private final String mKind;
+    private final String mDetail;
+
+    private Relation(String kind, String detail) {
+        mKind = kind;
+        mDetail = detail;
+    }
+
+    /**
+     * Returns the relation's kind.
+     */
+    @NonNull
+    public String getKind() {
+        return mKind;
+    }
+
+    /**
+     * Returns the relation's detail.
+     */
+    @NonNull
+    public String getDetail() {
+        return mDetail;
+    }
+
+    /**
+     * Creates a new Relation object for the specified {@code kind} and {@code detail}.
+     *
+     * @throws AssociationServiceException if {@code kind} or {@code detail} is not well formatted.
+     */
+    public static Relation create(@NonNull String kind, @NonNull String detail)
+            throws AssociationServiceException {
+        if (!KIND_PATTERN.matcher(kind).matches() || !DETAIL_PATTERN.matcher(detail).matches()) {
+            throw new AssociationServiceException("Relation not well formatted.");
+        }
+        return new Relation(kind, detail);
+    }
+
+    /**
+     * Creates a new Relation object from its string representation.
+     *
+     * @throws AssociationServiceException if the relation is not well formatted.
+     */
+    public static Relation create(@NonNull String relation) throws AssociationServiceException {
+        String[] r = relation.split("/", 2);
+        if (r.length != 2) {
+            throw new AssociationServiceException("Relation not well formatted.");
+        }
+        return create(r[0], r[1]);
+    }
+
+    /**
+     * Returns true if {@code relation} has the same kind and detail. If {@code
+     * relation.getDetail()} is wildcard (*) then returns true if the kind is the same.
+     */
+    public boolean matches(Relation relation) {
+        return getKind().equals(relation.getKind()) && (getDetail().equals(MATCH_ALL_DETAILS)
+                || getDetail().equals(relation.getDetail()));
+    }
+
+    /**
+     * Returns a string representation of this relation.
+     */
+    @Override
+    public String toString() {
+        StringBuilder relation = new StringBuilder();
+        relation.append(getKind());
+        relation.append("/");
+        relation.append(getDetail());
+        return relation.toString();
+    }
+
+    // equals() and hashCode() are generated by Android Studio.
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        Relation relation = (Relation) o;
+
+        if (mDetail != null ? !mDetail.equals(relation.mDetail) : relation.mDetail != null) {
+            return false;
+        }
+        if (mKind != null ? !mKind.equals(relation.mKind) : relation.mKind != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mKind != null ? mKind.hashCode() : 0;
+        result = 31 * result + (mDetail != null ? mDetail.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Statement.java b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java
new file mode 100644
index 0000000..f83edaf
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import android.annotation.NonNull;
+
+/**
+ * An immutable value type representing a statement, consisting of a source, target, and relation.
+ * This reflects an assertion that the relation holds for the source, target pair. For example, if a
+ * web site has the following in its associations.json file:
+ *
+ * <pre>
+ * {
+ * "relation": ["delegate_permission/common.handle_all_urls"],
+ * "target"  : {"namespace": "android_app", "package_name": "com.example.app",
+ *              "sha256_cert_fingerprints": ["00:11:22:33"] }
+ * }
+ * </pre>
+ *
+ * Then invoking {@link AbstractStatementRetriever#retrieveStatements(AbstractAsset)} will return a
+ * {@link Statement} with {@link #getSource} equal to the input parameter, {@link #getRelation}
+ * equal to
+ *
+ * <pre>Relation.create("delegate_permission", "common.get_login_creds");</pre>
+ *
+ * and with {@link #getTarget} equal to
+ *
+ * <pre>AbstractAsset.create("{\"namespace\" : \"android_app\","
+ *                           + "\"package_name\": \"com.example.app\"}"
+ *                           + "\"sha256_cert_fingerprints\": \"[\"00:11:22:33\"]\"}");
+ * </pre>
+ */
+public final class Statement {
+
+    private final AbstractAsset mTarget;
+    private final Relation mRelation;
+    private final AbstractAsset mSource;
+
+    private Statement(AbstractAsset source, AbstractAsset target, Relation relation) {
+        mSource = source;
+        mTarget = target;
+        mRelation = relation;
+    }
+
+    /**
+     * Returns the source asset of the statement.
+     */
+    @NonNull
+    public AbstractAsset getSource() {
+        return mSource;
+    }
+
+    /**
+     * Returns the target asset of the statement.
+     */
+    @NonNull
+    public AbstractAsset getTarget() {
+        return mTarget;
+    }
+
+    /**
+     * Returns the relation of the statement.
+     */
+    @NonNull
+    public Relation getRelation() {
+        return mRelation;
+    }
+
+    /**
+     * Creates a new Statement object for the specified target asset and relation. For example:
+     * <pre>
+     *   Asset asset = Asset.Factory.create(
+     *       "{\"namespace\" : \"web\",\"site\": \"https://www.test.com\"}");
+     *   Relation relation = Relation.create("delegate_permission", "common.get_login_creds");
+     *   Statement statement = Statement.create(asset, relation);
+     * </pre>
+     */
+    public static Statement create(@NonNull AbstractAsset source, @NonNull AbstractAsset target,
+                                   @NonNull Relation relation) {
+        return new Statement(source, target, relation);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        Statement statement = (Statement) o;
+
+        if (!mRelation.equals(statement.mRelation)) {
+            return false;
+        }
+        if (!mTarget.equals(statement.mTarget)) {
+            return false;
+        }
+        if (!mSource.equals(statement.mSource)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mTarget.hashCode();
+        result = 31 * result + mRelation.hashCode();
+        result = 31 * result + mSource.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder statement = new StringBuilder();
+        statement.append("Statement: ");
+        statement.append(mSource);
+        statement.append(", ");
+        statement.append(mTarget);
+        statement.append(", ");
+        statement.append(mRelation);
+        return statement.toString();
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java b/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java
new file mode 100644
index 0000000..bcd91bd
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class that parses JSON-formatted statements.
+ */
+/* package private */ final class StatementParser {
+
+    /**
+     * Parses a JSON array of statements.
+     */
+    static ParsedStatement parseStatementList(String statementList, AbstractAsset source)
+            throws JSONException, AssociationServiceException {
+        List<Statement> statements = new ArrayList<Statement>();
+        List<String> delegates = new ArrayList<String>();
+
+        JSONArray statementsJson = new JSONArray(statementList);
+        for (int i = 0; i < statementsJson.length(); i++) {
+            ParsedStatement result = parseStatement(statementsJson.getString(i), source);
+            statements.addAll(result.getStatements());
+            delegates.addAll(result.getDelegates());
+        }
+
+        return new ParsedStatement(statements, delegates);
+    }
+
+    /**
+     * Parses a single JSON statement.
+     */
+    static ParsedStatement parseStatement(String statementString, AbstractAsset source)
+            throws JSONException, AssociationServiceException {
+        List<Statement> statements = new ArrayList<Statement>();
+        List<String> delegates = new ArrayList<String>();
+        JSONObject statement = new JSONObject(statementString);
+        if (statement.optString(Utils.DELEGATE_FIELD_DELEGATE, null) != null) {
+            delegates.add(statement.optString(Utils.DELEGATE_FIELD_DELEGATE));
+        } else {
+            AbstractAsset target = AssetFactory
+                    .create(statement.getString(Utils.ASSET_DESCRIPTOR_FIELD_TARGET));
+            JSONArray relations = statement.getJSONArray(
+                    Utils.ASSET_DESCRIPTOR_FIELD_RELATION);
+            for (int i = 0; i < relations.length(); i++) {
+                statements.add(Statement
+                        .create(source, target, Relation.create(relations.getString(i))));
+            }
+        }
+
+        return new ParsedStatement(statements, delegates);
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java b/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java
new file mode 100644
index 0000000..4828ff9
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import com.android.volley.Cache;
+import com.android.volley.NetworkResponse;
+import com.android.volley.toolbox.HttpHeaderParser;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Helper class for fetching HTTP or HTTPS URL.
+ *
+ * Visible for testing.
+ *
+ * @hide
+ */
+public class URLFetcher {
+
+    private static final long DO_NOT_CACHE_RESULT = 0L;
+    private static final int INPUT_BUFFER_SIZE_IN_BYTES = 1024;
+
+    /**
+     * Fetches the specified url and returns the content and ttl.
+     *
+     * @throws IOException if it can't retrieve the content due to a network problem.
+     * @throws AssociationServiceException if the URL scheme is not http or https or the content
+     * length exceeds {code fileSizeLimit}.
+     */
+    public WebContent getWebContentFromUrl(URL url, long fileSizeLimit, int connectionTimeoutMillis)
+            throws AssociationServiceException, IOException {
+        final String scheme = url.getProtocol().toLowerCase(Locale.US);
+        if (!scheme.equals("http") && !scheme.equals("https")) {
+            throw new IllegalArgumentException("The url protocol should be on http or https.");
+        }
+
+        HttpURLConnection connection;
+        connection = (HttpURLConnection) url.openConnection();
+        connection.setInstanceFollowRedirects(true);
+        connection.setConnectTimeout(connectionTimeoutMillis);
+        connection.setReadTimeout(connectionTimeoutMillis);
+        connection.setUseCaches(true);
+        connection.addRequestProperty("Cache-Control", "max-stale=60");
+
+        if (connection.getContentLength() > fileSizeLimit) {
+            throw new AssociationServiceException("The content size of the url is larger than "
+                    + fileSizeLimit);
+        }
+
+        Long expireTimeMillis = getExpirationTimeMillisFromHTTPHeader(connection.getHeaderFields());
+
+        try {
+            return new WebContent(inputStreamToString(
+                    connection.getInputStream(), connection.getContentLength(), fileSizeLimit),
+                expireTimeMillis);
+        } finally {
+            connection.disconnect();
+        }
+    }
+
+    /**
+     * Visible for testing.
+     * @hide
+     */
+    public static String inputStreamToString(InputStream inputStream, int length, long sizeLimit)
+            throws IOException, AssociationServiceException {
+        if (length < 0) {
+            length = 0;
+        }
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(length);
+        BufferedInputStream bis = new BufferedInputStream(inputStream);
+        byte[] buffer = new byte[INPUT_BUFFER_SIZE_IN_BYTES];
+        int len = 0;
+        while ((len = bis.read(buffer)) != -1) {
+            baos.write(buffer, 0, len);
+            if (baos.size() > sizeLimit) {
+                throw new AssociationServiceException("The content size of the url is larger than "
+                        + sizeLimit);
+            }
+        }
+        return baos.toString("UTF-8");
+    }
+
+    /**
+     * Parses the HTTP headers to compute the ttl.
+     *
+     * @param headers a map that map the header key to the header values. Can be null.
+     * @return the ttl in millisecond or null if the ttl is not specified in the header.
+     */
+    private Long getExpirationTimeMillisFromHTTPHeader(Map<String, List<String>> headers) {
+        if (headers == null) {
+            return null;
+        }
+        Map<String, String> joinedHeaders = joinHttpHeaders(headers);
+
+        NetworkResponse response = new NetworkResponse(null, joinedHeaders);
+        Cache.Entry cachePolicy = HttpHeaderParser.parseCacheHeaders(response);
+
+        if (cachePolicy == null) {
+            // Cache is disabled, set the expire time to 0.
+            return DO_NOT_CACHE_RESULT;
+        } else if (cachePolicy.ttl == 0) {
+            // Cache policy is not specified, set the expire time to 0.
+            return DO_NOT_CACHE_RESULT;
+        } else {
+            // cachePolicy.ttl is actually the expire timestamp in millisecond.
+            return cachePolicy.ttl;
+        }
+    }
+
+    /**
+     * Converts an HTTP header map of the format provided by {@linkHttpUrlConnection} to a map of
+     * the format accepted by {@link HttpHeaderParser}. It does this by joining all the entries for
+     * a given header key with ", ".
+     */
+    private Map<String, String> joinHttpHeaders(Map<String, List<String>> headers) {
+        Map<String, String> joinedHeaders = new HashMap<String, String>();
+        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+            List<String> values = entry.getValue();
+            if (values.size() == 1) {
+                joinedHeaders.put(entry.getKey(), values.get(0));
+            } else {
+                joinedHeaders.put(entry.getKey(), Utils.joinStrings(", ", values));
+            }
+        }
+        return joinedHeaders;
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Utils.java b/packages/StatementService/src/com/android/statementservice/retriever/Utils.java
new file mode 100644
index 0000000..44af864
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/Utils.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.Signature;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Utility library for computing certificate fingerprints. Also includes fields name used by
+ * Statement JSON string.
+ */
+public final class Utils {
+
+    private Utils() {}
+
+    /**
+     * Field name for namespace.
+     */
+    public static final String NAMESPACE_FIELD = "namespace";
+
+    /**
+     * Supported asset namespaces.
+     */
+    public static final String NAMESPACE_WEB = "web";
+    public static final String NAMESPACE_ANDROID_APP = "android_app";
+
+    /**
+     * Field names in a web asset descriptor.
+     */
+    public static final String WEB_ASSET_FIELD_SITE = "site";
+
+    /**
+     * Field names in a Android app asset descriptor.
+     */
+    public static final String ANDROID_APP_ASSET_FIELD_PACKAGE_NAME = "package_name";
+    public static final String ANDROID_APP_ASSET_FIELD_CERT_FPS = "sha256_cert_fingerprints";
+
+    /**
+     * Field names in a statement.
+     */
+    public static final String ASSET_DESCRIPTOR_FIELD_RELATION = "relation";
+    public static final String ASSET_DESCRIPTOR_FIELD_TARGET = "target";
+    public static final String DELEGATE_FIELD_DELEGATE = "delegate";
+
+    private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+            'A', 'B', 'C', 'D', 'E', 'F' };
+
+    /**
+     * Joins a list of strings, by placing separator between each string. For example,
+     * {@code joinStrings("; ", Arrays.asList(new String[]{"a", "b", "c"}))} returns
+     * "{@code a; b; c}".
+     */
+    public static String joinStrings(String separator, List<String> strings) {
+        switch(strings.size()) {
+            case 0:
+                return "";
+            case 1:
+                return strings.get(0);
+            default:
+                StringBuilder joiner = new StringBuilder();
+                boolean first = true;
+                for (String field : strings) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        joiner.append(separator);
+                    }
+                    joiner.append(field);
+                }
+                return joiner.toString();
+        }
+    }
+
+    /**
+     * Returns the normalized sha-256 fingerprints of a given package according to the Android
+     * package manager.
+     */
+    public static List<String> getCertFingerprintsFromPackageManager(String packageName,
+            Context context) throws NameNotFoundException {
+        Signature[] signatures = context.getPackageManager().getPackageInfo(packageName,
+                PackageManager.GET_SIGNATURES).signatures;
+        ArrayList<String> result = new ArrayList<String>(signatures.length);
+        for (Signature sig : signatures) {
+            result.add(computeNormalizedSha256Fingerprint(sig.toByteArray()));
+        }
+        return result;
+    }
+
+    /**
+     * Computes the hash of the byte array using the specified algorithm, returning a hex string
+     * with a colon between each byte.
+     */
+    public static String computeNormalizedSha256Fingerprint(byte[] signature) {
+        MessageDigest digester;
+        try {
+            digester = MessageDigest.getInstance("SHA-256");
+        } catch (NoSuchAlgorithmException e) {
+            throw new AssertionError("No SHA-256 implementation found.");
+        }
+        digester.update(signature);
+        return byteArrayToHexString(digester.digest());
+    }
+
+    /**
+     * Returns true if there is at least one common string between the two lists of string.
+     */
+    public static boolean hasCommonString(List<String> list1, List<String> list2) {
+        HashSet<String> set2 = new HashSet<>(list2);
+        for (String string : list1) {
+            if (set2.contains(string)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Converts the byte array to an lowercase hexadecimal digits String with a colon character (:)
+     * between each byte.
+     */
+    private static String byteArrayToHexString(byte[] array) {
+        if (array.length == 0) {
+          return "";
+        }
+        char[] buf = new char[array.length * 3 - 1];
+
+        int bufIndex = 0;
+        for (int i = 0; i < array.length; i++) {
+            byte b = array[i];
+            if (i > 0) {
+                buf[bufIndex++] = ':';
+            }
+            buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
+            buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
+        }
+        return new String(buf);
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java
new file mode 100644
index 0000000..ca9e62d
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+import org.json.JSONObject;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Locale;
+
+/**
+ * Immutable value type that names a web asset.
+ *
+ * <p>A web asset can be named by its protocol, domain, and port using this JSON string:
+ *     { "namespace": "web",
+ *       "site": "[protocol]://[fully-qualified domain]{:[optional port]}" }
+ *
+ * <p>For example, a website hosted on a https server at www.test.com can be named using
+ *     { "namespace": "web",
+ *       "site": "https://www.test.com" }
+ *
+ * <p>The only protocol supported now are https and http. If the optional port is not specified,
+ * the default for each protocol will be used (i.e. 80 for http and 443 for https).
+ */
+/* package private */ final class WebAsset extends AbstractAsset {
+
+    private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
+
+    private final URL mUrl;
+
+    private WebAsset(URL url) {
+        int port = url.getPort() != -1 ? url.getPort() : url.getDefaultPort();
+        try {
+            mUrl = new URL(url.getProtocol().toLowerCase(), url.getHost().toLowerCase(), port, "");
+        } catch (MalformedURLException e) {
+            throw new AssertionError(
+                    "Url should always be validated before calling the constructor.");
+        }
+    }
+
+    public String getDomain() {
+        return mUrl.getHost();
+    }
+
+    public String getPath() {
+        return mUrl.getPath();
+    }
+
+    public String getScheme() {
+        return mUrl.getProtocol();
+    }
+
+    public int getPort() {
+        return mUrl.getPort();
+    }
+
+    @Override
+    public String toJson() {
+        AssetJsonWriter writer = new AssetJsonWriter();
+
+        writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_WEB);
+        writer.writeFieldLower(Utils.WEB_ASSET_FIELD_SITE, mUrl.toExternalForm());
+
+        return writer.closeAndGetString();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder asset = new StringBuilder();
+        asset.append("WebAsset: ");
+        asset.append(toJson());
+        return asset.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof WebAsset)) {
+            return false;
+        }
+
+        return ((WebAsset) o).toJson().equals(toJson());
+    }
+
+    @Override
+    public int hashCode() {
+        return toJson().hashCode();
+    }
+
+    @Override
+    public int lookupKey() {
+        return toJson().hashCode();
+    }
+
+    /**
+     * Checks that the input is a valid web asset.
+     *
+     * @throws AssociationServiceException if the asset is not well formatted.
+     */
+    protected static WebAsset create(JSONObject asset)
+            throws AssociationServiceException {
+        if (asset.optString(Utils.WEB_ASSET_FIELD_SITE).equals("")) {
+            throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
+                    Utils.WEB_ASSET_FIELD_SITE));
+        }
+
+        URL url;
+        try {
+            url = new URL(asset.optString(Utils.WEB_ASSET_FIELD_SITE));
+        } catch (MalformedURLException e) {
+            throw new AssociationServiceException("Url is not well formatted.", e);
+        }
+
+        String scheme = url.getProtocol().toLowerCase(Locale.US);
+        if (!scheme.equals("https") && !scheme.equals("http")) {
+            throw new AssociationServiceException("Expected scheme to be http or https.");
+        }
+
+        if (url.getUserInfo() != null) {
+            throw new AssociationServiceException("The url should not contain user info.");
+        }
+
+        String path = url.getFile(); // This is url.getPath() + url.getQuery().
+        if (!path.equals("/") && !path.equals("")) {
+            throw new AssociationServiceException(
+                    "Site should only have scheme, domain, and port.");
+        }
+
+        return new WebAsset(url);
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/WebAssetMatcher.java b/packages/StatementService/src/com/android/statementservice/retriever/WebAssetMatcher.java
new file mode 100644
index 0000000..8a1078b
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/WebAssetMatcher.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+/**
+ * Match assets that have the same 'site' field.
+ */
+/* package private */ final class WebAssetMatcher extends AbstractAssetMatcher {
+
+    private final WebAsset mQuery;
+
+    public WebAssetMatcher(WebAsset query) {
+        mQuery = query;
+    }
+
+    @Override
+    public boolean matches(AbstractAsset asset) {
+        if (asset instanceof WebAsset) {
+            WebAsset webAsset = (WebAsset) asset;
+            return webAsset.toJson().equals(mQuery.toJson());
+        }
+        return false;
+    }
+
+    @Override
+    public int getMatchedLookupKey() {
+        return mQuery.lookupKey();
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java b/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java
new file mode 100644
index 0000000..86a635c
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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 com.android.statementservice.retriever;
+
+/**
+ * An immutable value type representing the response from a web server.
+ *
+ * Visible for testing.
+ *
+ * @hide
+ */
+public final class WebContent {
+
+    private final String mContent;
+    private final Long mExpireTimeMillis;
+
+    public WebContent(String content, Long expireTimeMillis) {
+        mContent = content;
+        mExpireTimeMillis = expireTimeMillis;
+    }
+
+    /**
+     * Returns the expiration time of the content as specified in the HTTP header.
+     */
+    public Long getExpireTimeMillis() {
+        return mExpireTimeMillis;
+    }
+
+    /**
+     * Returns content of the HTTP message body.
+     */
+    public String getContent() {
+        return mContent;
+    }
+}