Add new resetPasswordWithToken test

1. Test resetPasswordWithToken() before user unlock on FBE devices.
2. Test resetPasswordWithToken() updates stored password metrics info
   in DevicePolicyManager.
3. Test isActivePasswordSufficient() throws exception before user unlock.

Test:  cts-tradefed run cts-dev -m CtsDevicePolicyManagerTestCases -t com.android.cts.devicepolicy.ManagedProfileTest#testResetPasswordWithTokenBeforeUnlock
Test: cts-tradefed run cts-dev -m CtsDevicePolicyManagerTestCases -t com.android.cts.devicepolicy.MixedManagedProfileOwnerTest#testResetPasswordWithToken
Bug: 64923343
Bug: 64928518
Change-Id: I05c96f4b32048109e2d43f6a5b922556f6cd6f1b
diff --git a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ResetPasswordWithTokenTest.java b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ResetPasswordWithTokenTest.java
index 9e4744c..f8c08ae 100644
--- a/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ResetPasswordWithTokenTest.java
+++ b/hostsidetests/devicepolicy/app/DeviceAndProfileOwner/src/com/android/cts/deviceandprofileowner/ResetPasswordWithTokenTest.java
@@ -15,63 +15,169 @@
  */
 package com.android.cts.deviceandprofileowner;
 
