Update com.android.statementservice to support v2 API

Brings AOSP up to a true representative implementation which can be
shipped on production devices.

Bug: 171219506

Test: manual, push AOSP, pm enable, pm verify-app-links

Change-Id: I5de6405afe884a19d35d09b266457c4ad4eee91b
diff --git a/packages/StatementService/Android.bp b/packages/StatementService/Android.bp
index 32defc8..a0d8ac9 100644
--- a/packages/StatementService/Android.bp
+++ b/packages/StatementService/Android.bp
@@ -22,17 +22,24 @@
 
 android_app {
     name: "StatementService",
-    defaults: ["platform_app_defaults"],
-    srcs: ["src/**/*.java"],
+    // Removed because Errorprone doesn't work with Kotlin, can fix up in the future
+    // defaults: ["platform_app_defaults"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
+    target_sdk_version: "29",
     platform_apis: true,
     privileged: true,
-    libs: ["org.apache.http.legacy"],
-    uses_libs: ["org.apache.http.legacy"],
+    certificate: "platform",
     static_libs: [
-        "libprotobuf-java-nano",
-        "volley",
+        "androidx.appcompat_appcompat",
+        "androidx.collection_collection-ktx",
+        "androidx.work_work-runtime",
+        "androidx.work_work-runtime-ktx",
+        "kotlinx-coroutines-android",
     ],
 }
diff --git a/packages/StatementService/AndroidManifest.xml b/packages/StatementService/AndroidManifest.xml
index e0abd50..42cd143 100644
--- a/packages/StatementService/AndroidManifest.xml
+++ b/packages/StatementService/AndroidManifest.xml
@@ -14,41 +14,62 @@
      limitations under the License.
 -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.statementservice"
-        android:versionCode="1"
-        android:versionName="1.0">
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.android.statementservice"
+    android:versionCode="1"
+    android:versionName="1.0">
 
-    <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.DOMAIN_VERIFICATION_AGENT"/>
     <uses-permission android:name="android.permission.INTENT_FILTER_VERIFICATION_AGENT"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+    <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+    <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION"/>
 
     <application
-            android:label="@string/service_name"
-            android:allowBackup="false">
-        <uses-library android:name="org.apache.http.legacy" />
-        <service
-                android:name=".DirectStatementService"
-                android:exported="false">
-            <intent-filter>
-                <category android:name="android.intent.category.DEFAULT"/>
-                <action android:name="com.android.statementservice.aosp.service.CHECK_ACTION"/>
-            </intent-filter>
-        </service>
+        android:label="@string/service_name"
+        android:allowBackup="false"
+        android:name=".StatementServiceApplication"
+        >
 
         <receiver
-                android:name=".IntentFilterVerificationReceiver"
-                android:permission="android.permission.BIND_INTENT_FILTER_VERIFIER"
-                android:exported="true">
-            <!-- Set the priority 1 so newer implementation can have higher priority. -->
-            <intent-filter
-                    android:priority="1">
+            android:name=".domain.BootCompletedReceiver"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED"/>
+            </intent-filter>
+        </receiver>
+
+        <receiver
+            android:name=".domain.DomainVerificationReceiverV1"
+            android:permission="android.permission.BIND_INTENT_FILTER_VERIFIER"
+            android:exported="true"
+            >
+            <intent-filter android:priority="1">
                 <action android:name="android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION"/>
                 <data android:mimeType="application/vnd.android.package-archive"/>
             </intent-filter>
         </receiver>
 
+        <!--
+            v2 receiver remains disabled assuming the device ships its own updated version.
+            If necessary, this can be enabled using shell.
+        -->
+        <receiver
+            android:name=".domain.DomainVerificationReceiverV2"
+            android:permission="android.permission.BIND_DOMAIN_VERIFICATION_AGENT"
+            android:directBootAware="true"
+            android:exported="true"
+            android:enabled="false"
+            >
+            <intent-filter android:priority="1">
+                <action android:name="android.intent.action.DOMAINS_NEED_VERIFICATION"/>
+            </intent-filter>
+        </receiver>
     </application>
 
 </manifest>
