Credential FRP: Add implementation

- Adds a facility to store a credential handle that survives factory reset
- Adds a method to KeyguardManager for verifying the stored credential for SetupWizard
- Dark launches persisting the primary user's credential as the FRP credential (behind a default-off flag)

Future work:
- Use a separate GK handle / synthetic password for the FRP credential
- Enroll the FRP credential in verifyCredential for the upgrade case

Bug: 36814845
Test: runtest -x core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java && runtest -x services/tests/servicestests/src/com/android/server/LockSettingsStorageTests.java && runtest -x services/tests/servicestests/src/com/android/server/SyntheticPasswordTests.java
Change-Id: Ia739408c5ecb169e5f09670cd9ceaa7febc2b1cc
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index fcf0aab..4a0b644 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -28,11 +28,12 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
 import android.os.UserHandle;
+import android.provider.Settings;
+import android.service.persistentdata.IPersistentDataBlockService;
 import android.util.Log;
 import android.view.IOnKeyguardExitResult;
 import android.view.IWindowManager;
@@ -40,6 +41,7 @@
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.widget.LockPatternUtils;
 
 import java.util.List;
 
@@ -74,6 +76,13 @@
             "android.app.action.CONFIRM_DEVICE_CREDENTIAL_WITH_USER";
 
     /**
+     * Intent used to prompt user for factory reset credentials.
+     * @hide
+     */
+    public static final String ACTION_CONFIRM_FRP_CREDENTIAL =
+            "android.app.action.CONFIRM_FRP_CREDENTIAL";
+
+    /**
      * A CharSequence dialog title to show to the user when used with a
      * {@link #ACTION_CONFIRM_DEVICE_CREDENTIAL}.
      * @hide
@@ -88,6 +97,23 @@
     public static final String EXTRA_DESCRIPTION = "android.app.extra.DESCRIPTION";
 
     /**
+     * A CharSequence description to show to the user on the alternate button when used with
+     * {@link #ACTION_CONFIRM_FRP_CREDENTIAL}.
+     * @hide
+     */
+    public static final String EXTRA_ALTERNATE_BUTTON_LABEL =
+            "android.app.extra.ALTERNATE_BUTTON_LABEL";
+
+    /**
+     * Result code returned by the activity started by
+     * {@link #createConfirmFactoryResetCredentialIntent} indicating that the user clicked the
+     * alternate button.
+     *
+     * @hide
+     */
+    public static final int RESULT_ALTERNATE = 1;
+
+    /**
      * Get an intent to prompt the user to confirm credentials (pin, pattern or password)
      * for the current user of the device. The caller is expected to launch this activity using
      * {@link android.app.Activity#startActivityForResult(Intent, int)} and check for
@@ -130,6 +156,63 @@
         return intent;
     }
 
+    /**
+     * Get an intent to prompt the user to confirm credentials (pin, pattern or password)
+     * for the previous owner of the device. The caller is expected to launch this activity using
+     * {@link android.app.Activity#startActivityForResult(Intent, int)} and check for
+     * {@link android.app.Activity#RESULT_OK} if the user successfully completes the challenge.
+     *
+     * @param alternateButtonLabel if not empty, a button is provided with the given label. Upon
+     *                             clicking this button, the activity returns
+     *                             {@link #RESULT_ALTERNATE}
+     *
+     * @return  the intent for launching the activity or null if the credential of the previous
+     * owner can not be verified (e.g. because there was none, or the device does not support
+     * verifying credentials after a factory reset, or device setup has already been completed).
+     *
+     * @hide
+     */
+    public Intent createConfirmFactoryResetCredentialIntent(
+            CharSequence title, CharSequence description, CharSequence alternateButtonLabel) {
+        if (!LockPatternUtils.frpCredentialEnabled()) {
+            Log.w(TAG, "Factory reset credentials not supported.");
+            return null;
+        }
+
+        // Cannot verify credential if the device is provisioned
+        if (Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
+            Log.e(TAG, "Factory reset credential cannot be verified after provisioning.");
+            return null;
+        }
+
+        // Make sure we have a credential
+        try {
+            IPersistentDataBlockService pdb = IPersistentDataBlockService.Stub.asInterface(
+                    ServiceManager.getService(Context.PERSISTENT_DATA_BLOCK_SERVICE));
+            if (pdb == null) {
+                Log.e(TAG, "No persistent data block service");
+                return null;
+            }
+            if (!pdb.hasFrpCredentialHandle()) {
+                Log.i(TAG, "The persistent data block does not have a factory reset credential.");
+                return null;
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        Intent intent = new Intent(ACTION_CONFIRM_FRP_CREDENTIAL);
+        intent.putExtra(EXTRA_TITLE, title);
+        intent.putExtra(EXTRA_DESCRIPTION, description);
+        intent.putExtra(EXTRA_ALTERNATE_BUTTON_LABEL, alternateButtonLabel);
+
+        // explicitly set the package for security
+        intent.setPackage(getSettingsPackageForIntent(intent));
+
+        return intent;
+    }
+
     private String getSettingsPackageForIntent(Intent intent) {
         List<ResolveInfo> resolveInfos = mContext.getPackageManager()
                 .queryIntentActivities(intent, PackageManager.MATCH_SYSTEM_ONLY);
diff --git a/core/java/android/service/gatekeeper/IGateKeeperService.aidl b/core/java/android/service/gatekeeper/IGateKeeperService.aidl
index 6db2110..abc6466 100644
--- a/core/java/android/service/gatekeeper/IGateKeeperService.aidl
+++ b/core/java/android/service/gatekeeper/IGateKeeperService.aidl
@@ -78,4 +78,10 @@
      * @param uid the Android user id.
      */
     void clearSecureUserId(int uid);
+
+    /**
+     * Notifies gatekeeper that device setup has been completed and any potentially still existing
+     * state from before a factory reset can be cleaned up (if it has not been already).
+     */
+    void reportDeviceSetupComplete();
 }
diff --git a/core/java/android/service/persistentdata/IPersistentDataBlockService.aidl b/core/java/android/service/persistentdata/IPersistentDataBlockService.aidl
index 626b408..31352f1 100644
--- a/core/java/android/service/persistentdata/IPersistentDataBlockService.aidl
+++ b/core/java/android/service/persistentdata/IPersistentDataBlockService.aidl
@@ -36,5 +36,6 @@
     void setOemUnlockEnabled(boolean enabled);
     boolean getOemUnlockEnabled();
     int getFlashLockState();
+    boolean hasFrpCredentialHandle();
 }
 
diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl
index b8c062e..ee16ab6 100644
--- a/core/java/com/android/internal/widget/ILockSettings.aidl
+++ b/core/java/com/android/internal/widget/ILockSettings.aidl
@@ -28,7 +28,7 @@
     boolean getBoolean(in String key, in boolean defaultValue, in int userId);
     long getLong(in String key, in long defaultValue, in int userId);
     String getString(in String key, in String defaultValue, in int userId);
-    void setLockCredential(in String credential, int type, in String savedCredential, int userId);
+    void setLockCredential(in String credential, int type, in String savedCredential, int requestedQuality, int userId);
     void resetKeyStore(int userId);
     VerifyCredentialResponse checkCredential(in String credential, int type, int userId,
             in ICheckCredentialProgressCallback progressCallback);
@@ -49,6 +49,7 @@
     long addEscrowToken(in byte[] token, int userId);
     boolean removeEscrowToken(long handle, int userId);
     boolean isEscrowTokenActive(long handle, int userId);
-    boolean setLockCredentialWithToken(String credential, int type, long tokenHandle, in byte[] token, int userId);
+    boolean setLockCredentialWithToken(String credential, int type, long tokenHandle,
+            in byte[] token, int requestedQuality, int userId);
     void unlockUserWithToken(long tokenHandle, in byte[] token, int userId);
 }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 51aef73..d476ea0 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -27,6 +27,7 @@
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.AsyncTask;
+import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -34,6 +35,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.IStorageManager;
@@ -65,6 +67,8 @@
 
     private static final String TAG = "LockPatternUtils";
     private static final boolean DEBUG = false;
+    private static final boolean FRP_CREDENTIAL_ENABLED =
+            Build.IS_DEBUGGABLE && SystemProperties.getBoolean("debug.frpcredential.enable", false);
 
     /**
      * The key to identify when the lock pattern enabled flag is being accessed for legacy reasons.
@@ -112,6 +116,11 @@
 
     public static final int CREDENTIAL_TYPE_PASSWORD = 2;
 
+    /**
+     * Special user id for triggering the FRP verification flow.
+     */
+    public static final int USER_FRP = UserHandle.USER_NULL + 1;
+
     @Deprecated
     public final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently";
     public final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline";
@@ -295,24 +304,39 @@
     }
 
     public void reportFailedPasswordAttempt(int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return;
+        }
         getDevicePolicyManager().reportFailedPasswordAttempt(userId);
         getTrustManager().reportUnlockAttempt(false /* authenticated */, userId);
     }
 
     public void reportSuccessfulPasswordAttempt(int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return;
+        }
         getDevicePolicyManager().reportSuccessfulPasswordAttempt(userId);
         getTrustManager().reportUnlockAttempt(true /* authenticated */, userId);
     }
 
     public void reportPasswordLockout(int timeoutMs, int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return;
+        }
         getTrustManager().reportUnlockLockout(timeoutMs, userId);
     }
 
     public int getCurrentFailedPasswordAttempts(int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return 0;
+        }
         return getDevicePolicyManager().getCurrentFailedPasswordAttempts(userId);
     }
 
     public int getMaximumFailedPasswordsForWipe(int userId) {
+        if (userId == USER_FRP && frpCredentialEnabled()) {
+            return 0;
+        }
         return getDevicePolicyManager().getMaximumFailedPasswordsForWipe(
                 null /* componentName */, userId);
     }
@@ -586,7 +610,7 @@
 
         try{
             getLockSettings().setLockCredential(null, CREDENTIAL_TYPE_NONE, savedCredential,
-                    userHandle);
+                    DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userHandle);
         } catch (RemoteException e) {
             // well, we tried...
         }