+import android.app.KeyguardManager;
+import android.app.admin.DevicePolicyManager;
+import android.support.test.InstrumentationRegistry;
 
 public class ResetPasswordWithTokenTest extends BaseDeviceAdminTest {
 
-    private static final String PASSWORD = "1234";
+    private static final String SHORT_PASSWORD = "1234";
+    private static final String COMPLEX_PASSWORD = "abc123.";
 
     private static final byte[] TOKEN0 = "abcdefghijklmnopqrstuvwxyz0123456789".getBytes();
     private static final byte[] TOKEN1 = "abcdefghijklmnopqrstuvwxyz012345678*".getBytes();
 
-    public void testResetPasswordWithToken() {
-        testResetPasswordWithToken(false);
+    private static final String ARG_ALLOW_FAILURE = "allowFailure";
+
+    private boolean mShouldRun;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        Boolean allowFailure = Boolean.parseBoolean(InstrumentationRegistry.getArguments()
+                .getString(ARG_ALLOW_FAILURE));
+        mShouldRun = setUpResetPasswordToken(allowFailure);
     }
 
-    public void testResetPasswordWithTokenMayFail() {
-        // If this test is executed on a device with password token disabled, allow the test to
-        // pass.
-        testResetPasswordWithToken(true);
-    }
-
-    private void testResetPasswordWithToken(boolean acceptFailure) {
-        try {
-            // set up a token
-            assertFalse(mDevicePolicyManager.isResetPasswordTokenActive(ADMIN_RECEIVER_COMPONENT));
-
-            try {
-                // On devices with password token disabled, calling this method will throw
-                // a security exception. If that's anticipated, then return early without failing.
-                assertTrue(mDevicePolicyManager.setResetPasswordToken(ADMIN_RECEIVER_COMPONENT,
-                        TOKEN0));
-            } catch (SecurityException e) {
-                if (acceptFailure &&
-                        e.getMessage().equals("Escrow token is disabled on the current user")) {
-                    return;
-                } else {
-                    throw e;
-                }
-            }
-            assertTrue(mDevicePolicyManager.isResetPasswordTokenActive(ADMIN_RECEIVER_COMPONENT));
-
-            // resetting password with wrong token should fail
-            assertFalse(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT, PASSWORD,
-                    TOKEN1, 0));
-            // try changing password with token
-            assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT, PASSWORD,
-                    TOKEN0, 0));
-            // clear password with token
-            assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT, null,
-                    TOKEN0, 0));
-
-            // remove token and check it succeeds
-            assertTrue(mDevicePolicyManager.clearResetPasswordToken(ADMIN_RECEIVER_COMPONENT));
-            assertFalse(mDevicePolicyManager.isResetPasswordTokenActive(ADMIN_RECEIVER_COMPONENT));
-            assertFalse(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT, PASSWORD,
-                    TOKEN0, 0));
-        } finally {
-            mDevicePolicyManager.clearResetPasswordToken(ADMIN_RECEIVER_COMPONENT);
+    @Override
+    protected void tearDown() throws Exception {
+        if (mShouldRun) {
+            cleanUpResetPasswordToken();
         }
+        super.tearDown();
     }
 
-    //TODO: add test to reboot device and reset password while user is still locked.
+    public void testBadTokenShouldFail() {
+        if (!mShouldRun) {
+            return;
+        }
+        // resetting password with wrong token should fail
+        assertFalse(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT,
+                SHORT_PASSWORD, TOKEN1, 0));
+    }
+
+    public void testChangePasswordWithToken() {
+        if (!mShouldRun) {
+            return;
+        }
+        // try changing password with token
+        assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT,
+                SHORT_PASSWORD, TOKEN0, 0));
+
+        // Set a strong password constraint and expect the sufficiency check to fail
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+        mDevicePolicyManager.setPasswordMinimumLength(ADMIN_RECEIVER_COMPONENT, 6);
+        assertPasswordSufficiency(false);
+
+        // try changing to a stronger password and verify it satisfies requested constraint
+        assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT,
+                COMPLEX_PASSWORD, TOKEN0, 0));
+        assertPasswordSufficiency(true);
+    }
+
+    public void testResetPasswordFailIfQualityNotMet() {
+        if (!mShouldRun) {
+            return;
+        }
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+        mDevicePolicyManager.setPasswordMinimumLength(ADMIN_RECEIVER_COMPONENT, 6);
+
+        assertFalse(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT,
+                SHORT_PASSWORD, TOKEN0, 0));
+
+        assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT,
+                COMPLEX_PASSWORD, TOKEN0, 0));
+    }
+
+    public void testPasswordMetricAfterResetPassword() {
+        if (!mShouldRun) {
+            return;
+        }
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
+        mDevicePolicyManager.setPasswordMinimumNumeric(ADMIN_RECEIVER_COMPONENT, 1);
+        mDevicePolicyManager.setPasswordMinimumLetters(ADMIN_RECEIVER_COMPONENT, 1);
+        mDevicePolicyManager.setPasswordMinimumSymbols(ADMIN_RECEIVER_COMPONENT, 0);
+        assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT,
+                COMPLEX_PASSWORD, TOKEN0, 0));
+
+        // Change required complexity and verify new password satisfies it
+        // First set a slightly stronger requirement and expect password sufficiency is false
+        mDevicePolicyManager.setPasswordMinimumNumeric(ADMIN_RECEIVER_COMPONENT, 3);
+        mDevicePolicyManager.setPasswordMinimumLetters(ADMIN_RECEIVER_COMPONENT, 3);
+        mDevicePolicyManager.setPasswordMinimumSymbols(ADMIN_RECEIVER_COMPONENT, 2);
+        assertPasswordSufficiency(false);
+        // Then sets the appropriate quality and verify it should pass
+        mDevicePolicyManager.setPasswordMinimumSymbols(ADMIN_RECEIVER_COMPONENT, 1);
+        assertPasswordSufficiency(true);
+    }
+
+    public void testClearPasswordWithToken() {
+        if (!mShouldRun) {
+            return;
+        }
+        KeyguardManager km = mContext.getSystemService(KeyguardManager.class);
+        // First set a password
+        assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT,
+                SHORT_PASSWORD, TOKEN0, 0));
+        assertTrue(km.isDeviceSecure());
+
+        // clear password with token
+        assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT, null,
+                TOKEN0, 0));
+        assertFalse(km.isDeviceSecure());
+    }
+
+    private boolean setUpResetPasswordToken(boolean acceptFailure) {
+        // set up a token
+        assertFalse(mDevicePolicyManager.isResetPasswordTokenActive(ADMIN_RECEIVER_COMPONENT));
+
+        try {
+            // On devices with password token disabled, calling this method will throw
+            // a security exception. If that's anticipated, then return early without failing.
+            assertTrue(mDevicePolicyManager.setResetPasswordToken(ADMIN_RECEIVER_COMPONENT,
+                    TOKEN0));
+        } catch (SecurityException e) {
+            if (acceptFailure &&
+                    e.getMessage().equals("Escrow token is disabled on the current user")) {
+                return false;
+            } else {
+                throw e;
+            }
+        }
+        assertTrue(mDevicePolicyManager.isResetPasswordTokenActive(ADMIN_RECEIVER_COMPONENT));
+        return true;
+    }
+
+    private void cleanUpResetPasswordToken() {
+        // First remove device lock
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED);
+        mDevicePolicyManager.setPasswordMinimumLength(ADMIN_RECEIVER_COMPONENT, 0);
+        assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT, null,
+                TOKEN0, 0));
+
+        // Then remove token and check it succeeds
+        assertTrue(mDevicePolicyManager.clearResetPasswordToken(ADMIN_RECEIVER_COMPONENT));
+        assertFalse(mDevicePolicyManager.isResetPasswordTokenActive(ADMIN_RECEIVER_COMPONENT));
+        assertFalse(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT,
+                SHORT_PASSWORD, TOKEN0, 0));
+    }
+
+    private void assertPasswordSufficiency(boolean expectPasswordSufficient) {
+        int retries = 15;
+        // isActivePasswordSufficient() gets the result asynchronously so let's retry a few times
+        while (retries >= 0
+                && mDevicePolicyManager.isActivePasswordSufficient() != expectPasswordSufficient) {
+            retries--;
+            try {
+                Thread.sleep(200);
+            } catch (InterruptedException e) {
+                break;
+            }
+        }
+        assertEquals(expectPasswordSufficient, mDevicePolicyManager.isActivePasswordSufficient());
+    }
 }