diff --git a/packages/StatementService/src/com/android/statementservice/DirectStatementService.java b/packages/StatementService/src/com/android/statementservice/DirectStatementService.java
deleted file mode 100644
index 659696e..0000000
--- a/packages/StatementService/src/com/android/statementservice/DirectStatementService.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * 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();
-        final HttpResponseCache responseCache = mHttpResponseCache;
-        mHandler.post(new Runnable() {
-            public void run() {
-                try {
-                    if (responseCache != null) {
-                        responseCache.delete();
-                    }
-                } catch (IOException e) {
-                    Log.i(TAG, "HTTP(S) response cache deletion failed:" + e);
-                }
-                Looper.myLooper().quit();
-            }
-        });
-        mHttpResponseCache = null;
-    }
-
-    @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
deleted file mode 100644
index 20c7f97..0000000
--- a/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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
deleted file mode 100644
index ba8e7a1..0000000
--- a/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * 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.text.TextUtils;
-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);
-
-                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;
-                }
-
-                ArrayList<String> finalHosts = new ArrayList<String>(hostList.length);
-                try {
-                    ArrayList<String> sourceAssets = new ArrayList<String>();
-                    for (String host : hostList) {
-                        // "*.example.tld" is validated via https://example.tld
-                        if (host.startsWith("*.")) {
-                            host = host.substring(2);
-                        }
-                        sourceAssets.add(createWebAssetString(scheme, host));
-                        finalHosts.add(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));
-
-                // Required for CTS: log a few details of the validcation operation to be performed
-                logValidationParametersForCTS(verificationId, scheme, finalHosts, packageName);
-
-                serviceIntent.putExtras(extras);
-                context.startService(serviceIntent);
-            }
-        } else {
-            Log.w(TAG, "Intent action not supported: " + action);
-        }
-    }
-
-    // CTS requirement: logging of the validation parameters in a specific format
-    private static final String CTS_LOG_FORMAT =
-            "Verifying IntentFilter. verificationId:%d scheme:\"%s\" hosts:\"%s\" package:\"%s\".";
-    private void logValidationParametersForCTS(int verificationId, String scheme,
-            ArrayList<String> finalHosts, String packageName) {
-        String hostString = TextUtils.join(" ", finalHosts.toArray());
-        Log.i(TAG, String.format(CTS_LOG_FORMAT, verificationId, scheme, hostString, packageName));
-    }
-
-    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/StatementServiceApplication.kt b/packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt
new file mode 100644
index 0000000..021a514
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/StatementServiceApplication.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 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.Application
+import android.os.UserManager
+import androidx.work.WorkManager
+import com.android.statementservice.domain.DomainVerificationUtils
+
+class StatementServiceApplication : Application() {
+
+    override fun onCreate() {
+        super.onCreate()
+        val userManager = getSystemService(UserManager::class.java) ?: return
+        if (userManager.isUserUnlocked) {
+            // WorkManager can only schedule when the user data directories are unencrypted (after
+            // the user has entered their lock password.
+            DomainVerificationUtils.schedulePeriodicCheckUnlocked(WorkManager.getInstance(this))
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/BaseDomainVerificationReceiver.kt b/packages/StatementService/src/com/android/statementservice/domain/BaseDomainVerificationReceiver.kt
new file mode 100644
index 0000000..de41486
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/BaseDomainVerificationReceiver.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.domain
+
+import android.content.BroadcastReceiver
+import android.util.Log
+import androidx.work.Constraints
+import androidx.work.NetworkType
+
+abstract class BaseDomainVerificationReceiver : BroadcastReceiver() {
+
+    companion object {
+        const val DEBUG = false
+    }
+
+    protected abstract val tag: String
+
+    protected val networkConstraints = Constraints.Builder()
+        .setRequiredNetworkType(NetworkType.CONNECTED)
+        .build()
+
+    protected fun debugLog(block: () -> String) {
+        if (DEBUG) {
+            Log.d(tag, block())
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/BootCompletedReceiver.kt b/packages/StatementService/src/com/android/statementservice/domain/BootCompletedReceiver.kt
new file mode 100644
index 0000000..7b5da83
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/BootCompletedReceiver.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 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.domain
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.work.Constraints
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import com.android.statementservice.domain.worker.RetryRequestWorker
+
+/**
+ * Handles [Intent.ACTION_BOOT_COMPLETED] to schedule recurring maintenance [WorkManager] tasks and
+ * run a one-time retry request to attempt to verify domains that may have failed or been added
+ * since last device reboot.
+ *
+ * Note that this requires the user to have unlocked the device, since [WorkManager] cannot handle
+ * the encrypted user data directories.
+ */
+class BootCompletedReceiver : BroadcastReceiver() {
+
+    companion object {
+        private const val PACKAGE_BOOT_REQUEST_KEY = "package_boot_request"
+    }
+
+    override fun onReceive(context: Context, intent: Intent) {
+        if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
+        val workManager = WorkManager.getInstance(context)
+        DomainVerificationUtils.schedulePeriodicCheckUnlocked(workManager)
+        workManager.beginUniqueWork(
+            PACKAGE_BOOT_REQUEST_KEY,
+            ExistingWorkPolicy.REPLACE,
+            OneTimeWorkRequestBuilder<RetryRequestWorker>()
+                .setConstraints(
+                    Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.CONNECTED)
+                        .build()
+                )
+                .build()
+        ).enqueue()
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt
new file mode 100644
index 0000000..0ec8ed3
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 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.domain
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import androidx.work.ExistingWorkPolicy
+import androidx.work.WorkManager
+import com.android.statementservice.domain.worker.CollectV1Worker
+import com.android.statementservice.domain.worker.SingleV1RequestWorker
+
+/**
+ * Receiver for V1 API. Separated so that the receiver permission can be declared for only the
+ * v1 and v2 permissions individually, exactly matching the intended usage.
+ */
+class DomainVerificationReceiverV1 : BaseDomainVerificationReceiver() {
+
+    companion object {
+        private const val ENABLE_V1 = true
+        private const val PACKAGE_WORK_PREFIX_V1 = "package_request_v1-"
+    }
+
+    override val tag = DomainVerificationReceiverV1::class.java.simpleName
+
+    override fun onReceive(context: Context, intent: Intent) {
+        when (intent.action) {
+            Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION ->
+                scheduleUnlockedV1(context, intent)
+            else -> debugLog { "Received invalid broadcast: $intent" }
+        }
+    }
+
+    private fun scheduleUnlockedV1(context: Context, intent: Intent) {
+        if (!ENABLE_V1) {
+            return
+        }
+
+        val verificationId =
+            intent.getIntExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID, -1)
+        val hosts =
+            (intent.getStringExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS) ?: return)
+                .split(" ")
+        val packageName =
+            intent.getStringExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME)
+                ?: return
+
+        debugLog { "Attempting v1 verification for $packageName" }
+
+        val workRequests = hosts.map {
+            SingleV1RequestWorker.buildRequest(packageName, it) {
+                setConstraints(networkConstraints)
+            }
+        }
+
+        WorkManager.getInstance(context)
+            .beginUniqueWork(
+                "$PACKAGE_WORK_PREFIX_V1$packageName",
+                ExistingWorkPolicy.REPLACE,
+                workRequests
+            )
+            .then(CollectV1Worker.buildRequest(verificationId, packageName))
+            .enqueue()
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV2.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV2.kt
new file mode 100644
index 0000000..24e0f50
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV2.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 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.domain
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.content.pm.verify.domain.DomainVerificationRequest
+import android.os.UserManager
+import androidx.work.BackoffPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.WorkManager
+import com.android.statementservice.domain.worker.SingleV2RequestWorker
+import com.android.statementservice.utils.component1
+import com.android.statementservice.utils.component2
+import com.android.statementservice.utils.component3
+
+import java.time.Duration
+
+/**
+ * Handles [DomainVerificationRequest]s from the system, which indicates a package on the device
+ * has domains which require verification against a server side assetlinks.json file, allowing the
+ * app to resolve web [Intent]s.
+ *
+ * This will delegate to v1 or v2 depending on the received broadcast and which components are
+ * enabled. See [DomainVerificationManager] for the full API.
+ */
+open class DomainVerificationReceiverV2 : BaseDomainVerificationReceiver() {
+
+    companion object {
+
+        private const val ENABLE_V2 = true
+
+        /**
+         * Toggle to always re-verify packages that this receiver is notified of. This means on
+         * every package change, even previously successful requests are re-sent. Generally only
+         * for debugging.
+         */
+        @Suppress("SimplifyBooleanWithConstants")
+        private const val ALWAYS_VERIFY = false || DEBUG
+
+        private const val PACKAGE_WORK_PREFIX_V2 = "package_request_v2-"
+    }
+
+    override val tag = DomainVerificationReceiverV2::class.java.simpleName
+
+    override fun onReceive(context: Context, intent: Intent) {
+        when (intent.action) {
+            Intent.ACTION_DOMAINS_NEED_VERIFICATION -> {
+                // If the user isn't unlocked yet, the request will be ignored, as WorkManager
+                // cannot schedule workers when the user data directories are encrypted.
+                if (context.getSystemService(UserManager::class.java)?.isUserUnlocked == true) {
+                    scheduleUnlockedV2(context, intent)
+                }
+            }
+            else -> debugLog { "Received invalid broadcast: $intent" }
+        }
+    }
+
+    private fun scheduleUnlockedV2(context: Context, intent: Intent) {
+        if (!ENABLE_V2) {
+            return
+        }
+
+        val manager = context.getSystemService(DomainVerificationManager::class.java) ?: return
+        val workManager = WorkManager.getInstance(context)
+
+        val request = intent.getParcelableExtra<DomainVerificationRequest>(
+            DomainVerificationManager.EXTRA_VERIFICATION_REQUEST
+        ) ?: return
+
+        debugLog { "Attempting v2 verification for ${request.packageNames}" }
+
+        request.packageNames.forEach { packageName ->
+            val (domainSetId, _, hostToStateMap) = manager.getDomainVerificationInfo(packageName)
+                ?: return@forEach
+
+            val workRequests = hostToStateMap
+                .filterValues {
+                    // TODO(b/159952358): Should we support re-query? There's no good way to
+                    //  signal to an AOSP implementation from an entity's website about when
+                    //  to re-query, unless it's just done on each update.
+                    // AOSP implementation does not support re-query
+                    ALWAYS_VERIFY || VerifyStatus.shouldRetry(it)
+                }
+                .map { (host, _) ->
+                    SingleV2RequestWorker.buildRequest(domainSetId, packageName, host) {
+                        setConstraints(networkConstraints)
+                        setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofHours(1))
+                    }
+                }
+
+            if (workRequests.isNotEmpty()) {
+                workManager.beginUniqueWork(
+                    "$PACKAGE_WORK_PREFIX_V2$packageName",
+                    ExistingWorkPolicy.REPLACE, workRequests
+                )
+                    .enqueue()
+            }
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt
new file mode 100644
index 0000000..6944248
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationUtils.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 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.domain
+
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import com.android.statementservice.domain.worker.RetryRequestWorker
+import java.time.Duration
+
+object DomainVerificationUtils {
+
+    private const val PERIODIC_SHORT_ID = "retry_short"
+    private const val PERIODIC_SHORT_HOURS = 24L
+    private const val PERIODIC_LONG_ID = "retry_long"
+    private const val PERIODIC_LONG_HOURS = 72L
+
+    /**
+     * In a majority of cases, the initial requests will be enough to verify domains, since they
+     * are also restricted to [NetworkType.CONNECTED], but for cases where they aren't sufficient,
+     * attempts are also made on a periodic basis.
+     *
+     * Once per 24 hours, a check of all packages is done with [NetworkType.CONNECTED]. To avoid
+     * cases where a proxy or other unusual device configuration prevents [WorkManager] from
+     * running, also schedule a 3 day task without constraints which will force the check to run.
+     *
+     * The actual logic may be skipped if a request was previously run successfully or there are no
+     * more domains that need verifying.
+     */
+    fun schedulePeriodicCheckUnlocked(workManager: WorkManager) {
+        workManager.apply {
+            PeriodicWorkRequestBuilder<RetryRequestWorker>(Duration.ofHours(PERIODIC_SHORT_HOURS))
+                .setConstraints(
+                    Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.CONNECTED)
+                        .setRequiresDeviceIdle(true)
+                        .build()
+                )
+                .build()
+                .let {
+                    enqueueUniquePeriodicWork(
+                        PERIODIC_SHORT_ID,
+                        ExistingPeriodicWorkPolicy.KEEP, it
+                    )
+                }
+            PeriodicWorkRequestBuilder<RetryRequestWorker>(Duration.ofDays(PERIODIC_LONG_HOURS))
+                .setConstraints(
+                    Constraints.Builder()
+                        .setRequiresDeviceIdle(true)
+                        .build()
+                )
+                .build()
+                .let {
+                    enqueueUniquePeriodicWork(
+                        PERIODIC_LONG_ID,
+                        ExistingPeriodicWorkPolicy.KEEP, it
+                    )
+                }
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt
new file mode 100644
index 0000000..29f844f
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2020 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.domain
+
+import android.content.Context
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.net.Network
+import android.util.Log
+import androidx.collection.LruCache
+import com.android.statementservice.network.retriever.StatementRetriever
+import com.android.statementservice.retriever.AbstractAsset
+import com.android.statementservice.retriever.AbstractAssetMatcher
+import com.android.statementservice.utils.Result
+import com.android.statementservice.utils.StatementUtils
+import com.android.statementservice.utils.component1
+import com.android.statementservice.utils.component2
+import com.android.statementservice.utils.component3
+import java.net.HttpURLConnection
+import java.util.Optional
+import java.util.UUID
+
+private typealias WorkResult = androidx.work.ListenableWorker.Result
+
+class DomainVerifier private constructor(
+    private val appContext: Context,
+    private val manager: DomainVerificationManager
+) {
+    companion object {
+        private val TAG = DomainVerifier::class.java.simpleName
+        private const val DEBUG = false
+
+        private var singleton: DomainVerifier? = null
+
+        fun getInstance(context: Context) = when {
+            singleton != null -> singleton!!
+            else -> synchronized(this) {
+                if (singleton == null) {
+                    val appContext = context.applicationContext
+                    val manager =
+                        appContext.getSystemService(DomainVerificationManager::class.java)!!
+                    singleton = DomainVerifier(appContext, manager)
+                }
+                singleton!!
+            }
+        }
+    }
+
+    private val retriever = StatementRetriever()
+
+    private val targetAssetCache = AssetLruCache()
+
+    fun collectHosts(packageNames: Iterable<String>): Iterable<Triple<UUID, String, String>> {
+        return packageNames.mapNotNull { packageName ->
+            val (domainSetId, _, hostToStateMap) = try {
+                manager.getDomainVerificationInfo(packageName)
+            } catch (ignored: Exception) {
+                // Package disappeared, assume it will be rescheduled if the package reappears
+                null
+            } ?: return@mapNotNull null
+
+            val hostsToRetry = hostToStateMap
+                .filterValues(VerifyStatus::shouldRetry)
+                .takeIf { it.isNotEmpty() }
+                ?.map { it.key }
+                ?: return@mapNotNull null
+
+            hostsToRetry.map { Triple(domainSetId, packageName, it) }
+        }
+            .flatten()
+    }
+
+    suspend fun verifyHost(
+        host: String,
+        packageName: String,
+        network: Network? = null
+    ): Pair<WorkResult, VerifyStatus> {
+        val assetMatcher = synchronized(targetAssetCache) { targetAssetCache[packageName] }
+            .takeIf { it!!.isPresent }
+            ?: return WorkResult.failure() to VerifyStatus.FAILURE_PACKAGE_MANAGER
+        return verifyHost(host, assetMatcher.get(), network)
+    }
+
+    private suspend fun verifyHost(
+        host: String,
+        assetMatcher: AbstractAssetMatcher,
+        network: Network? = null
+    ): Pair<WorkResult, VerifyStatus> {
+        var exception: Exception? = null
+        val resultAndStatus = try {
+            val sourceAsset = StatementUtils.createWebAssetString(host)
+                .let(AbstractAsset::create)
+            val result = retriever.retrieve(sourceAsset, network)
+                ?: return WorkResult.success() to VerifyStatus.FAILURE_UNKNOWN
+            when (result.responseCode) {
+                HttpURLConnection.HTTP_MOVED_PERM,
+                HttpURLConnection.HTTP_MOVED_TEMP -> {
+                    WorkResult.failure() to VerifyStatus.FAILURE_REDIRECT
+                }
+                else -> {
+                    val isVerified = result.statements.any { statement ->
+                        (StatementUtils.RELATION.matches(statement.relation) &&
+                                assetMatcher.matches(statement.target))
+                    }
+
+                    if (isVerified) {
+                        WorkResult.success() to VerifyStatus.SUCCESS
+                    } else {
+                        WorkResult.failure() to VerifyStatus.FAILURE_REJECTED_BY_SERVER
+                    }
+                }
+            }
+        } catch (e: Exception) {
+            exception = e
+            WorkResult.retry() to VerifyStatus.FAILURE_UNKNOWN
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "Verifying $host: ${resultAndStatus.second}", exception)
+        }
+
+        return resultAndStatus
+    }
+
+    private inner class AssetLruCache : LruCache<String, Optional<AbstractAssetMatcher>>(50) {
+        override fun create(packageName: String) =
+            StatementUtils.getCertFingerprintsFromPackageManager(appContext, packageName)
+                .let { (it as? Result.Success)?.value }
+                ?.let { StatementUtils.createAndroidAsset(packageName, it) }
+                ?.let(AbstractAssetMatcher::createMatcher)
+                .let { Optional.ofNullable(it) }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt b/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt
new file mode 100644
index 0000000..2193ec5
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/VerifyStatus.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 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.domain
+
+import android.content.pm.verify.domain.DomainVerificationInfo
+import android.content.pm.verify.domain.DomainVerificationManager
+
+/**
+ * Wraps known [DomainVerificationManager] status codes so that they can be used in a when
+ * statement. Unknown codes are coerced to [VerifyStatus.UNKNOWN] and should be treated as
+ * unverified.
+ *
+ * Also includes error codes specific to this implementation of the domain verification agent.
+ * These must be stable across all versions, as codes are persisted to disk. They do not
+ * technically have to be stable across different device factory resets, since they will be reset
+ * once the apps are re-initialized, but easier to keep them unique forever.
+ */
+enum class VerifyStatus(val value: Int) {
+    NO_RESPONSE(DomainVerificationInfo.STATE_NO_RESPONSE),
+    SUCCESS(DomainVerificationInfo.STATE_SUCCESS),
+
+    UNKNOWN(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED),
+    FAILURE_LEGACY_UNSUPPORTED_WILDCARD(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 1),
+    FAILURE_REJECTED_BY_SERVER(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 2),
+    FAILURE_TIMEOUT(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 3),
+    FAILURE_UNKNOWN(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 4),
+    FAILURE_REDIRECT(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 5),
+
+    // Failed to retrieve signature information from PackageManager
+    FAILURE_PACKAGE_MANAGER(DomainVerificationInfo.STATE_FIRST_VERIFIER_DEFINED + 6);
+
+    companion object {
+        fun shouldRetry(state: Int): Boolean {
+            if (state == DomainVerificationInfo.STATE_UNMODIFIABLE) {
+                return false
+            }
+
+            val status = values().find { it.value == state } ?: return true
+            return when (status) {
+                SUCCESS,
+                FAILURE_LEGACY_UNSUPPORTED_WILDCARD,
+                FAILURE_REJECTED_BY_SERVER,
+                FAILURE_PACKAGE_MANAGER,
+                UNKNOWN -> false
+                NO_RESPONSE,
+                FAILURE_TIMEOUT,
+                FAILURE_UNKNOWN,
+                FAILURE_REDIRECT -> true
+            }
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt
new file mode 100644
index 0000000..a17f9c9
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 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.domain.worker
+
+import android.content.Context
+import android.content.pm.verify.domain.DomainVerificationManager
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.android.statementservice.domain.DomainVerifier
+
+abstract class BaseRequestWorker(
+    protected val appContext: Context,
+    protected val params: WorkerParameters
+) : CoroutineWorker(appContext, params) {
+
+    protected val verificationManager =
+        appContext.getSystemService(DomainVerificationManager::class.java)!!
+
+    protected val verifier = DomainVerifier.getInstance(appContext)
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt
new file mode 100644
index 0000000..3a3aea9
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/CollectV1Worker.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 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.domain.worker
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.util.Log
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkerParameters
+import com.android.statementservice.utils.AndroidUtils
+import kotlinx.coroutines.coroutineScope
+
+class CollectV1Worker(appContext: Context, params: WorkerParameters) :
+    BaseRequestWorker(appContext, params) {
+
+    companion object {
+        private val TAG = CollectV1Worker::class.java.simpleName
+        private const val DEBUG = false
+
+        private const val VERIFICATION_ID_KEY = "verificationId"
+        private const val PACKAGE_NAME_KEY = "packageName"
+
+        fun buildRequest(verificationId: Int, packageName: String) =
+            OneTimeWorkRequestBuilder<CollectV1Worker>()
+                .setInputData(
+                    Data.Builder()
+                        .putInt(VERIFICATION_ID_KEY, verificationId)
+                        .apply {
+                            if (DEBUG) {
+                                putString(PACKAGE_NAME_KEY, packageName)
+                            }
+                        }
+                        .build()
+                )
+                .build()
+    }
+
+    override suspend fun doWork() = coroutineScope {
+        if (!AndroidUtils.isReceiverV1Enabled(appContext)) {
+            return@coroutineScope Result.success()
+        }
+
+        val inputData = params.inputData
+        val verificationId = inputData.getInt(VERIFICATION_ID_KEY, -1)
+        val successfulHosts = mutableListOf<String>()
+        val failedHosts = mutableListOf<String>()
+        inputData.keyValueMap.entries.forEach { (key, _) ->
+            when {
+                key.startsWith(SingleV1RequestWorker.HOST_SUCCESS_PREFIX) ->
+                    successfulHosts += key.removePrefix(SingleV1RequestWorker.HOST_SUCCESS_PREFIX)
+                key.startsWith(SingleV1RequestWorker.HOST_FAILURE_PREFIX) ->
+                    failedHosts += key.removePrefix(SingleV1RequestWorker.HOST_FAILURE_PREFIX)
+            }
+        }
+
+        if (DEBUG) {
+            val packageName = inputData.getString(PACKAGE_NAME_KEY)
+            Log.d(
+                TAG, "Domain verification v1 request for $packageName: " +
+                        "success = $successfulHosts, failed = $failedHosts"
+            )
+        }
+
+        val resultCode = if (failedHosts.isEmpty()) {
+            PackageManager.INTENT_FILTER_VERIFICATION_SUCCESS
+        } else {
+            PackageManager.INTENT_FILTER_VERIFICATION_FAILURE
+        }
+
+        appContext.packageManager.verifyIntentFilter(verificationId, resultCode, failedHosts)
+
+        Result.success()
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt
new file mode 100644
index 0000000..61ab2c2
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 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.domain.worker
+
+import android.content.Context
+import androidx.work.NetworkType
+import androidx.work.WorkerParameters
+import com.android.statementservice.domain.VerifyStatus
+import com.android.statementservice.utils.AndroidUtils
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
+import java.util.UUID
+
+/**
+ * Scheduled every 24 hours with [NetworkType.CONNECTED] and every 72 hours without any constraints
+ * to retry all domains for all packages with a failing error code.
+ */
+class RetryRequestWorker(
+    appContext: Context,
+    params: WorkerParameters
+) : BaseRequestWorker(appContext, params) {
+
+    data class VerifyResult(val domainSetId: UUID, val host: String, val status: VerifyStatus)
+
+    override suspend fun doWork() = coroutineScope {
+        if (!AndroidUtils.isReceiverV2Enabled(appContext)) {
+            return@coroutineScope Result.success()
+        }
+
+        val packageNames = verificationManager.queryValidVerificationPackageNames()
+
+        verifier.collectHosts(packageNames)
+            .map { (domainSetId, packageName, host) ->
+                async {
+                    if (isActive && !isStopped) {
+                        val (_, status) = verifier.verifyHost(host, packageName, params.network)
+                        VerifyResult(domainSetId, host, status)
+                    } else {
+                        // If the job gets cancelled, stop the remaining hosts, but continue the
+                        // job to commit the results for hosts that were already requested.
+                        null
+                    }
+                }
+            }
+            .awaitAll()
+            .filterNotNull() // TODO(b/159952358): Fast fail packages which can't be retrieved.
+            .groupBy { it.domainSetId }
+            .forEach { (domainSetId, resultsById) ->
+                resultsById.groupBy { it.status }
+                    .mapValues { it.value.map(VerifyResult::host).toSet() }
+                    .forEach { (status, hosts) ->
+                        verificationManager.setDomainVerificationStatus(
+                            domainSetId,
+                            hosts,
+                            status.value
+                        )
+                    }
+            }
+
+        // Succeed regardless of results since this retry is best effort and not required
+        Result.success()
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt
new file mode 100644
index 0000000..cd8a182
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 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.domain.worker
+
+import android.content.Context
+import android.util.Log
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkerParameters
+import com.android.statementservice.utils.AndroidUtils
+import kotlinx.coroutines.coroutineScope
+
+class SingleV1RequestWorker(appContext: Context, params: WorkerParameters) :
+    BaseRequestWorker(appContext, params) {
+
+    companion object {
+        private val TAG = SingleV1RequestWorker::class.java.simpleName
+        private const val DEBUG = false
+
+        private const val PACKAGE_NAME_KEY = "packageName"
+        private const val HOST_KEY = "host"
+        const val HOST_SUCCESS_PREFIX = "hostSuccess:"
+        const val HOST_FAILURE_PREFIX = "hostFailure:"
+
+        fun buildRequest(
+            packageName: String,
+            host: String,
+            block: OneTimeWorkRequest.Builder.() -> Unit = {}
+        ) = OneTimeWorkRequestBuilder<SingleV1RequestWorker>()
+            .setInputData(
+                Data.Builder()
+                    .putString(PACKAGE_NAME_KEY, packageName)
+                    .putString(HOST_KEY, host)
+                    .build()
+            )
+            .apply(block)
+            .build()
+    }
+
+    override suspend fun doWork() = coroutineScope {
+        if (!AndroidUtils.isReceiverV1Enabled(appContext)) {
+            return@coroutineScope Result.success()
+        }
+
+        val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!!
+        val host = params.inputData.getString(HOST_KEY)!!
+
+        val (result, status) = verifier.verifyHost(host, packageName, params.network)
+
+        if (DEBUG) {
+            Log.d(
+                TAG, "Domain verification v1 request for $packageName: " +
+                        "host = $host, status = $status"
+            )
+        }
+
+        // Coerce failure results into success so that final collection task gets a chance to run
+        when (result) {
+            is Result.Success -> Result.success(
+                Data.Builder()
+                    .putInt("$HOST_SUCCESS_PREFIX$host", status.value)
+                    .build()
+            )
+            is Result.Failure -> Result.success(
+                Data.Builder()
+                    .putInt("$HOST_FAILURE_PREFIX$host", status.value)
+                    .build()
+            )
+            else -> result
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt
new file mode 100644
index 0000000..562b132
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 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.domain.worker
+
+import android.content.Context
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkerParameters
+import com.android.statementservice.utils.AndroidUtils
+import kotlinx.coroutines.coroutineScope
+import java.util.UUID
+
+class SingleV2RequestWorker(appContext: Context, params: WorkerParameters) :
+    BaseRequestWorker(appContext, params) {
+
+    companion object {
+        private const val DOMAIN_SET_ID_KEY = "domainSetId"
+        private const val PACKAGE_NAME_KEY = "packageName"
+        private const val HOST_KEY = "host"
+
+        fun buildRequest(
+            domainSetId: UUID,
+            packageName: String,
+            host: String,
+            block: OneTimeWorkRequest.Builder.() -> Unit = {}
+        ) = OneTimeWorkRequestBuilder<SingleV2RequestWorker>()
+            .setInputData(
+                Data.Builder()
+                    .putString(DOMAIN_SET_ID_KEY, domainSetId.toString())
+                    .putString(PACKAGE_NAME_KEY, packageName)
+                    .putString(HOST_KEY, host)
+                    .build()
+            )
+            .apply(block)
+            .build()
+    }
+
+    override suspend fun doWork() = coroutineScope {
+        if (!AndroidUtils.isReceiverV2Enabled(appContext)) {
+            return@coroutineScope Result.success()
+        }
+
+        val domainSetId = params.inputData.getString(DOMAIN_SET_ID_KEY)!!.let(UUID::fromString)
+        val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!!
+        val host = params.inputData.getString(HOST_KEY)!!
+
+        val (result, status) = verifier.verifyHost(host, packageName, params.network)
+
+        verificationManager.setDomainVerificationStatus(domainSetId, setOf(host), status.value)
+
+        result
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt
new file mode 100644
index 0000000..455e8085
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 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.network.retriever
+
+import android.util.JsonReader
+import com.android.statementservice.retriever.AbstractAsset
+import com.android.statementservice.retriever.AssetFactory
+import com.android.statementservice.retriever.JsonParser
+import com.android.statementservice.retriever.Relation
+import com.android.statementservice.retriever.Statement
+import com.android.statementservice.utils.Result
+import com.android.statementservice.utils.StatementUtils
+import java.io.StringReader
+import java.util.ArrayList
+import com.android.statementservice.retriever.WebAsset
+import com.android.statementservice.retriever.AndroidAppAsset
+
+/**
+ * Parses JSON from the Digital Asset Links specification. For examples, see [WebAsset],
+ * [AndroidAppAsset], and [Statement].
+ */
+object StatementParser {
+
+    private const val FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string."
+    private const val FIELD_NOT_ARRAY_FORMAT_STRING = "Expected %s to be array."
+
+    /**
+     * Parses a JSON array of statements.
+     */
+    fun parseStatementList(statementList: String, source: AbstractAsset): Result<ParsedStatement> {
+        val statements: MutableList<Statement> = ArrayList()
+        val delegates: MutableList<String> = ArrayList()
+        StringReader(statementList).use { stringReader ->
+            JsonReader(stringReader).use { reader ->
+                reader.isLenient = false
+                reader.beginArray()
+                while (reader.hasNext()) {
+                    val result = parseOneStatement(reader, source)
+                    if (result is Result.Failure) {
+                        continue
+                    }
+                    result as Result.Success
+                    statements.addAll(result.value.statements)
+                    delegates.addAll(result.value.delegates)
+                }
+                reader.endArray()
+            }
+        }
+        return Result.Success(ParsedStatement(statements, delegates))
+    }
+
+    /**
+     * Parses a single JSON statement.
+     */
+    fun parseStatement(statementString: String, source: AbstractAsset) =
+        StringReader(statementString).use { stringReader ->
+            JsonReader(stringReader).use { reader ->
+                reader.isLenient = false
+                parseOneStatement(reader, source)
+            }
+        }
+
+    /**
+     * Parses a single JSON statement. This method guarantees that exactly one JSON object
+     * will be consumed.
+     */
+    private fun parseOneStatement(
+        reader: JsonReader,
+        source: AbstractAsset
+    ): Result<ParsedStatement> {
+        val statement = JsonParser.parse(reader)
+        val delegate = statement.optString(StatementUtils.DELEGATE_FIELD_DELEGATE)
+        if (!delegate.isNullOrEmpty()) {
+            return Result.Success(ParsedStatement(emptyList(), listOfNotNull(delegate)))
+        }
+
+        val targetObject = statement.optJSONObject(StatementUtils.ASSET_DESCRIPTOR_FIELD_TARGET)
+            ?: return Result.Failure(
+                FIELD_NOT_STRING_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_TARGET)
+            )
+        val relations = statement.optJSONArray(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION)
+            ?: return Result.Failure(
+                FIELD_NOT_ARRAY_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION)
+            )
+        val target = AssetFactory.create(targetObject)
+
+        val statements = (0 until relations.length())
+            .map { relations.getString(it) }
+            .map(Relation::create)
+            .map { Statement.create(source, target, it) }
+        return Result.Success(ParsedStatement(statements, listOfNotNull(delegate)))
+    }
+
+    data class ParsedStatement(val statements: List<Statement>, val delegates: List<String>)
+}
diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementRetriever.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementRetriever.kt
new file mode 100644
index 0000000..c27a26a
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementRetriever.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 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.network.retriever
+
+import android.content.Intent
+import android.net.Network
+import com.android.statementservice.retriever.AbstractAsset
+import com.android.statementservice.retriever.AndroidAppAsset
+import com.android.statementservice.retriever.Statement
+import com.android.statementservice.retriever.WebAsset
+import com.android.statementservice.utils.StatementUtils.tryOrNull
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.withContext
+import java.net.URL
+
+/**
+ * Retrieves the JSON configured at a given domain that's compliant with the Digital Asset Links
+ * specification, returning the list of statements which serve as assertions by the web server as
+ * to what other assets it can be connected with.
+ *
+ * Relevant to this app, it allows the website to report which Android app package and signature
+ * digest has been approved by the website owner, which considers them as the same author and safe
+ * to automatically delegate web [Intent]s to.
+ *
+ * The relevant data classes are [WebAsset], [AndroidAppAsset], and [Statement].
+ */
+class StatementRetriever {
+
+    companion object {
+        private const val HTTP_CONNECTION_TIMEOUT_MILLIS = 5000
+        private const val HTTP_CONTENT_SIZE_LIMIT_IN_BYTES = (1024 * 1024).toLong()
+        private const val MAX_INCLUDE_LEVEL = 1
+        private const val WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json"
+    }
+
+    private val fetcher = UrlFetcher()
+
+    data class Result(
+        val statements: List<Statement>,
+        val responseCode: Int?
+    ) {
+        companion object {
+            val EMPTY = Result(emptyList(), null)
+        }
+
+        constructor(statements: List<Statement>, webResult: UrlFetcher.Response) : this(
+            statements,
+            webResult.responseCode
+        )
+    }
+
+    suspend fun retrieve(source: AbstractAsset, network: Network? = null) = when (source) {
+        // TODO:(b/171219506): Does this have to be implemented?
+        is AndroidAppAsset -> null
+        is WebAsset -> retrieveFromWeb(source, network)
+        else -> null
+    }
+
+    private suspend fun retrieveFromWeb(asset: WebAsset, network: Network? = null): Result? {
+        val url = computeAssociationJsonUrl(asset) ?: return null
+        return retrieve(url, MAX_INCLUDE_LEVEL, asset, network)
+    }
+
+    private fun computeAssociationJsonUrl(asset: WebAsset) = tryOrNull {
+        URL(asset.scheme, asset.domain, asset.port, WELL_KNOWN_STATEMENT_PATH).toExternalForm()
+    }
+
+    private suspend fun retrieve(
+        urlString: String,
+        maxIncludeLevel: Int,
+        source: AbstractAsset,
+        network: Network? = null
+    ): Result {
+        if (maxIncludeLevel < 0) {
+            return Result.EMPTY
+        }
+
+        return withContext(Dispatchers.IO) {
+            val url = try {
+                @Suppress("BlockingMethodInNonBlockingContext")
+                URL(urlString)
+            } catch (ignored: Exception) {
+                return@withContext Result.EMPTY
+            }
+
+            val webResponse = fetcher.fetch(
+                url = url,
+                connectionTimeoutMillis = HTTP_CONNECTION_TIMEOUT_MILLIS,
+                fileSizeLimit = HTTP_CONTENT_SIZE_LIMIT_IN_BYTES,
+                network
+            ).successValueOrNull() ?: return@withContext Result.EMPTY
+
+            val content = webResponse.content ?: return@withContext Result(emptyList(), webResponse)
+            val (statements, delegates) = StatementParser.parseStatementList(content, source)
+                .successValueOrNull() ?: return@withContext Result(emptyList(), webResponse)
+
+            val delegatedStatements = delegates
+                .map { async { retrieve(it, maxIncludeLevel - 1, source).statements } }
+                .awaitAll()
+                .flatten()
+
+            Result(statements + delegatedStatements, webResponse)
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/UrlFetcher.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/UrlFetcher.kt
new file mode 100644
index 0000000..5c1f5e0
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/network/retriever/UrlFetcher.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2020 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.network.retriever
+
+import android.net.Network
+import android.net.TrafficStats
+import android.util.Log
+import com.android.statementservice.utils.Result
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import java.net.HttpURLConnection
+import java.net.URL
+import java.nio.charset.Charset
+import javax.net.ssl.HttpsURLConnection
+
+class UrlFetcher {
+
+    companion object {
+        private val TAG = UrlFetcher::class.java.simpleName
+    }
+
+    suspend fun fetch(
+        url: URL,
+        connectionTimeoutMillis: Int,
+        fileSizeLimit: Long,
+        network: Network? = null
+    ) = withContext(Dispatchers.IO) {
+        TrafficStats.setThreadStatsTag(Thread.currentThread().id.toInt())
+        @Suppress("BlockingMethodInNonBlockingContext")
+        val connection =
+            ((network?.openConnection(url) ?: url.openConnection()) as HttpsURLConnection)
+        try {
+            connection.apply {
+                connectTimeout = connectionTimeoutMillis
+                readTimeout = connectionTimeoutMillis
+                useCaches = true
+                instanceFollowRedirects = false
+                addRequestProperty("Cache-Control", "max-stale=60")
+            }
+            val responseCode = connection.responseCode
+            when {
+                responseCode != HttpURLConnection.HTTP_OK -> {
+                    Log.w(TAG, "The responses code is not 200 but $responseCode")
+                    Result.Success(Response(responseCode))
+                }
+                connection.contentLength > fileSizeLimit -> {
+                    Log.w(TAG, "The content size of the url is larger than $fileSizeLimit")
+                    Result.Success(Response(responseCode))
+                }
+                else -> {
+                    val content = async {
+                        connection.inputStream
+                            .bufferedReader(Charset.forName("UTF-8"))
+                            .readText()
+                    }
+
+                    Result.Success(Response(responseCode, content.await()))
+                }
+            }
+        } catch (ignored: Throwable) {
+            Result.Failure(ignored)
+        } finally {
+            connection.disconnect()
+        }
+    }
+
+    data class Response(
+        val responseCode: Int,
+        val content: String? = null
+    )
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
index 8d6fd66..4834626 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
@@ -81,6 +81,9 @@
     /**
      * If this is the source asset of a statement file, should the retriever follow
      * any insecure (non-HTTPS) include statements made by the asset.
+     *
+     * TODO(b/171219506): Why would this be allowed? Can it be removed, even for web assets?
+     *  Android doesn't even allow non-secure traffic by default.
      */
     public abstract boolean followInsecureInclude();
 }
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java
deleted file mode 100644
index fe9b99a..0000000
--- a/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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/assetlinks.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
index 8ead90b..14ca232 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java
@@ -16,6 +16,8 @@
 
 package com.android.statementservice.retriever;
 
+import com.android.statementservice.utils.StatementUtils;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -34,7 +36,8 @@
  * "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"]
+ * "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:
@@ -43,7 +46,7 @@
  * <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 {
+public 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 =
@@ -65,9 +68,10 @@
     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);
+        writer.writeFieldLower(StatementUtils.NAMESPACE_FIELD,
+                StatementUtils.NAMESPACE_ANDROID_APP);
+        writer.writeFieldLower(StatementUtils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName);
+        writer.writeArrayUpper(StatementUtils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints);
 
         return writer.closeAndGetString();
     }
@@ -114,17 +118,17 @@
      */
     public static AndroidAppAsset create(JSONObject asset)
             throws AssociationServiceException {
-        String packageName = asset.optString(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME);
+        String packageName = asset.optString(StatementUtils.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));
+                    StatementUtils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME));
         }
 
-        JSONArray certArray = asset.optJSONArray(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS);
+        JSONArray certArray = asset.optJSONArray(StatementUtils.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));
+                            StatementUtils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
         }
         List<String> certFingerprints = new ArrayList<>(certArray.length());
         for (int i = 0; i < certArray.length(); i++) {
@@ -133,7 +137,7 @@
             } catch (JSONException e) {
                 throw new AssociationServiceException(
                         String.format(APPCERT_NOT_STRING_FORMAT_STRING,
-                                Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
+                                StatementUtils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
             }
         }
 
@@ -143,7 +147,7 @@
     /**
      * Creates a new AndroidAppAsset.
      *
-     * @param packageName the package name of the Android app.
+     * @param packageName      the package name of the Android app.
      * @param certFingerprints at least one of the Android app signing certificate sha-256
      *                         fingerprint.
      */
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java
index 8a9d838..45798fa 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java
@@ -23,7 +23,7 @@
  * 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 {
+public final class AndroidAppAssetMatcher extends AbstractAssetMatcher {
 
     private final AndroidAppAsset mQuery;
 
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java
deleted file mode 100644
index 1000c4c..0000000
--- a/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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
index 519d73a2..ac0bfab 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java
@@ -16,12 +16,14 @@
 
 package com.android.statementservice.retriever;
 
+import com.android.statementservice.utils.StatementUtils;
+
 import org.json.JSONObject;
 
 /**
  * Factory to create asset from JSON string.
  */
-/* package private */ final class AssetFactory {
+public final class AssetFactory {
 
     private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string.";
 
@@ -34,15 +36,15 @@
      */
     public static AbstractAsset create(JSONObject asset)
             throws AssociationServiceException {
-        String namespace = asset.optString(Utils.NAMESPACE_FIELD, null);
+        String namespace = asset.optString(StatementUtils.NAMESPACE_FIELD, null);
         if (namespace == null) {
             throw new AssociationServiceException(String.format(
-                    FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD));
+                    FIELD_NOT_STRING_FORMAT_STRING, StatementUtils.NAMESPACE_FIELD));
         }
 
-        if (namespace.equals(Utils.NAMESPACE_WEB)) {
+        if (namespace.equals(StatementUtils.NAMESPACE_WEB)) {
             return WebAsset.create(asset);
-        } else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) {
+        } else if (namespace.equals(StatementUtils.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/AssetMatcherFactory.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java
index 1a50757..7773668 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java
@@ -16,6 +16,8 @@
 
 package com.android.statementservice.retriever;
 
+import com.android.statementservice.utils.StatementUtils;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -31,15 +33,15 @@
             JSONException {
         JSONObject queryObject = new JSONObject(query);
 
-        String namespace = queryObject.optString(Utils.NAMESPACE_FIELD, null);
+        String namespace = queryObject.optString(StatementUtils.NAMESPACE_FIELD, null);
         if (namespace == null) {
             throw new AssociationServiceException(String.format(
-                    FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD));
+                    FIELD_NOT_STRING_FORMAT_STRING, StatementUtils.NAMESPACE_FIELD));
         }
 
-        if (namespace.equals(Utils.NAMESPACE_WEB)) {
+        if (namespace.equals(StatementUtils.NAMESPACE_WEB)) {
             return new WebAssetMatcher(WebAsset.create(queryObject));
-        } else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) {
+        } else if (namespace.equals(StatementUtils.NAMESPACE_ANDROID_APP)) {
             return new AndroidAppAssetMatcher(AndroidAppAsset.create(queryObject));
         } else {
             throw new AssociationServiceException(
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java b/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java
deleted file mode 100644
index 9839329..0000000
--- a/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * 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 int HTTP_CONNECTION_BACKOFF_MILLIS = 3000;
-    private static final int HTTP_CONNECTION_RETRY = 3;
-    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/assetlinks.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 urlString, 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 {
-            URL url = new URL(urlString);
-            if (!source.followInsecureInclude()
-                    && !url.getProtocol().toLowerCase().equals("https")) {
-                return Result.create(statements, DO_NOT_CACHE_RESULT);
-            }
-            webContent = mUrlFetcher.getWebContentFromUrlWithRetry(url,
-                    HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS,
-                    HTTP_CONNECTION_BACKOFF_MILLIS, HTTP_CONNECTION_RETRY);
-        } catch (IOException | InterruptedException 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 | IOException 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 | IOException | 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
deleted file mode 100644
index 9446e66..0000000
--- a/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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/Statement.java b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java
index 0f40a62..f8bab3e 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/Statement.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java
@@ -17,6 +17,11 @@
 package com.android.statementservice.retriever;
 
 import android.annotation.NonNull;
+import android.net.Network;
+
+import com.android.statementservice.network.retriever.StatementRetriever;
+
+import kotlin.coroutines.Continuation;
 
 /**
  * An immutable value type representing a statement, consisting of a source, target, and relation.
@@ -31,9 +36,9 @@
  * }
  * </pre>
  *
- * Then invoking {@link AbstractStatementRetriever#retrieveStatements(AbstractAsset)} will return a
- * {@link Statement} with {@link #getSource} equal to the input parameter, {@link #getRelation}
- * equal to
+ * Then invoking {@link StatementRetriever#retrieve(AbstractAsset, Network, Continuation)} 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>
  *
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java b/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java
deleted file mode 100644
index 0369718..0000000
--- a/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.JsonReader;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Utility class that parses JSON-formatted statements.
- */
-/* package private */ final class StatementParser {
-
-    private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string.";
-    private static final String FIELD_NOT_ARRAY_FORMAT_STRING = "Expected %s to be array.";
-
-    /**
-     * Parses a JSON array of statements.
-     */
-    static ParsedStatement parseStatementList(String statementList, AbstractAsset source)
-            throws JSONException, IOException {
-        List<Statement> statements = new ArrayList<Statement>();
-        List<String> delegates = new ArrayList<String>();
-
-        JsonReader reader = new JsonReader(new StringReader(statementList));
-        reader.setLenient(false);
-
-        reader.beginArray();
-        while (reader.hasNext()) {
-            ParsedStatement result;
-            try {
-                result = parseStatement(reader, source);
-            } catch (AssociationServiceException e) {
-                // The element in the array is well formatted Json but not a well-formed Statement.
-                continue;
-            }
-            statements.addAll(result.getStatements());
-            delegates.addAll(result.getDelegates());
-        }
-        reader.endArray();
-
-        return new ParsedStatement(statements, delegates);
-    }
-
-    /**
-     * Parses a single JSON statement.
-     */
-    static ParsedStatement parseStatement(String statementString, AbstractAsset source)
-            throws AssociationServiceException, IOException, JSONException {
-        JsonReader reader = new JsonReader(new StringReader(statementString));
-        reader.setLenient(false);
-        return parseStatement(reader, source);
-    }
-
-    /**
-     * Parses a single JSON statement. This method guarantees that exactly one JSON object
-     * will be consumed.
-     */
-    static ParsedStatement parseStatement(JsonReader reader, AbstractAsset source)
-            throws JSONException, AssociationServiceException, IOException {
-        List<Statement> statements = new ArrayList<Statement>();
-        List<String> delegates = new ArrayList<String>();
-
-        JSONObject statement = JsonParser.parse(reader);
-
-        if (statement.optString(Utils.DELEGATE_FIELD_DELEGATE, null) != null) {
-            delegates.add(statement.optString(Utils.DELEGATE_FIELD_DELEGATE));
-        } else {
-            JSONObject targetObject = statement.optJSONObject(Utils.ASSET_DESCRIPTOR_FIELD_TARGET);
-            if (targetObject == null) {
-                throw new AssociationServiceException(String.format(
-                        FIELD_NOT_STRING_FORMAT_STRING, Utils.ASSET_DESCRIPTOR_FIELD_TARGET));
-            }
-
-            JSONArray relations = statement.optJSONArray(Utils.ASSET_DESCRIPTOR_FIELD_RELATION);
-            if (relations == null) {
-                throw new AssociationServiceException(String.format(
-                        FIELD_NOT_ARRAY_FORMAT_STRING, Utils.ASSET_DESCRIPTOR_FIELD_RELATION));
-            }
-
-            AbstractAsset target = AssetFactory.create(targetObject);
-            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
deleted file mode 100644
index 23cd832..0000000
--- a/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * 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.Log;
-
-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 String TAG = URLFetcher.class.getSimpleName();
-
-    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.
-     *
-     * <p>
-     * Retry {@code retry} times if the connection failed or timed out for any reason.
-     * HTTP error code (e.g. 404/500) won't be retried.
-     *
-     * @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 getWebContentFromUrlWithRetry(URL url, long fileSizeLimit,
-            int connectionTimeoutMillis, int backoffMillis, int retry)
-                    throws AssociationServiceException, IOException, InterruptedException {
-        if (retry <= 0) {
-            throw new IllegalArgumentException("retry should be a postive inetger.");
-        }
-        while (retry > 0) {
-            try {
-                return getWebContentFromUrl(url, fileSizeLimit, connectionTimeoutMillis);
-            } catch (IOException e) {
-                retry--;
-                if (retry == 0) {
-                    throw e;
-                }
-            }
-
-            Thread.sleep(backoffMillis);
-        }
-
-        // Should never reach here.
-        return null;
-    }
-
-    /**
-     * 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 = null;
-        try {
-            connection = (HttpURLConnection) url.openConnection();
-            connection.setInstanceFollowRedirects(true);
-            connection.setConnectTimeout(connectionTimeoutMillis);
-            connection.setReadTimeout(connectionTimeoutMillis);
-            connection.setUseCaches(true);
-            connection.setInstanceFollowRedirects(false);
-            connection.addRequestProperty("Cache-Control", "max-stale=60");
-
-            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
-                Log.e(TAG, "The responses code is not 200 but "  + connection.getResponseCode());
-                return new WebContent("", DO_NOT_CACHE_RESULT);
-            }
-
-            if (connection.getContentLength() > fileSizeLimit) {
-                Log.e(TAG, "The content size of the url is larger than "  + fileSizeLimit);
-                return new WebContent("", DO_NOT_CACHE_RESULT);
-            }
-
-            Long expireTimeMillis = getExpirationTimeMillisFromHTTPHeader(
-                    connection.getHeaderFields());
-
-            return new WebContent(inputStreamToString(
-                    connection.getInputStream(), connection.getContentLength(), fileSizeLimit),
-                expireTimeMillis);
-        } finally {
-            if (connection != null) {
-                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
deleted file mode 100644
index afb4c75..0000000
--- a/packages/StatementService/src/com/android/statementservice/retriever/Utils.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * 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 = "include";
-
-    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
index 947087a..608ce69 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java
@@ -16,6 +16,8 @@
 
 package com.android.statementservice.retriever;
 
+import com.android.statementservice.utils.StatementUtils;
+
 import org.json.JSONObject;
 
 import java.net.MalformedURLException;
@@ -36,7 +38,7 @@
  * <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 {
+public final class WebAsset extends AbstractAsset {
 
     private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
     private static final String SCHEME_HTTP = "http";
@@ -73,8 +75,8 @@
     public String toJson() {
         AssetJsonWriter writer = new AssetJsonWriter();
 
-        writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_WEB);
-        writer.writeFieldLower(Utils.WEB_ASSET_FIELD_SITE, mUrl.toExternalForm());
+        writer.writeFieldLower(StatementUtils.NAMESPACE_FIELD, StatementUtils.NAMESPACE_WEB);
+        writer.writeFieldLower(StatementUtils.WEB_ASSET_FIELD_SITE, mUrl.toExternalForm());
 
         return writer.closeAndGetString();
     }
@@ -119,14 +121,14 @@
      */
     protected static WebAsset create(JSONObject asset)
             throws AssociationServiceException {
-        if (asset.optString(Utils.WEB_ASSET_FIELD_SITE).equals("")) {
+        if (asset.optString(StatementUtils.WEB_ASSET_FIELD_SITE).equals("")) {
             throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
-                    Utils.WEB_ASSET_FIELD_SITE));
+                    StatementUtils.WEB_ASSET_FIELD_SITE));
         }
 
         URL url;
         try {
-            url = new URL(asset.optString(Utils.WEB_ASSET_FIELD_SITE));
+            url = new URL(asset.optString(StatementUtils.WEB_ASSET_FIELD_SITE));
         } catch (MalformedURLException e) {
             throw new AssociationServiceException("Url is not well formatted.", e);
         }
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java b/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java
index 86a635c..23b1f9b 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java
@@ -27,10 +27,12 @@
 
     private final String mContent;
     private final Long mExpireTimeMillis;
+    private final int mResponseCode;
 
-    public WebContent(String content, Long expireTimeMillis) {
+    public WebContent(String content, Long expireTimeMillis, int responseCode) {
         mContent = content;
         mExpireTimeMillis = expireTimeMillis;
+        mResponseCode = responseCode;
     }
 
     /**
@@ -46,4 +48,8 @@
     public String getContent() {
         return mContent;
     }
+
+    public int getResponseCode() {
+        return mResponseCode;
+    }
 }
diff --git a/packages/StatementService/src/com/android/statementservice/utils/AndroidUtils.kt b/packages/StatementService/src/com/android/statementservice/utils/AndroidUtils.kt
new file mode 100644
index 0000000..7fe0a02
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/utils/AndroidUtils.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 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.utils
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.verify.domain.DomainVerificationInfo
+import android.content.pm.verify.domain.DomainVerificationRequest
+import android.content.pm.verify.domain.DomainVerificationUserState
+import com.android.statementservice.domain.DomainVerificationReceiverV1
+import com.android.statementservice.domain.DomainVerificationReceiverV2
+
+// Top level extensions for models to allow Kotlin deconstructing declarations
+
+operator fun DomainVerificationRequest.component1() = packageNames
+
+operator fun DomainVerificationInfo.component1() = identifier
+operator fun DomainVerificationInfo.component2() = packageName
+operator fun DomainVerificationInfo.component3() = hostToStateMap
+
+operator fun DomainVerificationUserState.component1() = identifier
+operator fun DomainVerificationUserState.component2() = packageName
+operator fun DomainVerificationUserState.component3() = user
+operator fun DomainVerificationUserState.component4() = isLinkHandlingAllowed
+operator fun DomainVerificationUserState.component5() = hostToStateMap
+
+object AndroidUtils {
+
+    fun isReceiverV1Enabled(context: Context): Boolean {
+        val receiver = ComponentName(context, DomainVerificationReceiverV1::class.java)
+        return when (context.packageManager.getComponentEnabledSetting(receiver)) {
+            // Must change this if the manifest ever changes
+            PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> true
+            PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
+            else -> false
+        }
+    }
+
+    fun isReceiverV2Enabled(context: Context): Boolean {
+        val receiver = ComponentName(context, DomainVerificationReceiverV2::class.java)
+        return when (context.packageManager.getComponentEnabledSetting(receiver)) {
+            // Must change this if the manifest ever changes
+            PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> false
+            PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
+            else -> false
+        }
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/utils/Result.kt b/packages/StatementService/src/com/android/statementservice/utils/Result.kt
new file mode 100644
index 0000000..f23a010
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/utils/Result.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 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.utils
+
+sealed class Result<T> {
+
+    fun successValueOrNull() = (this as? Success<T>)?.value
+
+    data class Success<T>(val value: T) : Result<T>()
+    data class Failure<T>(val message: String? = null, val throwable: Throwable? = null) :
+        Result<T>() {
+
+        constructor(message: String) : this(message = message, throwable = null)
+        constructor(throwable: Throwable) : this(message = null, throwable = throwable)
+
+        @Suppress("UNCHECKED_CAST")
+        fun <T> asType() = this as Result<T>
+    }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt b/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt
new file mode 100644
index 0000000..92d752c
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2021 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.utils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.util.Patterns
+import com.android.statementservice.retriever.Relation
+import java.net.URL
+import java.security.MessageDigest
+
+internal object StatementUtils {
+
+    /**
+     * Field name for namespace.
+     */
+    const val NAMESPACE_FIELD = "namespace"
+
+    /**
+     * Supported asset namespaces.
+     */
+    const val NAMESPACE_WEB = "web"
+    const val NAMESPACE_ANDROID_APP = "android_app"
+
+    /**
+     * Field names in a web asset descriptor.
+     */
+    const val WEB_ASSET_FIELD_SITE = "site"
+
+    /**
+     * Field names in a Android app asset descriptor.
+     */
+    const val ANDROID_APP_ASSET_FIELD_PACKAGE_NAME = "package_name"
+    const val ANDROID_APP_ASSET_FIELD_CERT_FPS = "sha256_cert_fingerprints"
+
+    /**
+     * Field names in a statement.
+     */
+    const val ASSET_DESCRIPTOR_FIELD_RELATION = "relation"
+    const val ASSET_DESCRIPTOR_FIELD_TARGET = "target"
+    const val DELEGATE_FIELD_DELEGATE = "include"
+
+    val HEX_DIGITS =
+        charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
+
+    val RELATION by lazy { Relation.create("delegate_permission/common.handle_all_urls") }
+    private const val ANDROID_ASSET_FORMAT =
+        """{"namespace": "android_app", "package_name": "%s", "sha256_cert_fingerprints": [%s]}"""
+    private const val WEB_ASSET_FORMAT = """{"namespace": "web", "site": "%s"}"""
+
+    private val digesterSha256 by lazy { tryOrNull { MessageDigest.getInstance("SHA-256") } }
+
+    internal inline fun <T> tryOrNull(block: () -> T) =
+        try {
+            block()
+        } catch (ignored: Exception) {
+            null
+        }
+
+    /**
+     * Returns the normalized sha-256 fingerprints of a given package according to the Android
+     * package manager.
+     */
+    fun getCertFingerprintsFromPackageManager(
+        context: Context,
+        packageName: String
+    ): Result<List<String>> {
+        val signingInfo = try {
+            context.packageManager.getPackageInfo(
+                packageName,
+                PackageManager.GET_SIGNING_CERTIFICATES or PackageManager.MATCH_ANY_USER
+            )
+                .signingInfo
+        } catch (e: Exception) {
+            return Result.Failure(e)
+        }
+        return if (signingInfo.hasMultipleSigners()) {
+            signingInfo.apkContentsSigners
+        } else {
+            signingInfo.signingCertificateHistory
+        }.map {
+            val result = computeNormalizedSha256Fingerprint(it.toByteArray())
+            if (result is Result.Failure) {
+                return result.asType()
+            } else {
+                (result as Result.Success).value
+            }
+        }.let { Result.Success(it) }
+    }
+
+    /**
+     * Computes the hash of the byte array using the specified algorithm, returning a hex string
+     * with a colon between each byte.
+     */
+    fun computeNormalizedSha256Fingerprint(signature: ByteArray) =
+        digesterSha256?.digest(signature)
+            ?.let(StatementUtils::bytesToHexString)
+            ?.let { Result.Success(it) }
+            ?: Result.Failure()
+
+    private fun bytesToHexString(bytes: ByteArray): String {
+        val hexChars = CharArray(bytes.size * 3 - 1)
+        var bufIndex = 0
+        for (index in bytes.indices) {
+            val byte = bytes[index].toInt() and 0xFF
+            if (index > 0) {
+                hexChars[bufIndex++] = ':'
+            }
+
+            hexChars[bufIndex++] = HEX_DIGITS[byte ushr 4]
+            hexChars[bufIndex++] = HEX_DIGITS[byte and 0x0F]
+        }
+        return String(hexChars)
+    }
+
+    fun createAndroidAssetString(context: Context, packageName: String): Result<String> {
+        val result = getCertFingerprintsFromPackageManager(context, packageName)
+        if (result is Result.Failure) {
+            return result.asType()
+        }
+        return Result.Success(
+            ANDROID_ASSET_FORMAT.format(
+                packageName,
+                (result as Result.Success).value.joinToString(separator = "\", \"")
+            )
+        )
+    }
+
+    fun createAndroidAsset(packageName: String, certFingerprints: List<String>) =
+        String.format(
+            ANDROID_ASSET_FORMAT,
+            packageName,
+            certFingerprints.joinToString(separator = ", ") { "\"$it\"" })
+
+    fun createWebAssetString(scheme: String, host: String): Result<String> {
+        if (!Patterns.DOMAIN_NAME.matcher(host).matches()) {
+            return Result.Failure("Input host is not valid.")
+        }
+        if (scheme != "http" && scheme != "https") {
+            return Result.Failure("Input scheme is not valid.")
+        }
+        return Result.Success(WEB_ASSET_FORMAT.format(URL(scheme, host, "").toString()))
+    }
+
+    // Hosts with *. for wildcard subdomain support are verified against their root domain
+    fun createWebAssetString(host: String) =
+        WEB_ASSET_FORMAT.format(URL("https", host.removePrefix("*."), "").toString())
+}