@@ -651,7 +675,7 @@
 
             setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId);
             getLockSettings().setLockCredential(patternToString(pattern), CREDENTIAL_TYPE_PATTERN,
-                    savedPattern, userId);
+                    savedPattern, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId);
 
             // Update the device encryption password.
             if (userId == UserHandle.USER_SYSTEM
@@ -765,10 +789,10 @@
      * password.
      * @param password The password to save
      * @param savedPassword The previously saved lock password, or null if none
-     * @param quality {@see DevicePolicyManager#getPasswordQuality(android.content.ComponentName)}
+     * @param requestedQuality {@see DevicePolicyManager#getPasswordQuality(android.content.ComponentName)}
      * @param userHandle The userId of the user to change the password for
      */
-    public void saveLockPassword(String password, String savedPassword, int quality,
+    public void saveLockPassword(String password, String savedPassword, int requestedQuality,
             int userHandle) {
         try {
             if (password == null || password.length() < MIN_LOCK_PASSWORD_SIZE) {
@@ -777,9 +801,9 @@
             }
 
             final int computedQuality = PasswordMetrics.computeForPassword(password).quality;
-            setLong(PASSWORD_TYPE_KEY, Math.max(quality, computedQuality), userHandle);
+            setLong(PASSWORD_TYPE_KEY, Math.max(requestedQuality, computedQuality), userHandle);
             getLockSettings().setLockCredential(password, CREDENTIAL_TYPE_PASSWORD, savedPassword,
-                    userHandle);
+                    requestedQuality, userHandle);
 
             updateEncryptionPasswordIfNeeded(password, computedQuality, userHandle);
             updatePasswordHistory(password, userHandle);
@@ -1474,12 +1498,13 @@
                 }
 
                 final int computedQuality = PasswordMetrics.computeForPassword(credential).quality;
+                int quality = Math.max(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC,
+                        computedQuality);
                 if (!getLockSettings().setLockCredentialWithToken(credential, type, tokenHandle,
-                        token, userId)) {
+                        token, quality, userId)) {
                     return false;
                 }
-                setLong(PASSWORD_TYPE_KEY, Math.max(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC,
-                        computedQuality), userId);
+                setLong(PASSWORD_TYPE_KEY, quality, userId);
 
                 updateEncryptionPasswordIfNeeded(credential, computedQuality, userId);
                 updatePasswordHistory(credential, userId);
@@ -1488,7 +1513,8 @@
                     throw new IllegalArgumentException("password must be emtpy for NONE type");
                 }
                 if (!getLockSettings().setLockCredentialWithToken(null, CREDENTIAL_TYPE_NONE,
-                        tokenHandle, token, userId)) {
+                        tokenHandle, token, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED,
+                        userId)) {
                     return false;
                 }
                 setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED,
@@ -1691,4 +1717,12 @@
     public boolean isSyntheticPasswordEnabled() {
         return getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM) != 0;
     }
+
+    public static boolean userOwnsFrpCredential(UserInfo info) {
+        return info != null && info.isPrimary() && info.isAdmin() && frpCredentialEnabled();
+    }
+
+    public static boolean frpCredentialEnabled() {
+        return FRP_CREDENTIAL_ENABLED;
+    }
 }
diff --git a/core/java/com/android/internal/widget/VerifyCredentialResponse.java b/core/java/com/android/internal/widget/VerifyCredentialResponse.java
index 48109ca..ad6020c 100644
--- a/core/java/com/android/internal/widget/VerifyCredentialResponse.java
+++ b/core/java/com/android/internal/widget/VerifyCredentialResponse.java
@@ -18,6 +18,8 @@
 
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.service.gatekeeper.GateKeeperResponse;
+import android.util.Slog;
 
 /**
  * Response object for a ILockSettings credential verification request.
@@ -32,6 +34,7 @@
     public static final VerifyCredentialResponse OK = new VerifyCredentialResponse();
     public static final VerifyCredentialResponse ERROR
             = new VerifyCredentialResponse(RESPONSE_ERROR, 0, null);
+    private static final String TAG = "VerifyCredentialResponse";
 
     private int mResponseCode;
     private byte[] mPayload;
@@ -123,4 +126,29 @@
     private void setPayload(byte[] payload) {
         mPayload = payload;
     }
+
+    public VerifyCredentialResponse stripPayload() {
+        return new VerifyCredentialResponse(mResponseCode, mTimeout, new byte[0]);
+    }
+
+    public static VerifyCredentialResponse fromGateKeeperResponse(
+            GateKeeperResponse gateKeeperResponse) {
+        VerifyCredentialResponse response;
+        int responseCode = gateKeeperResponse.getResponseCode();
+        if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+            response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
+        } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+            byte[] token = gateKeeperResponse.getPayload();
+            if (token == null) {
+                // something's wrong if there's no payload with a challenge
+                Slog.e(TAG, "verifyChallenge response had no associated payload");
+                response = VerifyCredentialResponse.ERROR;
+            } else {
+                response = new VerifyCredentialResponse(token);
+            }
+        } else {
+            response = VerifyCredentialResponse.ERROR;
+        }
+        return response;
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java
new file mode 100644
index 0000000..f73950a
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.android.internal.widget;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.UserHandle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LockPatternUtilsTest {
+
+    @Test
+    public void testUserFrp_isNotRegularUser() throws Exception {
+        assertTrue(LockPatternUtils.USER_FRP < 0);
+    }
+
+    @Test
+    public void testUserFrp_isNotAReservedSpecialUser() throws Exception {
+        assertNotEquals(UserHandle.USER_NULL, LockPatternUtils.USER_FRP);
+        assertNotEquals(UserHandle.USER_ALL, LockPatternUtils.USER_FRP);
+        assertNotEquals(UserHandle.USER_CURRENT, LockPatternUtils.USER_FRP);
+        assertNotEquals(UserHandle.USER_CURRENT_OR_SELF, LockPatternUtils.USER_FRP);
+    }
+}
diff --git a/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java b/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java
new file mode 100644
index 0000000..80f8e51
--- /dev/null
+++ b/services/core/java/com/android/server/PersistentDataBlockManagerInternal.java
@@ -0,0 +1,29 @@
+/*
+ * 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.android.server;
+
+/**
+ * Internal interface for storing and retrieving persistent data.
+ */
+public interface PersistentDataBlockManagerInternal {
+
+    /** Stores the handle to a lockscreen credential to be used for Factory Reset Protection. */
+    void setFrpCredentialHandle(byte[] handle);
+
+    /** Retrieves handle to a lockscreen credential to be used for Factory Reset Protection. */
+    byte[] getFrpCredentialHandle();
+}
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index e3cd87c..1d4c3db 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -32,6 +32,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
 
 import libcore.io.IoUtils;
 
@@ -71,8 +72,13 @@
     private static final int HEADER_SIZE = 8;
     // Magic number to mark block device as adhering to the format consumed by this service
     private static final int PARTITION_TYPE_MARKER = 0x19901873;
+    /** Size of the block reserved for FPR credential, including 4 bytes for the size header. */
+    private static final int FRP_CREDENTIAL_RESERVED_SIZE = 1000;
+    /** Maximum size of the FRP credential handle that can be stored. */
+    private static final int MAX_FRP_CREDENTIAL_HANDLE_SIZE = FRP_CREDENTIAL_RESERVED_SIZE - 4;
     // Limit to 100k as blocks larger than this might cause strain on Binder.
     private static final int MAX_DATA_BLOCK_SIZE = 1024 * 100;
+
     public static final int DIGEST_SIZE_BYTES = 32;
     private static final String OEM_UNLOCK_PROP = "sys.oem_unlock_allowed";
     private static final String FLASH_LOCK_PROP = "ro.boot.flash.locked";
@@ -136,6 +142,7 @@
                 Thread.currentThread().interrupt();
                 throw new IllegalStateException("Service " + TAG + " init interrupted", e);
             }
+            LocalServices.addService(PersistentDataBlockManagerInternal.class, mInternalService);
         }
         super.onBootPhase(phase);
     }
@@ -382,7 +389,7 @@
             enforceUid(Binder.getCallingUid());
 
             // Need to ensure we don't write over the last byte
-            long maxBlockSize = getBlockDeviceSize() - HEADER_SIZE - 1;
+            long maxBlockSize = getMaximumDataBlockSize();
             if (data.length > maxBlockSize) {
                 // partition is ~500k so shouldn't be a problem to downcast
                 return (int) -maxBlockSize;
@@ -562,8 +569,99 @@
 
         @Override
         public long getMaximumDataBlockSize() {
-            long actualSize = getBlockDeviceSize() - HEADER_SIZE - 1;
+            long actualSize = getBlockDeviceSize() - HEADER_SIZE - DIGEST_SIZE_BYTES
+                    - FRP_CREDENTIAL_RESERVED_SIZE - 1;
             return actualSize <= MAX_DATA_BLOCK_SIZE ? actualSize : MAX_DATA_BLOCK_SIZE;
         }
+
+        @Override
+        public boolean hasFrpCredentialHandle() {
+            enforcePersistentDataBlockAccess();
+            return mInternalService.getFrpCredentialHandle() != null;
+        }
+    };
+
+    private PersistentDataBlockManagerInternal mInternalService =
+            new PersistentDataBlockManagerInternal() {
+
+        @Override
+        public void setFrpCredentialHandle(byte[] handle) {
+            Preconditions.checkArgument(handle == null || handle.length > 0,
+                    "handle must be null or non-empty");
+            Preconditions.checkArgument(handle == null
+                            || handle.length <= MAX_FRP_CREDENTIAL_HANDLE_SIZE,
+                    "handle must not be longer than " + MAX_FRP_CREDENTIAL_HANDLE_SIZE);
+
+            FileOutputStream outputStream;
+            try {
+                outputStream = new FileOutputStream(new File(mDataBlockFile));
+            } catch (FileNotFoundException e) {
+                Slog.e(TAG, "partition not available", e);
+                return;
+            }
+
+            ByteBuffer data = ByteBuffer.allocate(FRP_CREDENTIAL_RESERVED_SIZE);
+            data.putInt(handle == null ? 0 : handle.length);
+            if (handle != null) {
+                data.put(handle);
+            }
+            data.flip();
+
+            synchronized (mLock) {
+                if (!mIsWritable) {
+                    IoUtils.closeQuietly(outputStream);
+                    return;
+                }
+
+                try {
+                    FileChannel channel = outputStream.getChannel();
+
+                    channel.position(getBlockDeviceSize() - 1 - FRP_CREDENTIAL_RESERVED_SIZE);
+                    channel.write(data);
+                    outputStream.flush();
+                } catch (IOException e) {
+                    Slog.e(TAG, "unable to access persistent partition", e);
+                    return;
+                } finally {
+                    IoUtils.closeQuietly(outputStream);
+                }
+
+                computeAndWriteDigestLocked();
+            }
+        }
+
+        @Override
+        public byte[] getFrpCredentialHandle() {
+            if (!enforceChecksumValidity()) {
+                return null;
+            }
+
+            DataInputStream inputStream;
+            try {
+                inputStream = new DataInputStream(
+                        new FileInputStream(new File(mDataBlockFile)));
+            } catch (FileNotFoundException e) {
+                Slog.e(TAG, "partition not available");
+                return null;
+            }
+
+            try {
+                synchronized (mLock) {
+                    inputStream.skip(getBlockDeviceSize() - 1 - FRP_CREDENTIAL_RESERVED_SIZE);
+                    int length = inputStream.readInt();
+                    if (length <= 0 || length > MAX_FRP_CREDENTIAL_HANDLE_SIZE) {
+                        return null;
+                    }
+                    byte[] bytes = new byte[length];
+                    inputStream.readFully(bytes);
+                    return bytes;
+                }
+            } catch (IOException e) {
+                Slog.e(TAG, "unable to access persistent partition", e);
+                return null;
+            } finally {
+                IoUtils.closeQuietly(inputStream);
+            }
+        }
     };
 }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 270dcbe..321b660 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -19,9 +19,12 @@
 import static android.Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE;
 import static android.Manifest.permission.READ_CONTACTS;
 import static android.content.Context.KEYGUARD_SERVICE;