\ No newline at end of file
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml b/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml
index c680af0..05ebac0 100644
--- a/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/AndroidManifest.xml
@@ -159,6 +159,16 @@
                 <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION" />
             </intent-filter>
         </receiver>
+
+        <!-- Dummy receiver that's decleared direct boot aware. This is needed to make the test app
+             executable by instrumentation before device unlock -->
+        <receiver android:name=".ResetPasswordWithTokenTest$DummyReceiver"
+          android:directBootAware="true" >
+          <intent-filter>
+            <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
+          </intent-filter>
+        </receiver>
+
     </application>
 
     <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ResetPasswordWithTokenTest.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ResetPasswordWithTokenTest.java
new file mode 100644
index 0000000..26c200d
--- /dev/null
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/ResetPasswordWithTokenTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.cts.managedprofile;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserManager;
+
+public class ResetPasswordWithTokenTest extends BaseManagedProfileTest {
+
+    private static final String PASSWORD0 = "1234";
+    private static final String PASSWORD1 = "123456";
+
+    private static final byte[] token = "abcdefghijklmnopqrstuvwxyz0123456789".getBytes();
+
+    /**
+     * A dummy receiver marked as direct boot aware in manifest to make this test app
+     * runnable by instrumentation before FBE unlock.
+     */
+    public static class DummyReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+        }
+    }
+
+    /**
+     * Set a reset password token and work challenge on the work profile, and then lock it
+     * with CE evicted. This is the preparation step for {@link #testResetPasswordBeforeUnlock}
+     * to put the profile in RUNNING_LOCKED state, and will be called by the hostside logic before
+     * {@link #testResetPasswordBeforeUnlock} is exercised.
+     */
+    public void setupWorkProfileAndLock() {
+        assertTrue(mDevicePolicyManager.setResetPasswordToken(ADMIN_RECEIVER_COMPONENT, token));
+        assertTrue(mDevicePolicyManager.isResetPasswordTokenActive(ADMIN_RECEIVER_COMPONENT));
+        // Reset password on the work profile will enable separate work challenge for it.
+        assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT, PASSWORD0,
+                token, 0));
+
+        mDevicePolicyManager.setPasswordQuality(ADMIN_RECEIVER_COMPONENT,
+                DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+        mDevicePolicyManager.setPasswordMinimumLength(ADMIN_RECEIVER_COMPONENT, 6);
+
+        mDevicePolicyManager.lockNow(DevicePolicyManager.FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY);
+    }
+
+    public void testResetPasswordBeforeUnlock() {
+        UserManager um = mContext.getSystemService(UserManager.class);
+        assertFalse(um.isUserUnlocked());
+        assertTrue(mDevicePolicyManager.isResetPasswordTokenActive(ADMIN_RECEIVER_COMPONENT));
+        assertTrue(mDevicePolicyManager.resetPasswordWithToken(ADMIN_RECEIVER_COMPONENT, PASSWORD1,
+                token, 0));
+        try {
+            mDevicePolicyManager.isActivePasswordSufficient();
+            fail("Did not throw expected exception.");
+        } catch (IllegalStateException expected) {}
+    }
+}
\ No newline at end of file
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
index 4b4a575..b660959 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
@@ -26,7 +26,9 @@
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Set of tests for use cases that apply to profile and device owner.
@@ -111,6 +113,7 @@
     protected static final String ASSIST_INTERACTION_SERVICE =
             ASSIST_APP_PKG + "/.MyInteractionService";
 
