Initial Digital Asset Links support (DAL).

This change uses DAL to verify that an app being autofilled is linked to
the web domain set in a node, and fail if the association could not be verified.

It also includes some minor improvements, such as:

- Clears cache on WebView activity.
- Takes an optional url extra in the WebView activity intent.
- Uses FillCallback.onFailure() to report some errors.

Bug: 66414472
Bug: 66417779
Bug: 66900717
Test: manual verification

Change-Id: I2fa96ae61201d7d11aaab168957da2a5685a5764
diff --git a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml
index a5abfe8..0af5716 100644
--- a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml
+++ b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml
@@ -18,6 +18,8 @@
     android:versionCode="1"
     android:versionName="1.0">
 
+    <uses-permission android:name="android.permission.INTERNET"/>
+
     <application
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java
index 84b5a97..8c429d0 100644
--- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/CommonUtil.java
@@ -31,6 +31,7 @@
 
     public static final String TAG = "AutofillSample";
     public static final boolean DEBUG = true;
+    public static final boolean VERBOSE = false;
     public static final String EXTRA_DATASET_NAME = "dataset_name";
     public static final String EXTRA_FOR_RESPONSE = "for_response";
 
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/WebViewSignInActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/WebViewSignInActivity.java
index 9ef8428..616529a 100644
--- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/WebViewSignInActivity.java
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/app/WebViewSignInActivity.java
@@ -19,9 +19,14 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.webkit.WebSettings;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 
+import static com.example.android.autofillframework.CommonUtil.DEBUG;
+import static com.example.android.autofillframework.CommonUtil.TAG;
+
 import com.example.android.autofillframework.R;
 
 public class WebViewSignInActivity extends AppCompatActivity {
@@ -38,7 +43,19 @@
         setContentView(R.layout.login_webview_activity);
 
         WebView webView = findViewById(R.id.webview);
+        WebSettings webSettings = webView.getSettings();
         webView.setWebViewClient(new WebViewClient());
-        webView.loadUrl("file:///android_res/raw/sample_form.html");
+        webSettings.setJavaScriptEnabled(true);
+
+        String url = getIntent().getStringExtra("url");
+        if (url == null) {
+            url = "file:///android_res/raw/sample_form.html";
+        }
+        if (DEBUG) Log.d(TAG, "Clearing WebView data");
+        webView.clearHistory();
+        webView.clearFormData();
+        webView.clearCache(true);
+        Log.i(TAG, "Loading URL " + url);
+        webView.loadUrl(url);
     }
 }
\ No newline at end of file
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.java
index 015dc68..631cc0a 100644
--- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.java
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AuthActivity.java
@@ -124,7 +124,7 @@
         Intent intent = getIntent();
         boolean forResponse = intent.getBooleanExtra(EXTRA_FOR_RESPONSE, true);
         AssistStructure structure = intent.getParcelableExtra(EXTRA_ASSIST_STRUCTURE);