+
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
 import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_ENABLED_KEY;
 import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_HANDLE_KEY;
+import static com.android.internal.widget.LockPatternUtils.USER_FRP;
+import static com.android.internal.widget.LockPatternUtils.frpCredentialEnabled;
 
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
@@ -43,7 +46,9 @@
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
@@ -63,7 +68,6 @@
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.provider.Settings.SettingNotFoundException;
-import android.security.GateKeeper;
 import android.security.KeyStore;
 import android.security.keystore.AndroidKeyStoreProvider;
 import android.security.keystore.KeyProperties;
@@ -80,6 +84,7 @@
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.Preconditions;
 import com.android.internal.widget.ICheckCredentialProgressCallback;
 import com.android.internal.widget.ILockSettings;
 import com.android.internal.widget.LockPatternUtils;
@@ -88,6 +93,7 @@
 import com.android.server.locksettings.LockSettingsStorage.CredentialHash;
 import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationResult;
 import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationToken;
+import com.android.server.locksettings.LockSettingsStorage.PersistentData;
 
 import libcore.util.HexEncoding;
 
@@ -105,7 +111,6 @@
 import java.security.SecureRandom;
 import java.security.UnrecoverableKeyException;
 import java.security.cert.CertificateException;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -138,6 +143,9 @@
     // Order of holding lock: mSeparateChallengeLock -> mSpManager -> this
     private final Object mSeparateChallengeLock = new Object();
 
+    private final DeviceProvisionedObserver mDeviceProvisionedObserver =
+            new DeviceProvisionedObserver();
+
     private final Injector mInjector;
     private final Context mContext;
     private final Handler mHandler;
@@ -266,13 +274,13 @@
         try {
             randomLockSeed = SecureRandom.getInstance("SHA1PRNG").generateSeed(40);
             String newPassword = String.valueOf(HexEncoding.encode(randomLockSeed));
+            final int quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
             setLockCredentialInternal(newPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
-                    managedUserPassword, managedUserId);
+                    managedUserPassword, quality, managedUserId);
             // We store a private credential for the managed user that's unlocked by the primary
             // account holder's credential. As such, the user will never be prompted to enter this
             // password directly, so we always store a password.
-            setLong(LockPatternUtils.PASSWORD_TYPE_KEY,
-                    DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, managedUserId);
+            setLong(LockPatternUtils.PASSWORD_TYPE_KEY, quality, managedUserId);
             tieProfileLockToParent(managedUserId, newPassword);
         } catch (NoSuchAlgorithmException | RemoteException e) {
             Slog.e(TAG, "Fail to tie managed profile", e);
@@ -353,7 +361,7 @@
         }
 
         public SyntheticPasswordManager getSyntheticPasswordManager(LockSettingsStorage storage) {
-            return new SyntheticPasswordManager(storage);
+            return new SyntheticPasswordManager(storage, getUserManager());
         }
 
         public int binderGetCallingUid() {
@@ -547,6 +555,7 @@
         } catch (RemoteException e) {
             Slog.e(TAG, "Failure retrieving IGateKeeperService", e);
         }
+        mDeviceProvisionedObserver.onSystemReady();
         // TODO: maybe skip this for split system user mode.
         mStorage.prefetchUser(UserHandle.USER_SYSTEM);
     }
@@ -783,6 +792,8 @@
     }
 
     private void setStringUnchecked(String key, int userId, String value) {
+        Preconditions.checkArgument(userId != USER_FRP, "cannot store lock settings for FRP user");
+
         mStorage.writeKeyValue(key, value, userId);
         if (ArrayUtils.contains(SETTINGS_TO_BACKUP, key)) {
             BackupManager.dataChanged("com.android.providers.settings");
@@ -820,6 +831,10 @@
             }
         }
 
+        if (userId == USER_FRP) {
+            return getFrpStringUnchecked(key);
+        }
+
         if (LockPatternUtils.LEGACY_LOCK_PATTERN_ENABLED.equals(key)) {
             key = Settings.Secure.LOCK_PATTERN_ENABLED;
         }
@@ -827,6 +842,17 @@
         return mStorage.readKeyValue(key, defaultValue, userId);
     }
 
+    private String getFrpStringUnchecked(String key) {
+        if (LockPatternUtils.PASSWORD_TYPE_KEY.equals(key)) {
+            return String.valueOf(readFrpPasswordQuality());
+        }
+        return null;
+    }
+
+    private int readFrpPasswordQuality() {
+        return mStorage.readPersistentDataBlock().qualityForUi;
+    }
+
     @Override
     public boolean havePassword(int userId) throws RemoteException {
         synchronized (mSpManager) {
@@ -1036,12 +1062,13 @@
                     // credential, otherwise they get lost
                     if (profilePasswordMap != null && profilePasswordMap.containsKey(managedUserId)) {
                         setLockCredentialInternal(null, LockPatternUtils.CREDENTIAL_TYPE_NONE,
-                                profilePasswordMap.get(managedUserId), managedUserId);
+                                profilePasswordMap.get(managedUserId),
+                                DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, managedUserId);
                     } else {
                         Slog.wtf(TAG, "clear tied profile challenges, but no password supplied.");
                         // Supplying null here would lead to untrusted credential change
                         setLockCredentialInternal(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, null,
-                                managedUserId);
+                                DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, managedUserId);
                     }
                     mStorage.removeChildProfileLock(managedUserId);
                     removeKeystoreProfileKey(managedUserId);
@@ -1063,18 +1090,19 @@
     // This method should be called by LockPatternUtil only, all internal methods in this class
     // should call setLockCredentialInternal.
     @Override
-    public void setLockCredential(String credential, int type, String savedCredential, int userId)
+    public void setLockCredential(String credential, int type, String savedCredential,
+            int requestedQuality, int userId)
             throws RemoteException {
         checkWritePermission(userId);
         synchronized (mSeparateChallengeLock) {
-            setLockCredentialInternal(credential, type, savedCredential, userId);
+            setLockCredentialInternal(credential, type, savedCredential, requestedQuality, userId);
             setSeparateProfileChallengeEnabled(userId, true, null);
             notifyPasswordChanged(userId);
         }
     }
 
     private void setLockCredentialInternal(String credential, int credentialType,
-            String savedCredential, int userId) throws RemoteException {
+            String savedCredential, int requestedQuality, int userId) throws RemoteException {
         // Normalize savedCredential and credential such that empty string is always represented
         // as null.
         if (TextUtils.isEmpty(savedCredential)) {
@@ -1086,10 +1114,11 @@
         synchronized (mSpManager) {
             if (isSyntheticPasswordBasedCredentialLocked(userId)) {
                 spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential,
-                        userId);
+                        requestedQuality, userId);
                 return;
             }
         }
+
         if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
             if (credential != null) {
                 Slog.wtf(TAG, "CredentialType is none, but credential is non-null.");
@@ -1101,6 +1130,12 @@
             fixateNewestUserKeyAuth(userId);
             synchronizeUnifiedWorkChallengeForProfiles(userId, null);
             notifyActivePasswordMetricsAvailable(null, userId);
+
+            if (mStorage.getPersistentDataBlock() != null
+                    && LockPatternUtils.userOwnsFrpCredential(mUserManager.getUserInfo(userId))) {
+                // If owner, write to persistent storage for FRP
+                mStorage.writePersistentDataBlock(PersistentData.TYPE_NONE, userId, 0, null);
+            }
             return;
         }
         if (credential == null) {
@@ -1133,9 +1168,9 @@
         synchronized (mSpManager) {
             if (shouldMigrateToSyntheticPasswordLocked(userId)) {
                 initializeSyntheticPasswordLocked(currentHandle.hash, savedCredential,
-                        currentHandle.type, userId);
+                        currentHandle.type, requestedQuality, userId);
                 spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential,
-                        userId);
+                        requestedQuality, userId);
                 return;
             }
         }
@@ -1153,6 +1188,12 @@
             // Refresh the auth token
             doVerifyCredential(credential, credentialType, true, 0, userId, null /* progressCallback */);
             synchronizeUnifiedWorkChallengeForProfiles(userId, null);