+    private static final String ARG_ALLOW_FAILURE = "allowFailure";
     // ID of the user all tests are run as. For device owner this will be the primary user, for
     // profile owner it is the user id of the created profile.
     protected int mUserId;
@@ -796,7 +799,12 @@
         // This is the default test for MixedDeviceOwnerTest and MixedProfileOwnerTest,
         // MixedManagedProfileOwnerTest overrides this method to execute the same test more strictly
         // without allowing failures.
-        executeDeviceTestMethod(".ResetPasswordWithTokenTest", "testResetPasswordWithTokenMayFail");
+        executeResetPasswordWithTokenTests(true);
+    }
+
+    protected void executeResetPasswordWithTokenTests(Boolean allowFailures) throws Exception {
+        runDeviceTestsAsUser(DEVICE_ADMIN_PKG, ".ResetPasswordWithTokenTest", null, mUserId,
+                Collections.singletonMap(ARG_ALLOW_FAILURE, Boolean.toString(allowFailures)));
     }
 
     protected void executeDeviceTestClass(String className) throws Exception {
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTest.java
index fb41bc9..27f479f 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/ManagedProfileTest.java
@@ -80,7 +80,7 @@
     private static final String SIMPLE_APP_APK = "CtsSimpleApp.apk";
     private static final String SIMPLE_APP_PKG = "com.android.cts.launcherapps.simpleapp";
 
-    private static final long TIMEOUT_USER_LOCKED_MILLIS = TimeUnit.SECONDS.toMillis(15);
+    private static final long TIMEOUT_USER_LOCKED_MILLIS = TimeUnit.SECONDS.toMillis(30);
 
     private static final String PARAM_PROFILE_ID = "profile-id";
 
@@ -157,6 +157,10 @@
         changeUserCredential("1234", null, mProfileUserId);
         runDeviceTestsAsUser(MANAGED_PROFILE_PKG, MANAGED_PROFILE_PKG + ".LockNowTest",
                 "testLockNowWithKeyEviction", mProfileUserId);
+        waitUntilProfileLocked();
+    }
+
+    private void waitUntilProfileLocked() throws Exception {
         final String cmd = "dumpsys activity | grep 'User #" + mProfileUserId + ": state='";
         final Pattern p = Pattern.compile("state=([\\p{Upper}_]+)$");
         SuccessCondition userLocked = () -> {
@@ -972,6 +976,20 @@
                 "testOppDisabledWhenRestrictionSet", mProfileUserId);
     }
 
+    public void testResetPasswordWithTokenBeforeUnlock() throws Exception {
+        if (!mHasFeature || !mSupportsFbe) {
+            return;
+        }
+
+        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ResetPasswordWithTokenTest",
+                "setupWorkProfileAndLock", mProfileUserId);
+        waitUntilProfileLocked();
+        runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ResetPasswordWithTokenTest",
+                "testResetPasswordBeforeUnlock", mProfileUserId);
+        // Password needs to be in sync with ResetPasswordWithTokenTest.PASSWORD1
+        verifyUserCredential("123456", mProfileUserId);
+    }
+
     private void disableActivityForUser(String activityName, int userId)
             throws DeviceNotAvailableException {
         String command = "am start -W --user " + userId
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTest.java
index 730592a..6cedba5 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTest.java
@@ -163,6 +163,6 @@
         // Execute the test method that's guaranteed to succeed. See also test in base class
         // which are tolerant to failure and executed by MixedDeviceOwnerTest and
         // MixedProfileOwnerTest
-        executeDeviceTestMethod(".ResetPasswordWithTokenTest", "testResetPasswordWithToken");
+        executeResetPasswordWithTokenTests(false);
     }
 }