-        StructureParser parser = new StructureParser(structure);
+        StructureParser parser = new StructureParser(getApplicationContext(), structure);
         parser.parseForFill();
         AutofillFieldMetadataCollection autofillFields = parser.getAutofillFields();
         int saveTypes = autofillFields.getSaveType();
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.java
index 0b47cfd..4c0f173 100644
--- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.java
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/AutofillHelper.java
@@ -38,6 +38,10 @@
  */
 public final class AutofillHelper {
 
+    private AutofillHelper() {
+        throw new UnsupportedOperationException("provide static methods only");
+    }
+
     /**
      * Wraps autofill data in a LoginCredential  Dataset object which can then be sent back to the
      * client View.
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.java
index f30e91b..ac03022 100644
--- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.java
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/MyAutofillService.java
@@ -29,7 +29,6 @@
 import android.util.Log;
 import android.view.autofill.AutofillId;
 import android.widget.RemoteViews;
-import android.widget.Toast;
 
 import com.example.android.autofillframework.R;
 import com.example.android.autofillframework.multidatasetservice.datasource.SharedPrefsAutofillRepository;
@@ -41,8 +40,8 @@
 import java.util.HashMap;
 import java.util.List;
 
-import static com.example.android.autofillframework.CommonUtil.DEBUG;
 import static com.example.android.autofillframework.CommonUtil.TAG;
+import static com.example.android.autofillframework.CommonUtil.VERBOSE;
 import static com.example.android.autofillframework.CommonUtil.bundleToString;
 import static com.example.android.autofillframework.CommonUtil.dumpStructure;
 
@@ -56,13 +55,13 @@
         String packageName = structure.getActivityComponent().getPackageName();
         if (!SharedPrefsPackageVerificationRepository.getInstance()
                 .putPackageSignatures(getApplicationContext(), packageName)) {
-            Toast.makeText(getApplicationContext(), R.string.invalid_package_signature,
-                    Toast.LENGTH_SHORT).show();
+            callback.onFailure(
+                    getApplicationContext().getString(R.string.invalid_package_signature));
             return;
         }
         final Bundle data = request.getClientState();
-        if (DEBUG) {
-            Log.d(TAG, "onFillRequest(): data=" + bundleToString(data));
+        if (VERBOSE) {
+            Log.v(TAG, "onFillRequest(): data=" + bundleToString(data));
             dumpStructure(structure);
         }
 
@@ -73,8 +72,17 @@
             }
         });
         // Parse AutoFill data in Activity
-        StructureParser parser = new StructureParser(structure);
-        parser.parseForFill();
+        StructureParser parser = new StructureParser(getApplicationContext(), structure);
+        // TODO: try / catch on other places (onSave, auth activity, etc...)
+        try {
+            parser.parseForFill();
+        } catch (SecurityException e) {
+            // TODO: handle cases where DAL didn't pass by showing a custom UI asking the user
+            // to confirm the mapping. Might require subclassing SecurityException.
+            Log.w(TAG, "Security exception handling " + request, e);
+            callback.onFailure(e.getMessage());
+            return;
+        }
         AutofillFieldMetadataCollection autofillFields = parser.getAutofillFields();
         FillResponse.Builder responseBuilder = new FillResponse.Builder();
         // Check user's settings for authenticating Responses and Datasets.
@@ -108,16 +116,16 @@
         String packageName = structure.getActivityComponent().getPackageName();
         if (!SharedPrefsPackageVerificationRepository.getInstance()
                 .putPackageSignatures(getApplicationContext(), packageName)) {
-            Toast.makeText(getApplicationContext(), R.string.invalid_package_signature,
-                    Toast.LENGTH_SHORT).show();
+            callback.onFailure(
+                    getApplicationContext().getString(R.string.invalid_package_signature));
             return;
         }
         final Bundle data = request.getClientState();
-        if (DEBUG) {
-            Log.d(TAG, "onSaveRequest(): data=" + bundleToString(data));
+        if (VERBOSE) {
+            Log.v(TAG, "onSaveRequest(): data=" + bundleToString(data));
             dumpStructure(structure);
         }
-        StructureParser parser = new StructureParser(structure);
+        StructureParser parser = new StructureParser(getApplicationContext(), structure);
         parser.parseForSave();
         FilledAutofillFieldCollection filledAutofillFieldCollection = parser.getClientFormData();
         SharedPrefsAutofillRepository.getInstance()
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/SecurityHelper.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/SecurityHelper.java
new file mode 100644
index 0000000..3d13b6d
--- /dev/null
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/SecurityHelper.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017 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.example.android.autofillframework.multidatasetservice;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import static com.example.android.autofillframework.CommonUtil.DEBUG;
+import static com.example.android.autofillframework.CommonUtil.TAG;
+import static com.example.android.autofillframework.CommonUtil.VERBOSE;
+
+/**
+ * Helper class for security checks.
+ */
+public final class SecurityHelper {
+
+    private static final String REST_TEMPLATE =
+            "https://digitalassetlinks.googleapis.com/v1/assetlinks:check?"
+                    + "source.web.site=%s&relation=delegate_permission/%s"
+                    + "&target.android_app.package_name=%s"
+                    + "&target.android_app.certificate.sha256_fingerprint=%s";
+
+    private static final String PERMISSION_GET_LOGIN_CREDS = "common.get_login_creds";
+    private static final String PERMISSION_HANDLE_ALL_URLS = "common.handle_all_urls";
+
+    private SecurityHelper() {
+        throw new UnsupportedOperationException("provides static methods only");
+    }
+
+    private static boolean isValidSync(String webDomain, String permission, String packageName,
+            String fingerprint) {
+        if (DEBUG) Log.d(TAG, "validating domain " + webDomain + " for pkg " + packageName
+                + " and fingerprint " + fingerprint + " for permission" + permission);
+        if (!webDomain.startsWith("http:") && !webDomain.startsWith("https:") ) {
+            // Unfortunately AssistStructure.ViewNode does not tell what the domain is, so let's
+            // assume it's https
+            webDomain = "https://" + webDomain;
+        }
+
+        String restUrl =
+                String.format(REST_TEMPLATE, webDomain, permission, packageName, fingerprint);
+        if (DEBUG) Log.d(TAG, "DAL REST request: " + restUrl);
+
+        HttpURLConnection urlConnection = null;
+        StringBuilder output = new StringBuilder();
+        try {
+            URL url = new URL(restUrl);
+            urlConnection = (HttpURLConnection) url.openConnection();
+            try (BufferedReader reader = new BufferedReader(
+                    new InputStreamReader(urlConnection.getInputStream()))) {
+                String line = null;
+                while ((line = reader.readLine()) != null) {
+                    output.append(line);
+                }
+            }
+            String response = output.toString();
+            if (VERBOSE) Log.v(TAG, "DAL REST Response: " + response);
+
+            JSONObject jsonObject = new JSONObject(response);
+            boolean valid = jsonObject.optBoolean("linked", false);
+            if (DEBUG) Log.d(TAG, "Valid: " + valid);
+
+            return valid;
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to validate", e);
+        } finally {
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+
+    }
+
+    private static boolean isValidSync(String webDomain, String packageName, String fingerprint) {
+        boolean isValid =
+                isValidSync(webDomain, PERMISSION_GET_LOGIN_CREDS, packageName, fingerprint);
+        if (!isValid) {
+            // Ideally we should only check for the get_login_creds, but not all domains set
+            // it yet, so validating for handle_all_urls gives a higher coverage.
+            if (DEBUG) {
+                Log.d(TAG, PERMISSION_GET_LOGIN_CREDS + " validation failed; trying "
+                        + PERMISSION_HANDLE_ALL_URLS);
+            }
+            isValid = isValidSync(webDomain, PERMISSION_HANDLE_ALL_URLS, packageName, fingerprint);
+        }
+        return isValid;
+    }
+
+
+    public static boolean isValid(String webDomain, String packageName, String fingerprint) {
+        if (DEBUG) Log.d(TAG, "validating domain " + webDomain + " for pkg " + packageName
+                + " and fingerprint " + fingerprint );
+        final String fullDomain;
+        if (!webDomain.startsWith("http:") && !webDomain.startsWith("https:") ) {
+            // Unfortunately AssistStructure.ViewNode does not tell what the domain is, so let's
+            // assume it's https
+            fullDomain = "https://" + webDomain;
+        } else {
+            fullDomain = webDomain;
+        }
+
+        // TODO: use the DAL Java API or a better REST alternative like Volley
+        // and/or document it should not block until it returns (for example, the server could
+        // start parsing the structure while it waits for the result.
+        AsyncTask<String, Integer, Boolean> task = new AsyncTask<String, Integer, Boolean>() {
+            @Override
+            protected Boolean doInBackground(String... strings) {
+                return isValidSync(fullDomain, packageName, fingerprint);
+            }
+        };
+        try {
+            return task.execute((String[]) null).get();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            Log.w(TAG, "Thread interrupted");
+        } catch (Exception e) {
+            Log.w(TAG, "Async task failed", e);
+        }
+        return false;
+    }
+
+    /**
+     * Gets the fingerprint of the signed certificate of a package.
+     */
+    public static String getFingerprint(Context context, String packageName) throws Exception {
+        PackageManager pm = context.getPackageManager();
+        PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+        Signature[] signatures = packageInfo.signatures;
+        if (signatures.length != 1) {
+            throw new SecurityException(packageName + " has " + signatures.length + " signatures");
+        }
+        byte[] cert = signatures[0].toByteArray();
+        try (InputStream input = new ByteArrayInputStream(cert)) {
+            CertificateFactory factory = CertificateFactory.getInstance("X509");
+            X509Certificate x509 = (X509Certificate) factory.generateCertificate(input);
+            MessageDigest md = MessageDigest.getInstance("SHA256");
+            byte[] publicKey = md.digest(x509.getEncoded());
+            return toHexFormat(publicKey);
+        }
+    }
+
+    private static String toHexFormat(byte[] bytes) {
+        StringBuilder builder = new StringBuilder(bytes.length * 2);
+        for (int i = 0; i < bytes.length; i++) {
+            String hex = Integer.toHexString(bytes[i]);
+            int length = hex.length();
+            if (length == 1) {
+                hex = "0" + hex;
+            }
+            if (length > 2) {
+                hex = hex.substring(length - 2, length);
+            }
+            builder.append(hex.toUpperCase());
+            if (i < (bytes.length - 1)) {
+                builder.append(':');
+            }
+        }
+        return builder.toString();
+    }
+}
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.java
index 812ba40..a912c0f 100644
--- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.java
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/StructureParser.java
@@ -18,13 +18,17 @@
 import android.app.assist.AssistStructure;
 import android.app.assist.AssistStructure.ViewNode;
 import android.app.assist.AssistStructure.WindowNode;
+import android.content.Context;
 import android.util.Log;
 import android.view.autofill.AutofillValue;
 
+import com.example.android.autofillframework.R;
+import com.example.android.autofillframework.multidatasetservice.datasource.SharedPrefsDigitalAssetLinksRepository;
 import com.example.android.autofillframework.multidatasetservice.model.FilledAutofillField;
 import com.example.android.autofillframework.multidatasetservice.model.FilledAutofillFieldCollection;
 
 import static com.example.android.autofillframework.CommonUtil.TAG;
+import static com.example.android.autofillframework.CommonUtil.DEBUG;
 
 /**
  * Parser for an AssistStructure object. This is invoked when the Autofill Service receives an
@@ -34,10 +38,12 @@
 final class StructureParser {
     private final AutofillFieldMetadataCollection mAutofillFields =
             new AutofillFieldMetadataCollection();
+    private final Context mContext;
     private final AssistStructure mStructure;
     private FilledAutofillFieldCollection mFilledAutofillFieldCollection;
 
-    StructureParser(AssistStructure structure) {
+    StructureParser(Context context, AssistStructure structure) {
+        mContext = context;
         mStructure = structure;
     }
 
@@ -53,17 +59,42 @@
      * Traverse AssistStructure and add ViewNode metadata to a flat list.
      */
     private void parse(boolean forFill) {
-        Log.d(TAG, "Parsing structure for " + mStructure.getActivityComponent());
+        if (DEBUG) Log.d(TAG, "Parsing structure for " + mStructure.getActivityComponent());
         int nodes = mStructure.getWindowNodeCount();
         mFilledAutofillFieldCollection = new FilledAutofillFieldCollection();
+        StringBuilder webDomain = new StringBuilder();
         for (int i = 0; i < nodes; i++) {
             WindowNode node = mStructure.getWindowNodeAt(i);
             ViewNode view = node.getRootViewNode();
-            parseLocked(forFill, view);
+            parseLocked(forFill, view, webDomain);
+        }
+        if (webDomain.length() > 0 ) {
+            String packageName = mStructure.getActivityComponent().getPackageName();
+            boolean valid = SharedPrefsDigitalAssetLinksRepository.getInstance().isValid(mContext,
+                    webDomain.toString(), packageName);
+            if (!valid) {
+                throw new SecurityException(mContext.getString(
+                        R.string.invalid_link_association, webDomain, packageName));
+            }
+            if (DEBUG) Log.d(TAG, "Domain " + webDomain + " is valid for " + packageName);
+        } else {
+            if (DEBUG) Log.d(TAG, "no web domain");
         }
     }
 
-    private void parseLocked(boolean forFill, ViewNode viewNode) {
+    private void parseLocked(boolean forFill, ViewNode viewNode, StringBuilder validWebDomain) {
+        String webDomain = viewNode.getWebDomain();
+        if (webDomain != null) {
+            if (DEBUG) Log.d(TAG, "child web domain: " + webDomain);
+            if (validWebDomain.length() > 0) {
+                if (!webDomain.equals(validWebDomain.toString())) {
+                    throw new SecurityException("Found multiple web domains: valid= "
+                            + validWebDomain + ", child=" + webDomain);
+                }
+            } else {
+                validWebDomain.append(webDomain);
+            }
+        }
 
         if (viewNode.getAutofillHints() != null) {
             String[] filteredHints = AutofillHints.filterForSupportedHints(
@@ -92,7 +123,7 @@
         int childrenSize = viewNode.getChildCount();
         if (childrenSize > 0) {
             for (int i = 0; i < childrenSize; i++) {
-                parseLocked(forFill, viewNode.getChildAt(i));
+                parseLocked(forFill, viewNode.getChildAt(i), validWebDomain);
             }
         }
     }
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/DigitalAssetLinksDataSource.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/DigitalAssetLinksDataSource.java
new file mode 100644
index 0000000..04624cb
--- /dev/null
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/DigitalAssetLinksDataSource.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 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.example.android.autofillframework.multidatasetservice.datasource;
+
+import android.content.Context;
+
+/**
+ * Helper format
+ * <a href="https://developers.google.com/digital-asset-links/">Digital Asset Links</a> needs.
+ */
+public interface DigitalAssetLinksDataSource {
+
+    /**
+     * Checks if the association between a web domain and a package is valid.
+     */
+    boolean isValid(Context context, String webDomain, String packageName);
+
+    /**
+     * Clears all cached data.
+     */
+    void clear(Context context);
+}
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.java
index 7b55ef2..91a1923 100644
--- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.java
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsAutofillRepository.java
@@ -28,8 +28,9 @@
 
 /**
  * Singleton autofill data repository that stores autofill fields to SharedPreferences.
- * Disclaimer: you should not store sensitive fields like user data unencrypted. This is done
- * here only for simplicity and learning purposes.
+ *
+ * <p><b>Disclaimer</b>: you should not store sensitive fields like user data unencrypted.
+ * This is done here only for simplicity and learning purposes.
  */
 public class SharedPrefsAutofillRepository implements AutofillDataSource {
     private static final String SHARED_PREF_KEY = "com.example.android.autofillframework"
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsDigitalAssetLinksRepository.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsDigitalAssetLinksRepository.java
new file mode 100644
index 0000000..fdfd657
--- /dev/null
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsDigitalAssetLinksRepository.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017 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.example.android.autofillframework.multidatasetservice.datasource;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.example.android.autofillframework.multidatasetservice.SecurityHelper;
+
+import static com.example.android.autofillframework.CommonUtil.TAG;
+
+/**
+ * Singleton repository that caches the result of Digital Asset Links checks.
+ */
+public class SharedPrefsDigitalAssetLinksRepository implements DigitalAssetLinksDataSource {
+
+    private static SharedPrefsDigitalAssetLinksRepository sInstance;
+
+    private SharedPrefsDigitalAssetLinksRepository() {
+    }
+
+    public static SharedPrefsDigitalAssetLinksRepository getInstance() {
+        if (sInstance == null) {
+            sInstance = new SharedPrefsDigitalAssetLinksRepository();
+        }
+        return sInstance;
+    }
+
+    @Override
+    public boolean isValid(Context context, String webDomain, String packageName) {
+        // TODO: implement caching. It could cache the whole domain -> (packagename, fingerprint),
+        // but then either invalidate when the package change or when the DAL association times out
+        // (the maxAge is part of the API response), or document that a real-life service
+        // should do that.
+
+        String fingerprint = null;
+        try {
+            fingerprint = SecurityHelper.getFingerprint(context, packageName);
+        } catch (Exception e) {
+            Log.w(TAG, "error getting fingerprint for " + packageName, e);
+            return false;
+        }
+        return SecurityHelper.isValid(webDomain,packageName,fingerprint);
+    }
+
+    @Override
+    public void clear(Context context) {
+        // TODO: implement once if caches results or remove from the interface
+    }
+}
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsPackageVerificationRepository.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsPackageVerificationRepository.java
index b7bb582..aa46778 100644
--- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsPackageVerificationRepository.java
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofillframework/multidatasetservice/datasource/SharedPrefsPackageVerificationRepository.java
@@ -17,16 +17,9 @@
 
 import android.content.Context;
 import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.Signature;
 import android.util.Log;
 
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.security.MessageDigest;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
+import com.example.android.autofillframework.multidatasetservice.SecurityHelper;
 
 import static com.example.android.autofillframework.CommonUtil.TAG;
 
@@ -58,7 +51,7 @@
     public boolean putPackageSignatures(Context context, String packageName) {
         String hash;
         try {
-            hash = getCertificateHash(context, packageName);
+            hash = SecurityHelper.getFingerprint(context, packageName);
             Log.d(TAG, "Hash for " + packageName + ": " + hash);
         } catch (Exception e) {
             Log.w(TAG, "Error getting hash for " + packageName + ": " + e);
@@ -89,38 +82,4 @@
                 SHARED_PREF_KEY, Context.MODE_PRIVATE);
         return hash.equals(prefs.getString(packageName, null));
     }
-
-    private String getCertificateHash(Context context, String packageName)
-            throws Exception {
-        PackageManager pm = context.getPackageManager();
-        PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
-        Signature[] signatures = packageInfo.signatures;
-        byte[] cert = signatures[0].toByteArray();
-        try (InputStream input = new ByteArrayInputStream(cert)) {
-            CertificateFactory factory = CertificateFactory.getInstance("X509");
-            X509Certificate x509 = (X509Certificate) factory.generateCertificate(input);
-            MessageDigest md = MessageDigest.getInstance("SHA256");
-            byte[] publicKey = md.digest(x509.getEncoded());
-            return toHexFormat(publicKey);
-        }
-    }
-
-    private String toHexFormat(byte[] bytes) {
-        StringBuilder builder = new StringBuilder(bytes.length * 2);
-        for (int i = 0; i < bytes.length; i++) {
-            String hex = Integer.toHexString(bytes[i]);
-            int length = hex.length();
-            if (length == 1) {
-                hex = "0" + hex;
-            }
-            if (length > 2) {
-                hex = hex.substring(length - 2, length);
-            }
-            builder.append(hex.toUpperCase());
-            if (i < (bytes.length - 1)) {
-                builder.append(':');
-            }
-        }
-        return builder.toString();
-    }
 }
diff --git a/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml b/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml
index 229cf9e..01a743b 100644
--- a/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml
+++ b/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml
@@ -79,6 +79,7 @@
     <string name="cc_exp_month_description">Credit Card Expiration Month</string>
     <string name="cc_exp_year_description">Credit Card Expiration Year</string>
     <string name="invalid_package_signature">Invalid package signature</string>
+    <string name="invalid_link_association">Could not associate web domain %1$s with app %2$s</string>
     <string name="edittext_login_info">This is a sample login page that uses standard EditTexts
         from the UI toolkit. EditTexts are already optimized for autofill so extra autofill-specific
         code is almost never needed.