+            if (mStorage.getPersistentDataBlock() != null
+                    && LockPatternUtils.userOwnsFrpCredential(mUserManager.getUserInfo(userId))) {
+                // If owner, write to persistent storage for FRP
+                mStorage.writePersistentDataBlock(PersistentData.TYPE_GATEKEEPER, userId,
+                        requestedQuality, willStore.toBytes());
+            }
         } else {
             throw new RemoteException("Failed to enroll " +
                     (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD ? "password"
@@ -1161,23 +1202,7 @@
     }
 
     private VerifyCredentialResponse convertResponse(GateKeeperResponse gateKeeperResponse) {
-        VerifyCredentialResponse response;
-        int responseCode = gateKeeperResponse.getResponseCode();
-        if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
-            response = new VerifyCredentialResponse(gateKeeperResponse.getTimeout());
-        } else if (responseCode == GateKeeperResponse.RESPONSE_OK) {
-            byte[] token = gateKeeperResponse.getPayload();
-            if (token == null) {
-                // something's wrong if there's no payload with a challenge
-                Slog.e(TAG, "verifyChallenge response had no associated payload");
-                response = VerifyCredentialResponse.ERROR;
-            } else {
-                response = new VerifyCredentialResponse(token);
-            }
-        } else {
-            response = VerifyCredentialResponse.ERROR;
-        }
-        return response;
+        return VerifyCredentialResponse.fromGateKeeperResponse(gateKeeperResponse);
     }
 
     @VisibleForTesting
@@ -1403,6 +1428,11 @@
         if (TextUtils.isEmpty(credential)) {
             throw new IllegalArgumentException("Credential can't be null or empty");
         }
+        if (userId == USER_FRP && Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
+            Slog.e(TAG, "FRP credential can only be verified prior to provisioning.");
+            return VerifyCredentialResponse.ERROR;
+        }
         synchronized (mSpManager) {
             if (isSyntheticPasswordBasedCredentialLocked(userId)) {
                 VerifyCredentialResponse response = spBasedDoVerifyCredentialLocked(credential,
@@ -1413,7 +1443,18 @@
                 return response;
             }
         }
-        CredentialHash storedHash = mStorage.readCredentialHash(userId);
+        final CredentialHash storedHash;
+        if (userId == USER_FRP) {
+            PersistentData data = mStorage.readPersistentDataBlock();
+            if (data.type != PersistentData.TYPE_GATEKEEPER) {
+                Slog.wtf(TAG, "Expected PersistentData.TYPE_GATEKEEPER, but was: " + data.type);
+                return VerifyCredentialResponse.ERROR;
+            }
+            return verifyFrpCredential(credential, credentialType, data, progressCallback);
+        } else {
+            storedHash = mStorage.readCredentialHash(userId);
+        }
+
         if (storedHash.type != credentialType) {
             Slog.wtf(TAG, "doVerifyCredential type mismatch with stored credential??"
                     + " stored: " + storedHash.type + " passed in: " + credentialType);
@@ -1436,13 +1477,37 @@
         if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
             mStrongAuth.reportSuccessfulStrongAuthUnlock(userId);
             if (shouldReEnrollBaseZero) {
-                setLockCredentialInternal(credential, storedHash.type, credentialToVerify, userId);
+                setLockCredentialInternal(credential, storedHash.type, credentialToVerify,
+                        DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, userId);
             }
         }
 
         return response;
     }
 
+    private VerifyCredentialResponse verifyFrpCredential(String credential, int credentialType,
+            PersistentData data, ICheckCredentialProgressCallback progressCallback)
+            throws RemoteException {
+        CredentialHash storedHash = CredentialHash.fromBytes(data.payload);
+        if (storedHash.type != credentialType) {
+            Slog.wtf(TAG, "doVerifyCredential type mismatch with stored credential??"
+                    + " stored: " + storedHash.type + " passed in: " + credentialType);
+            return VerifyCredentialResponse.ERROR;
+        }
+        if (ArrayUtils.isEmpty(storedHash.hash) || TextUtils.isEmpty(credential)) {
+            Slog.e(TAG, "Stored hash or credential is empty");
+            return VerifyCredentialResponse.ERROR;
+        }
+        VerifyCredentialResponse response = VerifyCredentialResponse.fromGateKeeperResponse(
+                getGateKeeperService().verifyChallenge(data.userId, 0 /* challenge */,
+                        storedHash.hash, credential.getBytes()));
+        if (progressCallback != null
+                && response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+            progressCallback.onCredentialVerified();
+        }
+        return response;
+    }
+
     @Override
     public VerifyCredentialResponse verifyTiedProfileChallenge(String credential, int type,
             long challenge, int userId) throws RemoteException {
@@ -1521,7 +1586,11 @@
                 unlockUser(userId, fakeToken, fakeToken);
 
                 // migrate credential to GateKeeper
-                setLockCredentialInternal(credential, storedHash.type, null, userId);
+                setLockCredentialInternal(credential, storedHash.type, null,
+                        storedHash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN
+                                ? DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
+                                : DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
+                                /* TODO(roosa): keep the same password quality */, userId);
                 if (!hasChallenge) {
                     notifyActivePasswordMetricsAvailable(credential, userId);
                     return VerifyCredentialResponse.OK;
@@ -1557,15 +1626,21 @@
                         (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
                 trustManager.setDeviceLockedForUser(userId, false);
             }
+            int reEnrollQuality = storedHash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN
+                    ? DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
+                    : DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
+                    /* TODO(roosa): keep the same password quality */;
             if (shouldReEnroll) {
-                setLockCredentialInternal(credential, storedHash.type, credential, userId);
+                setLockCredentialInternal(credential, storedHash.type, credential,
+                        reEnrollQuality, userId);
             } else {
                 // Now that we've cleared of all required GK migration, let's do the final
                 // migration to synthetic password.
                 synchronized (mSpManager) {
                     if (shouldMigrateToSyntheticPasswordLocked(userId)) {
                         AuthenticationToken auth = initializeSyntheticPasswordLocked(
-                                storedHash.hash, credential, storedHash.type, userId);
+                                storedHash.hash, credential, storedHash.type, reEnrollQuality,
+                                userId);
                         activateEscrowTokens(auth, userId);
                     }
                 }
@@ -1857,7 +1932,8 @@
      *     FOR THE FIRST TIME on a SP-enabled device. New credential and new SID will be created
      */
     private AuthenticationToken initializeSyntheticPasswordLocked(byte[] credentialHash,
-            String credential, int credentialType, int userId) throws RemoteException {
+            String credential, int credentialType, int requestedQuality,
+            int userId) throws RemoteException {
         Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
         AuthenticationToken auth = mSpManager.newSyntheticPasswordAndSid(getGateKeeperService(),
                 credentialHash, credential, userId);
@@ -1866,7 +1942,7 @@
             return null;
         }
         long handle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(),
-                credential, credentialType, auth, userId);
+                credential, credentialType, auth, requestedQuality, userId);
         if (credential != null) {
             if (credentialHash == null) {
                 // Since when initializing SP, we didn't provide an existing password handle
@@ -1895,6 +1971,10 @@
     }
 
     private boolean isSyntheticPasswordBasedCredentialLocked(int userId) throws RemoteException {
+        if (userId == USER_FRP) {
+            final int type = mStorage.readPersistentDataBlock().type;
+            return type == PersistentData.TYPE_SP || type == PersistentData.TYPE_SP_WEAVER;
+        }
         long handle = getSyntheticPasswordHandleLocked(userId);
         // This is a global setting
         long enabled = getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM);
@@ -1919,6 +1999,11 @@
         if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
             userCredential = null;
         }
+        if (userId == USER_FRP) {
+            return mSpManager.verifyFrpCredential(getGateKeeperService(),
+                    userCredential, credentialType, progressCallback);
+        }
+
         long handle = getSyntheticPasswordHandleLocked(userId);
         AuthenticationResult authResult = mSpManager.unwrapPasswordBasedSyntheticPassword(
                 getGateKeeperService(), handle, userCredential, userId);
@@ -1969,10 +2054,10 @@
      * added back when new password is set in future.
      */
     private long setLockCredentialWithAuthTokenLocked(String credential, int credentialType,
-            AuthenticationToken auth, int userId) throws RemoteException {
+            AuthenticationToken auth, int requestedQuality, int userId) throws RemoteException {
         if (DEBUG) Slog.d(TAG, "setLockCredentialWithAuthTokenLocked: user=" + userId);
         long newHandle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(),
-                credential, credentialType, auth, userId);
+                credential, credentialType, auth, requestedQuality, userId);
         final Map<Integer, String> profilePasswords;
         if (credential != null) {
             // // not needed by synchronizeUnifiedWorkChallengeForProfiles()
@@ -2014,7 +2099,7 @@
     }
 
     private void spBasedSetLockCredentialInternalLocked(String credential, int credentialType,
-            String savedCredential, int userId) throws RemoteException {
+            String savedCredential, int requestedQuality, int userId) throws RemoteException {
         if (DEBUG) Slog.d(TAG, "spBasedSetLockCredentialInternalLocked: user=" + userId);
         if (isManagedProfileWithUnifiedLock(userId)) {
             // get credential from keystore when managed profile has unified lock
@@ -2037,7 +2122,8 @@
         if (auth != null) {
             // We are performing a trusted credential change i.e. a correct existing credential
             // is provided
-            setLockCredentialWithAuthTokenLocked(credential, credentialType, auth, userId);
+            setLockCredentialWithAuthTokenLocked(credential, credentialType, auth, requestedQuality,
+                    userId);
             mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
         } else if (response != null
                 && response.getResponseCode() == VerifyCredentialResponse.RESPONSE_ERROR){
@@ -2046,7 +2132,8 @@
             // Still support this for now but this flow will be removed in the next release.
 
             Slog.w(TAG, "Untrusted credential change invoked");
-            initializeSyntheticPasswordLocked(null, credential, credentialType, userId);
+            initializeSyntheticPasswordLocked(null, credential, credentialType, requestedQuality,
+                    userId);
             synchronizeUnifiedWorkChallengeForProfiles(userId, null);
             mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
         } else /* response == null || responseCode == VerifyCredentialResponse.RESPONSE_RETRY */ {
@@ -2070,7 +2157,8 @@
             if (!isUserSecure(userId)) {
                 if (shouldMigrateToSyntheticPasswordLocked(userId)) {
                     auth = initializeSyntheticPasswordLocked(null, null,
-                            LockPatternUtils.CREDENTIAL_TYPE_NONE, userId);
+                            LockPatternUtils.CREDENTIAL_TYPE_NONE,
+                            DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userId);
                 } else /* isSyntheticPasswordBasedCredentialLocked(userId) */ {
                     long pwdHandle = getSyntheticPasswordHandleLocked(userId);
                     auth = mSpManager.unwrapPasswordBasedSyntheticPassword(getGateKeeperService(),
@@ -2132,7 +2220,7 @@
 
     @Override
     public boolean setLockCredentialWithToken(String credential, int type, long tokenHandle,
-            byte[] token, int userId) throws RemoteException {
+            byte[] token, int requestedQuality, int userId) throws RemoteException {
         ensureCallerSystemUid();
         boolean result;
         synchronized (mSpManager) {
@@ -2140,7 +2228,7 @@
                 throw new SecurityException("Escrow token is disabled on the current user");
             }
             result = setLockCredentialWithTokenInternal(credential, type, tokenHandle, token,
-                    userId);
+                    requestedQuality, userId);
         }
         if (result) {
             synchronized (mSeparateChallengeLock) {
@@ -2152,7 +2240,7 @@
     }
 
     private boolean setLockCredentialWithTokenInternal(String credential, int type,
-            long tokenHandle, byte[] token, int userId) throws RemoteException {
+            long tokenHandle, byte[] token, int requestedQuality, int userId) throws RemoteException {
         synchronized (mSpManager) {
             AuthenticationResult result = mSpManager.unwrapTokenBasedSyntheticPassword(
                     getGateKeeperService(), tokenHandle, token, userId);
@@ -2161,7 +2249,8 @@
                 return false;
             }
             long oldHandle = getSyntheticPasswordHandleLocked(userId);
-            setLockCredentialWithAuthTokenLocked(credential, type, result.authToken, userId);
+            setLockCredentialWithAuthTokenLocked(credential, type, result.authToken,
+                    requestedQuality, userId);
             mSpManager.destroyPasswordBasedSyntheticPassword(oldHandle, userId);
             return true;
         }
@@ -2261,4 +2350,69 @@
             throw new SecurityException("Only system can call this API.");
         }
     }
+
+    private class DeviceProvisionedObserver extends ContentObserver {
+        private final Uri mDeviceProvisionedUri = Settings.Global.getUriFor(
+                Settings.Global.DEVICE_PROVISIONED);
+
+        private boolean mRegistered;
+
+        public DeviceProvisionedObserver() {
+            super(null);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (mDeviceProvisionedUri.equals(uri)) {
+                updateRegistration();
+
+                if (isProvisioned()) {
+                    Slog.i(TAG, "Reporting device setup complete to IGateKeeperService");
+                    reportDeviceSetupComplete();
+                }
+            }
+        }
+
+        public void onSystemReady() {
+            if (frpCredentialEnabled()) {
+                updateRegistration();
+            } else {
+                // If we don't intend to use frpCredentials and we're not provisioned yet, send
+                // deviceSetupComplete immediately, so gatekeeper can discard any lingering
+                // credentials immediately.
+                if (!isProvisioned()) {
+                    Slog.i(TAG, "FRP credential disabled, reporting device setup complete "
+                            + "to Gatekeeper immediately");
+                    reportDeviceSetupComplete();
+                }
+            }
+        }
+
+        private void reportDeviceSetupComplete() {
+            try {
+                getGateKeeperService().reportDeviceSetupComplete();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failure reporting to IGateKeeperService", e);
+            }
+        }
+
+        private void updateRegistration() {
+            boolean register = !isProvisioned();
+            if (register == mRegistered) {
+                return;
+            }
+            if (register) {
+                mContext.getContentResolver().registerContentObserver(mDeviceProvisionedUri,
+                        false, this);
+            } else {
+                mContext.getContentResolver().unregisterContentObserver(this);
+            }
+            mRegistered = register;
+        }
+
+        private boolean isProvisioned() {
+            return Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.DEVICE_PROVISIONED, 0) != 0;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index d621a68..79372e48 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -18,6 +18,8 @@
 
 import static android.content.Context.USER_SERVICE;
 
+import android.annotation.Nullable;
+import android.app.admin.DevicePolicyManager;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.pm.UserInfo;
@@ -25,6 +27,7 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.os.Environment;
+import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.StorageManager;
 import android.util.ArrayMap;
@@ -33,8 +36,15 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.server.LocalServices;
+import com.android.server.PersistentDataBlockManagerInternal;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.RandomAccessFile;
@@ -79,12 +89,18 @@
     private final Cache mCache = new Cache();
     private final Object mFileWriteLock = new Object();
 
+    private PersistentDataBlockManagerInternal mPersistentDataBlockManagerInternal;
+
     @VisibleForTesting
     public static class CredentialHash {
         static final int VERSION_LEGACY = 0;
         static final int VERSION_GATEKEEPER = 1;
 
         private CredentialHash(byte[] hash, int type, int version) {
+            this(hash, type, version, false /* isBaseZeroPattern */);
+        }
+
+        private CredentialHash(byte[] hash, int type, int version, boolean isBaseZeroPattern) {
             if (type != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
                 if (hash == null) {
                     throw new RuntimeException("Empty hash for CredentialHash");
@@ -97,14 +113,12 @@
             this.hash = hash;
             this.type = type;
             this.version = version;
-            this.isBaseZeroPattern = false;
+            this.isBaseZeroPattern = isBaseZeroPattern;
         }
 
-        private CredentialHash(byte[] hash, boolean isBaseZeroPattern) {
-            this.hash = hash;
-            this.type = LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
-            this.version = VERSION_GATEKEEPER;
-            this.isBaseZeroPattern = isBaseZeroPattern;
+        private static CredentialHash createBaseZeroPattern(byte[] hash) {
+            return new CredentialHash(hash, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
+                    VERSION_GATEKEEPER, true /* isBaseZeroPattern */);
         }
 
         static CredentialHash create(byte[] hash, int type) {
@@ -123,6 +137,44 @@
         int type;
         int version;
         boolean isBaseZeroPattern;
+
+        public byte[] toBytes() {
+            Preconditions.checkState(!isBaseZeroPattern, "base zero patterns are not serializable");
+
+            try {
+                ByteArrayOutputStream os = new ByteArrayOutputStream();
+                DataOutputStream dos = new DataOutputStream(os);
+                dos.write(version);
+                dos.write(type);
+                if (hash != null && hash.length > 0) {
+                    dos.writeInt(hash.length);
+                    dos.write(hash);
+                } else {
+                    dos.writeInt(0);
+                }
+                dos.close();
+                return os.toByteArray();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        public static CredentialHash fromBytes(byte[] bytes) {
+            try {
+                DataInputStream is = new DataInputStream(new ByteArrayInputStream(bytes));
+                int version = is.read();
+                int type = is.read();
+                int hashSize = is.readInt();
+                byte[] hash = null;
+                if (hashSize > 0) {
+                    hash = new byte[hashSize];
+                    is.readFully(hash);
+                }
+                return new CredentialHash(hash, type, version);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
     }
 
     public LockSettingsStorage(Context context) {
@@ -234,7 +286,7 @@
 
         stored = readFile(getBaseZeroLockPatternFilename(userId));
         if (!ArrayUtils.isEmpty(stored)) {
-            return new CredentialHash(stored, true);
+            return CredentialHash.createBaseZeroPattern(stored);
         }
 
         stored = readFile(getLegacyLockPatternFilename(userId));
@@ -551,6 +603,108 @@
         mCache.clear();
     }
 
+    @Nullable
+    public PersistentDataBlockManagerInternal getPersistentDataBlock() {
+        if (mPersistentDataBlockManagerInternal == null) {
+            mPersistentDataBlockManagerInternal =
+                    LocalServices.getService(PersistentDataBlockManagerInternal.class);
+        }
+        return mPersistentDataBlockManagerInternal;
+    }
+
+    public void writePersistentDataBlock(int persistentType, int userId, int qualityForUi,
+            byte[] payload) {
+        PersistentDataBlockManagerInternal persistentDataBlock = getPersistentDataBlock();
+        if (persistentDataBlock == null) {
+            return;
+        }
+        persistentDataBlock.setFrpCredentialHandle(PersistentData.toBytes(
+                persistentType, userId, qualityForUi, payload));
+    }
+
+    public PersistentData readPersistentDataBlock() {
+        PersistentDataBlockManagerInternal persistentDataBlock = getPersistentDataBlock();
+        if (persistentDataBlock == null) {
+            return PersistentData.NONE;
+        }
+        return PersistentData.fromBytes(persistentDataBlock.getFrpCredentialHandle());
+    }
+
+    public static class PersistentData {
+        static final byte VERSION_1 = 1;
+        static final int VERSION_1_HEADER_SIZE = 1 + 1 + 4 + 4;
+
+        public static final int TYPE_NONE = 0;
+        public static final int TYPE_GATEKEEPER = 1;
+        public static final int TYPE_SP = 2;
+        public static final int TYPE_SP_WEAVER = 3;
+
+        public static final PersistentData NONE = new PersistentData(TYPE_NONE,
+                UserHandle.USER_NULL, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, null);
+
+        final int type;
+        final int userId;
+        final int qualityForUi;
+        final byte[] payload;
+
+        private PersistentData(int type, int userId, int qualityForUi, byte[] payload) {
+            this.type = type;
+            this.userId = userId;
+            this.qualityForUi = qualityForUi;
+            this.payload = payload;
+        }
+
+        public static PersistentData fromBytes(byte[] frpData) {
+            if (frpData == null || frpData.length == 0) {
+                return NONE;
+            }
+
+            DataInputStream is = new DataInputStream(new ByteArrayInputStream(frpData));
+            try {
+                byte version = is.readByte();
+                if (version == PersistentData.VERSION_1) {
+                    int type = is.readByte() & 0xFF;
+                    int userId = is.readInt();
+                    int qualityForUi = is.readInt();
+                    byte[] payload = new byte[frpData.length - VERSION_1_HEADER_SIZE];
+                    System.arraycopy(frpData, VERSION_1_HEADER_SIZE, payload, 0, payload.length);
+                    return new PersistentData(type, userId, qualityForUi, payload);
+                } else {
+                    Slog.wtf(TAG, "Unknown PersistentData version code: " + version);
+                    return null;
+                }
+            } catch (IOException e) {
+                Slog.wtf(TAG, "Could not parse PersistentData", e);
+                return null;
+            }
+        }
+
+        public static byte[] toBytes(int persistentType, int userId, int qualityForUi,
+                byte[] payload) {
+            if (persistentType == PersistentData.TYPE_NONE) {
+                Preconditions.checkArgument(payload == null,
+                        "TYPE_NONE must have empty payload");
+                return null;
+            }
+            Preconditions.checkArgument(payload != null && payload.length > 0,
+                    "empty payload must only be used with TYPE_NONE");
+
+            ByteArrayOutputStream os = new ByteArrayOutputStream(
+                    VERSION_1_HEADER_SIZE + payload.length);
+            DataOutputStream dos = new DataOutputStream(os);
+            try {
+                dos.writeByte(PersistentData.VERSION_1);
+                dos.writeByte(persistentType);
+                dos.writeInt(userId);
+                dos.writeInt(qualityForUi);
+                dos.write(payload);
+            } catch (IOException e) {
+                throw new RuntimeException("ByteArrayOutputStream cannot throw IOException");
+            }
+            return os.toByteArray();
+        }
+    }
+
     public interface Callback {
         void initialize(SQLiteDatabase db);
     }
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 2f8a1b4..f45c208 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -18,13 +18,15 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.admin.DevicePolicyManager;
 import android.hardware.weaver.V1_0.IWeaver;
 import android.hardware.weaver.V1_0.WeaverConfig;
 import android.hardware.weaver.V1_0.WeaverReadResponse;
 import android.hardware.weaver.V1_0.WeaverReadStatus;
 import android.hardware.weaver.V1_0.WeaverStatus;
-import android.os.RemoteException;
 import android.security.GateKeeper;
+import android.os.RemoteException;
+import android.os.UserManager;
 import android.service.gatekeeper.GateKeeperResponse;
 import android.service.gatekeeper.IGateKeeperService;
 import android.util.ArrayMap;
@@ -33,8 +35,10 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.ICheckCredentialProgressCallback;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.server.locksettings.LockSettingsStorage.PersistentData;
 
 import libcore.util.HexEncoding;
 
@@ -253,8 +257,11 @@
     private IWeaver mWeaver;
     private WeaverConfig mWeaverConfig;
 
-    public SyntheticPasswordManager(LockSettingsStorage storage) {
+    private final UserManager mUserManager;
+
+    public SyntheticPasswordManager(LockSettingsStorage storage, UserManager userManager) {
         mStorage = storage;
+        mUserManager = userManager;
     }
 
     @VisibleForTesting
@@ -557,7 +564,8 @@
      * @see #clearSidForUser
      */
     public long createPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper,
-            String credential, int credentialType, AuthenticationToken authToken, int userId)
+            String credential, int credentialType, AuthenticationToken authToken,
+            int requestedQuality, int userId)
                     throws RemoteException {
         if (credential == null || credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
             credentialType = LockPatternUtils.CREDENTIAL_TYPE_NONE;
@@ -579,6 +587,7 @@
                 return DEFAULT_HANDLE;
             }
             saveWeaverSlot(weaverSlot, handle, userId);
+            synchronizeWeaverFrpPassword(pwd, requestedQuality, userId, weaverSlot);
 
             pwd.passwordHandle = null;
             sid = GateKeeper.INVALID_SECURE_USER_ID;
@@ -598,6 +607,7 @@
             sid = sidFromPasswordHandle(pwd.passwordHandle);
             applicationId = transformUnderSecdiscardable(pwdToken,
                     createSecdiscardable(handle, userId));
+            synchronizeFrpPassword(pwd, requestedQuality, userId);
         }
         saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId);
 
@@ -606,6 +616,57 @@
         return handle;
     }
 
+    public VerifyCredentialResponse verifyFrpCredential(IGateKeeperService gatekeeper,
+            String userCredential, int credentialType,
+            ICheckCredentialProgressCallback progressCallback) throws RemoteException {
+        PersistentData persistentData = mStorage.readPersistentDataBlock();
+        if (persistentData.type == PersistentData.TYPE_SP) {
+            PasswordData pwd = PasswordData.fromBytes(persistentData.payload);
+            byte[] pwdToken = computePasswordToken(userCredential, pwd);
+
+            GateKeeperResponse response = gatekeeper.verify(fakeUid(persistentData.userId),
+                    pwd.passwordHandle, passwordTokenToGkInput(pwdToken));
+            return VerifyCredentialResponse.fromGateKeeperResponse(response);
+        } else if (persistentData.type == PersistentData.TYPE_SP_WEAVER) {
+            PasswordData pwd = PasswordData.fromBytes(persistentData.payload);
+            byte[] pwdToken = computePasswordToken(userCredential, pwd);
+            int weaverSlot = persistentData.userId;
+
+            return weaverVerify(weaverSlot, passwordTokenToWeaverKey(pwdToken)).stripPayload();
+        } else {
+            Log.e(TAG, "persistentData.type must be TYPE_SP or TYPE_SP_WEAVER, but is "
+                    + persistentData.type);
+            return VerifyCredentialResponse.ERROR;
+        }
+    }
+
+
+    private void synchronizeFrpPassword(PasswordData pwd,
+            int requestedQuality, int userId) {
+        if (mStorage.getPersistentDataBlock() != null
+                && LockPatternUtils.userOwnsFrpCredential(mUserManager.getUserInfo(userId))) {
+            if (pwd.passwordType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+                mStorage.writePersistentDataBlock(PersistentData.TYPE_SP, userId, requestedQuality,
+                        pwd.toBytes());
+            } else {
+                mStorage.writePersistentDataBlock(PersistentData.TYPE_NONE, userId, 0, null);
+            }
+        }
+    }
+
+    private void synchronizeWeaverFrpPassword(PasswordData pwd, int requestedQuality, int userId,
+            int weaverSlot) {
+        if (mStorage.getPersistentDataBlock() != null
+                && LockPatternUtils.userOwnsFrpCredential(mUserManager.getUserInfo(userId))) {
+            if (pwd.passwordType != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+                mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, weaverSlot,
+                        requestedQuality, pwd.toBytes());
+            } else {
+                mStorage.writePersistentDataBlock(PersistentData.TYPE_NONE, 0, 0, null);
+            }
+        }
+    }
+
     private ArrayMap<Integer, ArrayMap<Long, TokenData>> tokenMap = new ArrayMap<>();
 
     public long createTokenBasedSyntheticPassword(byte[] token, int userId) {
@@ -730,6 +791,12 @@
                     if (reenrollResponse.getResponseCode() == GateKeeperResponse.RESPONSE_OK) {
                         pwd.passwordHandle = reenrollResponse.getPayload();
                         saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId);
+                        synchronizeFrpPassword(pwd,
+                                pwd.passwordType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN
+                                ? DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
+                                : DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
+                                /* TODO(roosa): keep the same password quality */,
+                                userId);
                     } else {
                         Log.w(TAG, "Fail to re-enroll user password for user " + userId);
                         // continue the flow anyway
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index 7de46d9..84cca0e 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -96,7 +96,7 @@
             storageDir.mkdirs();
         }
 
-        mSpManager = new MockSyntheticPasswordManager(mStorage, mGateKeeperService);
+        mSpManager = new MockSyntheticPasswordManager(mStorage, mGateKeeperService, mUserManager);
         mService = new LockSettingsServiceTestable(mContext, mLockPatternUtils,
                 mStorage, mGateKeeperService, mKeyStore, mStorageManager, mActivityManager,
                 mSpManager);
@@ -164,4 +164,3 @@
         assertFalse(Arrays.equals(expected, actual));
     }
 }
-
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index 2f0ac38..cb32492 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -16,6 +16,11 @@
 
 package com.android.server.locksettings;
 
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
@@ -44,21 +49,23 @@
     }
 
     public void testCreatePasswordPrimaryUser() throws RemoteException {
-        testCreateCredential(PRIMARY_USER_ID, "password", CREDENTIAL_TYPE_PASSWORD);
+        testCreateCredential(PRIMARY_USER_ID, "password", CREDENTIAL_TYPE_PASSWORD,
+                PASSWORD_QUALITY_ALPHABETIC);
     }
 
     public void testCreatePatternPrimaryUser() throws RemoteException {
-        testCreateCredential(PRIMARY_USER_ID, "123456789", CREDENTIAL_TYPE_PATTERN);
+        testCreateCredential(PRIMARY_USER_ID, "123456789", CREDENTIAL_TYPE_PATTERN,
+                PASSWORD_QUALITY_SOMETHING);
     }
 
     public void testChangePasswordPrimaryUser() throws RemoteException {
         testChangeCredentials(PRIMARY_USER_ID, "78963214", CREDENTIAL_TYPE_PATTERN,
-                "asdfghjk", CREDENTIAL_TYPE_PASSWORD);
+                "asdfghjk", CREDENTIAL_TYPE_PASSWORD, PASSWORD_QUALITY_ALPHABETIC);
     }
 
     public void testChangePatternPrimaryUser() throws RemoteException {
         testChangeCredentials(PRIMARY_USER_ID, "!£$%^&*(())", CREDENTIAL_TYPE_PASSWORD,
-                "1596321", CREDENTIAL_TYPE_PATTERN);
+                "1596321", CREDENTIAL_TYPE_PATTERN, PASSWORD_QUALITY_SOMETHING);
     }
 
     public void testChangePasswordFailPrimaryUser() throws RemoteException {
@@ -68,13 +75,14 @@
 
         try {
             mService.setLockCredential("newpwd", CREDENTIAL_TYPE_PASSWORD, "badpwd",
-                    PRIMARY_USER_ID);
+                    PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
             fail("Did not fail when enrolling using incorrect credential");
         } catch (RemoteException expected) {
             assertTrue(expected.getMessage().equals(FAILED_MESSAGE));
         }
         try {
-            mService.setLockCredential("newpwd", CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+            mService.setLockCredential("newpwd", CREDENTIAL_TYPE_PASSWORD, null,
+                    PASSWORD_QUALITY_UNSPECIFIED, PRIMARY_USER_ID);
             fail("Did not fail when enrolling using incorrect credential");
         } catch (RemoteException expected) {
             assertTrue(expected.getMessage().equals(FAILED_MESSAGE));
@@ -85,7 +93,8 @@
     public void testClearPasswordPrimaryUser() throws RemoteException {
         final String PASSWORD = "password";
         initializeStorageWithCredential(PRIMARY_USER_ID, PASSWORD, CREDENTIAL_TYPE_PASSWORD, 1234);
-        mService.setLockCredential(null, CREDENTIAL_TYPE_NONE, PASSWORD, PRIMARY_USER_ID);
+        mService.setLockCredential(null, CREDENTIAL_TYPE_NONE, PASSWORD,
+                PASSWORD_QUALITY_UNSPECIFIED, PRIMARY_USER_ID);
         assertFalse(mService.havePassword(PRIMARY_USER_ID));
         assertFalse(mService.havePattern(PRIMARY_USER_ID));
         assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
@@ -94,7 +103,7 @@
     public void testManagedProfileUnifiedChallenge() throws RemoteException {
         final String UnifiedPassword = "testManagedProfileUnifiedChallenge-pwd";
         mService.setLockCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
-                PRIMARY_USER_ID);
+                PASSWORD_QUALITY_COMPLEX, PRIMARY_USER_ID);
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
         final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
@@ -129,14 +138,14 @@
         mStorageManager.setIgnoreBadUnlock(true);
         // Change primary password and verify that profile SID remains
         mService.setLockCredential("pwd", LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
-                UnifiedPassword, PRIMARY_USER_ID);
+                UnifiedPassword, PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
         mStorageManager.setIgnoreBadUnlock(false);
         assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
         assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID));
 
         // Clear unified challenge
         mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, UnifiedPassword,
-                PRIMARY_USER_ID);
+                PASSWORD_QUALITY_UNSPECIFIED, PRIMARY_USER_ID);
         assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
         assertEquals(0, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
         assertEquals(0, mGateKeeperService.getSecureUserId(TURNED_OFF_PROFILE_USER_ID));
@@ -146,14 +155,14 @@
         final String primaryPassword = "testManagedProfileSeparateChallenge-primary";
         final String profilePassword = "testManagedProfileSeparateChallenge-profile";
         mService.setLockCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
-                PRIMARY_USER_ID);
+                PASSWORD_QUALITY_COMPLEX, PRIMARY_USER_ID);
         /* Currently in LockSettingsService.setLockCredential, unlockUser() is called with the new
          * credential as part of verifyCredential() before the new credential is committed in
          * StorageManager. So we relax the check in our mock StorageManager to allow that.
          */
         mStorageManager.setIgnoreBadUnlock(true);
         mService.setLockCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
-                MANAGED_PROFILE_USER_ID);
+                PASSWORD_QUALITY_COMPLEX, MANAGED_PROFILE_USER_ID);
         mStorageManager.setIgnoreBadUnlock(false);
 
         final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
@@ -180,7 +189,7 @@
         // Change primary credential and make sure we don't affect profile
         mStorageManager.setIgnoreBadUnlock(true);
         mService.setLockCredential("pwd", LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
-                primaryPassword, PRIMARY_USER_ID);
+                primaryPassword, PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
         mStorageManager.setIgnoreBadUnlock(false);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                 profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0,
@@ -188,17 +197,17 @@
         assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID));
     }
 
-    private void testCreateCredential(int userId, String credential, int type)
+    private void testCreateCredential(int userId, String credential, int type, int quality)
             throws RemoteException {
-        mService.setLockCredential(credential, type, null, userId);
+        mService.setLockCredential(credential, type, null, quality, userId);
         assertVerifyCredentials(userId, credential, type, -1);
     }
 
     private void testChangeCredentials(int userId, String newCredential, int newType,
-            String oldCredential, int oldType) throws RemoteException {
+            String oldCredential, int oldType, int quality) throws RemoteException {
         final long sid = 1234;
         initializeStorageWithCredential(userId, oldCredential, oldType, sid);
-        mService.setLockCredential(newCredential, newType, oldCredential, userId);
+        mService.setLockCredential(newCredential, newType, oldCredential, quality, userId);
         assertVerifyCredentials(userId, newCredential, newType, sid);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
index 449a54c..a0578c9 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
@@ -22,8 +22,6 @@
 
 import android.app.NotificationManager;
 import android.app.admin.DevicePolicyManager;
-import android.content.Context;
-import android.content.ContextWrapper;
 import android.content.pm.UserInfo;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.FileUtils;
@@ -33,6 +31,8 @@
 
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.locksettings.LockSettingsStorage.CredentialHash;
+import com.android.server.locksettings.LockSettingsStorage.PersistentData;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -43,11 +43,14 @@
  * runtest frameworks-services -c com.android.server.locksettings.LockSettingsStorageTests
  */
 public class LockSettingsStorageTests extends AndroidTestCase {
+    private static final int SOME_USER_ID = 1034;
     private final byte[] PASSWORD_0 = "thepassword0".getBytes();
     private final byte[] PASSWORD_1 = "password1".getBytes();
     private final byte[] PATTERN_0 = "123654".getBytes();
     private final byte[] PATTERN_1 = "147852369".getBytes();
 
+    public static final byte[] PAYLOAD = new byte[] {1, 2, -1, -2, 33};
+
     LockSettingsStorage mStorage;
     File mStorageDir;
 
@@ -342,6 +345,83 @@
         assertEquals(null, mStorage.readSyntheticPasswordState(10, 1234L, "state"));
     }
 
+    public void testPersistentData_serializeUnserialize() {
+        byte[] serialized = PersistentData.toBytes(PersistentData.TYPE_GATEKEEPER, SOME_USER_ID,
+                DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, PAYLOAD);
+        PersistentData deserialized = PersistentData.fromBytes(serialized);
+
+        assertEquals(PersistentData.TYPE_GATEKEEPER, deserialized.type);
+        assertEquals(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, deserialized.qualityForUi);
+        assertArrayEquals(PAYLOAD, deserialized.payload);
+    }
+
+    public void testPersistentData_unserializeNull() {
+        PersistentData deserialized = PersistentData.fromBytes(null);
+        assertSame(PersistentData.NONE, deserialized);
+    }
+
+    public void testPersistentData_unserializeEmptyArray() {
+        PersistentData deserialized = PersistentData.fromBytes(new byte[0]);
+        assertSame(PersistentData.NONE, deserialized);
+    }
+
+    public void testPersistentData_unserialize_version1() {
+        // This test ensures that we can read serialized VERSION_1 PersistentData even if we change
+        // the wire format in the future.
+        byte[] serializedVersion1 = new byte[] {
+                1, /* PersistentData.VERSION_1 */
+                2, /* PersistentData.TYPE_SP */
+                0x00, 0x00, 0x04, 0x0A,  /* SOME_USER_ID */
+                0x00, 0x03, 0x00, 0x00,  /* PASSWORD_NUMERIC_COMPLEX */
+                1, 2, -1, -2, 33, /* PAYLOAD */
+        };
+        PersistentData deserialized = PersistentData.fromBytes(serializedVersion1);
+        assertEquals(PersistentData.TYPE_SP, deserialized.type);
+        assertEquals(SOME_USER_ID, deserialized.userId);
+        assertEquals(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX,
+                deserialized.qualityForUi);
+        assertArrayEquals(PAYLOAD, deserialized.payload);
+
+        // Make sure the constants we use on the wire do not change.
+        assertEquals(0, PersistentData.TYPE_NONE);
+        assertEquals(1, PersistentData.TYPE_GATEKEEPER);
+        assertEquals(2, PersistentData.TYPE_SP);
+        assertEquals(3, PersistentData.TYPE_SP_WEAVER);
+    }
+
+    public void testCredentialHash_serializeUnserialize() {
+        byte[] serialized = CredentialHash.create(
+                PAYLOAD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD).toBytes();
+        CredentialHash deserialized = CredentialHash.fromBytes(serialized);
+
+        assertEquals(CredentialHash.VERSION_GATEKEEPER, deserialized.version);
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, deserialized.type);
+        assertArrayEquals(PAYLOAD, deserialized.hash);
+        assertFalse(deserialized.isBaseZeroPattern);
+    }
+
+    public void testCredentialHash_unserialize_versionGatekeeper() {
+        // This test ensures that we can read serialized VERSION_GATEKEEPER CredentialHashes
+        // even if we change the wire format in the future.
+        byte[] serialized = new byte[] {
+                1, /* VERSION_GATEKEEPER */
+                2, /* CREDENTIAL_TYPE_PASSWORD */
+                0, 0, 0, 5, /* hash length */
+                1, 2, -1, -2, 33, /* hash */
+        };
+        CredentialHash deserialized = CredentialHash.fromBytes(serialized);
+
+        assertEquals(CredentialHash.VERSION_GATEKEEPER, deserialized.version);
+        assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, deserialized.type);
+        assertArrayEquals(PAYLOAD, deserialized.hash);
+        assertFalse(deserialized.isBaseZeroPattern);
+
+        // Make sure the constants we use on the wire do not change.
+        assertEquals(-1, LockPatternUtils.CREDENTIAL_TYPE_NONE);
+        assertEquals(1, LockPatternUtils.CREDENTIAL_TYPE_PATTERN);
+        assertEquals(2, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD);
+    }
+
     private static void assertArrayEquals(byte[] expected, byte[] actual) {
         if (!Arrays.equals(expected, actual)) {
             fail("expected:<" + Arrays.toString(expected) +
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/MockGateKeeperService.java b/services/tests/servicestests/src/com/android/server/locksettings/MockGateKeeperService.java
index eefd361..b89c1d1 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/MockGateKeeperService.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/MockGateKeeperService.java
@@ -173,6 +173,10 @@
     }
 
     @Override
+    public void reportDeviceSetupComplete() throws RemoteException {
+    }
+
+    @Override
     public long getSecureUserId(int userId) throws RemoteException {
         if (sidMap.containsKey(userId)) {
             return sidMap.get(userId);
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java b/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java
index ddef5dc..d7468c2 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/MockSyntheticPasswordManager.java
@@ -17,6 +17,7 @@
 
 import android.hardware.weaver.V1_0.IWeaver;
 import android.os.RemoteException;
+import android.os.UserManager;
 import android.util.ArrayMap;
 
 import junit.framework.AssertionFailedError;
@@ -35,8 +36,8 @@
     private IWeaver mWeaverService;
 
     public MockSyntheticPasswordManager(LockSettingsStorage storage,
-            MockGateKeeperService gatekeeper) {
-        super(storage);
+            MockGateKeeperService gatekeeper, UserManager userManager) {
+        super(storage, userManager);
         mGateKeeper = gatekeeper;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 0d35385..ba4ff33 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -16,9 +16,15 @@
 
 package com.android.server.locksettings;
 
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
 import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_ENABLED_KEY;
 import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_HANDLE_KEY;
 
+import android.app.admin.DevicePolicyManager;
 import android.os.RemoteException;
 import android.os.UserHandle;
 
@@ -26,6 +32,7 @@
 import com.android.internal.widget.VerifyCredentialResponse;
 import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationResult;
 import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationToken;
+import com.android.server.locksettings.SyntheticPasswordManager.PasswordData;
 
 
 /**
@@ -33,6 +40,9 @@
  */
 public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
 
+    public static final byte[] PAYLOAD = new byte[] {1, 2, -1, -2, 55};
+    public static final byte[] PAYLOAD2 = new byte[] {2, 3, -2, -3, 44, 1};
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -47,11 +57,13 @@
         final int USER_ID = 10;
         final String PASSWORD = "user-password";
         final String BADPASSWORD = "bad-password";
-        MockSyntheticPasswordManager manager = new MockSyntheticPasswordManager(mStorage, mGateKeeperService);
+        MockSyntheticPasswordManager manager = new MockSyntheticPasswordManager(mStorage,
+                mGateKeeperService, mUserManager);
         AuthenticationToken authToken = manager.newSyntheticPasswordAndSid(mGateKeeperService, null,
                 null, USER_ID);
         long handle = manager.createPasswordBasedSyntheticPassword(mGateKeeperService, PASSWORD,
-                LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, authToken, USER_ID);
+                LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, authToken, PASSWORD_QUALITY_ALPHABETIC,
+                USER_ID);
 
         AuthenticationResult result = manager.unwrapPasswordBasedSyntheticPassword(mGateKeeperService, handle, PASSWORD, USER_ID);
         assertEquals(result.authToken.deriveKeyStorePassword(), authToken.deriveKeyStorePassword());
@@ -76,7 +88,8 @@
         final String PASSWORD = "testPasswordMigration-password";
 
         disableSyntheticPassword(PRIMARY_USER_ID);
-        mService.setLockCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        mService.setLockCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
+                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
         long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
         enableSyntheticPassword(PRIMARY_USER_ID);
@@ -94,7 +107,11 @@
 
     private void initializeCredentialUnderSP(String password, int userId) throws RemoteException {
         enableSyntheticPassword(userId);
-        mService.setLockCredential(password, password != null ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD : LockPatternUtils.CREDENTIAL_TYPE_NONE, null, userId);
+        int quality = password != null ? PASSWORD_QUALITY_ALPHABETIC
+                : PASSWORD_QUALITY_UNSPECIFIED;
+        int type = password != null ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
+                : LockPatternUtils.CREDENTIAL_TYPE_NONE;
+        mService.setLockCredential(password, type, null, quality, userId);
     }
 
     public void testSyntheticPasswordChangeCredential() throws RemoteException {
@@ -103,7 +120,8 @@
 
         initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
         long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
-        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, PASSWORD, PRIMARY_USER_ID);
+        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, PASSWORD,
+                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
         mGateKeeperService.clearSecureUserId(PRIMARY_USER_ID);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK,
                 mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
@@ -129,11 +147,13 @@
         initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
         long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         // clear password
-        mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, PASSWORD, PRIMARY_USER_ID);
+        mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, PASSWORD,
+                PASSWORD_QUALITY_UNSPECIFIED, PRIMARY_USER_ID);
         assertEquals(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
 
         // set a new password
-        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
+                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK,
                 mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
         assertNotSame(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
@@ -146,11 +166,13 @@
         initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
         long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         // clear password
-        mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, null,
+                PASSWORD_QUALITY_UNSPECIFIED, PRIMARY_USER_ID);
         assertEquals(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
 
         // set a new password
-        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
+                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
         assertEquals(VerifyCredentialResponse.RESPONSE_OK,
                 mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
         assertNotSame(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
@@ -163,7 +185,8 @@
         initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
         long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         // Untrusted change password
-        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
+                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
         assertNotSame(0 ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
         assertNotSame(sid ,mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
 
@@ -177,7 +200,8 @@
         final String UnifiedPassword = "testManagedProfileUnifiedChallengeMigration-pwd";
         disableSyntheticPassword(PRIMARY_USER_ID);
         disableSyntheticPassword(MANAGED_PROFILE_USER_ID);
-        mService.setLockCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
+        mService.setLockCredential(UnifiedPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
+                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
         mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null);
         final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
@@ -207,8 +231,10 @@
     public void testManagedProfileSeparateChallengeMigration() throws RemoteException {
         final String primaryPassword = "testManagedProfileSeparateChallengeMigration-primary";
         final String profilePassword = "testManagedProfileSeparateChallengeMigration-profile";
-        mService.setLockCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, PRIMARY_USER_ID);
-        mService.setLockCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null, MANAGED_PROFILE_USER_ID);
+        mService.setLockCredential(primaryPassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
+                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
+        mService.setLockCredential(profilePassword, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
+                PASSWORD_QUALITY_ALPHABETIC, MANAGED_PROFILE_USER_ID);
         final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
         final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID);
         final byte[] primaryStorageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID);
@@ -251,7 +277,8 @@
         mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode();
         assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
 
-        mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+        mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
+                handle, TOKEN.getBytes(), PASSWORD_QUALITY_SOMETHING, PRIMARY_USER_ID);
 
         assertEquals(VerifyCredentialResponse.RESPONSE_OK,
                 mService.verifyCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 0, PRIMARY_USER_ID).getResponseCode());
@@ -271,8 +298,10 @@
         mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode();
         assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
 
-        mService.setLockCredentialWithToken(null, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
-        mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+        mService.setLockCredentialWithToken(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, handle,
+                TOKEN.getBytes(), PASSWORD_QUALITY_UNSPECIFIED, PRIMARY_USER_ID);
+        mService.setLockCredentialWithToken(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
+                handle, TOKEN.getBytes(), PASSWORD_QUALITY_SOMETHING, PRIMARY_USER_ID);
 
         assertEquals(VerifyCredentialResponse.RESPONSE_OK,
                 mService.verifyCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, 0, PRIMARY_USER_ID).getResponseCode());
@@ -293,9 +322,11 @@
         mService.verifyCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode();
         assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
 
-        mService.setLockCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, PASSWORD, PRIMARY_USER_ID);
+        mService.setLockCredential(PATTERN, LockPatternUtils.CREDENTIAL_TYPE_PATTERN, PASSWORD,
+                PASSWORD_QUALITY_SOMETHING, PRIMARY_USER_ID);
 
-        mService.setLockCredentialWithToken(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, handle, TOKEN.getBytes(), PRIMARY_USER_ID);
+        mService.setLockCredentialWithToken(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
+                handle, TOKEN.getBytes(), PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
 
         assertEquals(VerifyCredentialResponse.RESPONSE_OK,
                 mService.verifyCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID).getResponseCode());
@@ -326,7 +357,7 @@
         // Set up pre-SP user password
         disableSyntheticPassword(PRIMARY_USER_ID);
         mService.setLockCredential(PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
-                PRIMARY_USER_ID);
+                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
         enableSyntheticPassword(PRIMARY_USER_ID);
 
         long handle = mService.addEscrowToken(TOKEN.getBytes(), PRIMARY_USER_ID);
@@ -340,10 +371,51 @@
         assertTrue(mService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
     }
 
+    public void testPasswordData_serializeDeserialize() {
+        PasswordData data = new PasswordData();
+        data.scryptN = 11;
+        data.scryptR = 22;
+        data.scryptP = 33;
+        data.passwordType = CREDENTIAL_TYPE_PASSWORD;
+        data.salt = PAYLOAD;
+        data.passwordHandle = PAYLOAD2;
+
+        PasswordData deserialized = PasswordData.fromBytes(data.toBytes());
+
+        assertEquals(11, deserialized.scryptN);
+        assertEquals(22, deserialized.scryptR);
+        assertEquals(33, deserialized.scryptP);
+        assertEquals(CREDENTIAL_TYPE_PASSWORD, deserialized.passwordType);
+        assertArrayEquals(PAYLOAD, deserialized.salt);
+        assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+    }
+
+    public void testPasswordData_deserialize() {
+        // Test that we can deserialize existing PasswordData and don't inadvertently change the
+        // wire format.
+        byte[] serialized = new byte[] {
+                0, 0, 0, 2, /* CREDENTIAL_TYPE_PASSWORD */
+                11, /* scryptN */
+                22, /* scryptR */
+                33, /* scryptP */
+                0, 0, 0, 5, /* salt.length */
+                1, 2, -1, -2, 55, /* salt */
+                0, 0, 0, 6, /* passwordHandle.length */
+                2, 3, -2, -3, 44, 1, /* passwordHandle */
+        };
+        PasswordData deserialized = PasswordData.fromBytes(serialized);
+
+        assertEquals(11, deserialized.scryptN);
+        assertEquals(22, deserialized.scryptR);
+        assertEquals(33, deserialized.scryptP);
+        assertEquals(CREDENTIAL_TYPE_PASSWORD, deserialized.passwordType);
+        assertArrayEquals(PAYLOAD, deserialized.salt);
+        assertArrayEquals(PAYLOAD2, deserialized.passwordHandle);
+    }
+
     // b/34600579
     //TODO: add non-migration work profile case, and unify/un-unify transition.
     //TODO: test token after user resets password
     //TODO: test token based reset after unified work challenge
     //TODO: test clear password after unified work challenge
 }
-