| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.server.biometrics; |
| |
| import static android.os.PowerManager.FULL_WAKE_LOCK; |
| import static android.server.biometrics.SensorStates.SensorState; |
| import static android.server.biometrics.SensorStates.UserState; |
| |
| import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; |
| |
| import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_IDLE; |
| import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_PENDING_CONFIRM; |
| import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING; |
| import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_SHOWING_DEVICE_CREDENTIAL; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeTrue; |
| import static org.mockito.Mockito.mock; |
| |
| import android.app.Instrumentation; |
| import android.content.ComponentName; |
| import android.content.pm.PackageManager; |
| import android.hardware.biometrics.BiometricManager; |
| import android.hardware.biometrics.BiometricManager.Authenticators; |
| import android.hardware.biometrics.BiometricPrompt; |
| import android.hardware.biometrics.BiometricTestSession; |
| import android.hardware.biometrics.SensorProperties; |
| import android.os.Bundle; |
| import android.os.CancellationSignal; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.PowerManager; |
| import android.server.wm.ActivityManagerTestBase; |
| import android.server.wm.TestJournalProvider.TestJournal; |
| import android.server.wm.UiDeviceUtils; |
| import android.server.wm.WindowManagerState; |
| import android.support.test.uiautomator.By; |
| import android.support.test.uiautomator.UiDevice; |
| import android.support.test.uiautomator.UiObject2; |
| import android.util.Log; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.server.biometrics.nano.BiometricServiceStateProto; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Base class containing useful functionality. Actual tests should be done in subclasses. |
| */ |
| abstract class BiometricTestBase extends ActivityManagerTestBase implements TestSessionList.Idler { |
| |
| private static final String TAG = "BiometricTestBase"; |
| private static final String DUMPSYS_BIOMETRIC = Utils.DUMPSYS_BIOMETRIC; |
| private static final String FLAG_CLEAR_SCHEDULER_LOG = " --clear-scheduler-buffer"; |
| |
| // Negative-side (left) buttons |
| protected static final String BUTTON_ID_NEGATIVE = "button_negative"; |
| protected static final String BUTTON_ID_USE_CREDENTIAL = "button_use_credential"; |
| |
| // Positive-side (right) buttons |
| protected static final String BUTTON_ID_CONFIRM = "button_confirm"; |
| protected static final String BUTTON_ID_TRY_AGAIN = "button_try_again"; |
| |
| // Biometric text contents |
| protected static final String TITLE_VIEW = "title"; |
| protected static final String SUBTITLE_VIEW = "subtitle"; |
| protected static final String DESCRIPTION_VIEW = "description"; |
| |
| protected static final String VIEW_ID_PASSWORD_FIELD = "lockPassword"; |
| |
| @NonNull protected Instrumentation mInstrumentation; |
| @NonNull protected BiometricManager mBiometricManager; |
| @NonNull protected List<SensorProperties> mSensorProperties; |
| @Nullable private PowerManager.WakeLock mWakeLock; |
| @NonNull protected UiDevice mDevice; |
| protected boolean mHasStrongBox; |
| |
| /** |
| * Expose this functionality to our package, since ActivityManagerTestBase's is `protected`. |
| * @param componentName |
| */ |
| void launchActivity(@NonNull ComponentName componentName) { |
| super.launchActivity(componentName); |
| } |
| |
| @Override |
| public void waitForIdleSensors() { |
| try { |
| Utils.waitForIdleService(this::getSensorStates); |
| } catch (Exception e) { |
| Log.e(TAG, "Exception when waiting for idle", e); |
| } |
| } |
| |
| /** @see Utils#getBiometricServiceCurrentState() */ |
| @NonNull |
| protected BiometricServiceState getCurrentState() throws Exception { |
| return Utils.getBiometricServiceCurrentState(); |
| } |
| |
| @NonNull |
| protected BiometricServiceState getCurrentStateAndClearSchedulerLog() throws Exception { |
| final byte[] dump = Utils.executeShellCommand(DUMPSYS_BIOMETRIC |
| + FLAG_CLEAR_SCHEDULER_LOG); |
| final BiometricServiceStateProto proto = BiometricServiceStateProto.parseFrom(dump); |
| return BiometricServiceState.parseFrom(proto); |
| } |
| |
| @Nullable |
| protected UiObject2 findView(String id) { |
| Log.d(TAG, "Finding view: " + id); |
| return mDevice.findObject(By.res(mBiometricManager.getUiPackage(), id)); |
| } |
| |
| protected void findAndPressButton(String id) { |
| final UiObject2 button = findView(id); |
| assertNotNull(button); |
| Log.d(TAG, "Clicking button: " + id); |
| button.click(); |
| } |
| |
| protected SensorStates getSensorStates() throws Exception { |
| return getCurrentState().mSensorStates; |
| } |
| |
| protected void waitForState(@BiometricServiceState.AuthSessionState int state) |
| throws Exception { |
| for (int i = 0; i < 20; i++) { |
| final BiometricServiceState serviceState = getCurrentState(); |
| if (serviceState.mState != state) { |
| Log.d(TAG, "Not in state " + state + " yet, current: " + serviceState.mState); |
| Thread.sleep(300); |
| } else { |
| return; |
| } |
| } |
| Log.d(TAG, "Timed out waiting for state to become: " + state); |
| } |
| |
| private void waitForStateNotEqual(@BiometricServiceState.AuthSessionState int state) |
| throws Exception { |
| for (int i = 0; i < 20; i++) { |
| final BiometricServiceState serviceState = getCurrentState(); |
| if (serviceState.mState == state) { |
| Log.d(TAG, "Not out of state yet, current: " + serviceState.mState); |
| Thread.sleep(300); |
| } else { |
| return; |
| } |
| } |
| Log.d(TAG, "Timed out waiting for state to not equal: " + state); |
| } |
| |
| private boolean anyEnrollmentsExist() throws Exception { |
| final BiometricServiceState serviceState = getCurrentState(); |
| |
| for (SensorState sensorState : serviceState.mSensorStates.sensorStates.values()) { |
| for (UserState userState : sensorState.getUserStates().values()) { |
| if (userState.numEnrolled != 0) { |
| Log.d(TAG, "Enrollments still exist: " + serviceState); |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| protected void successfullyAuthenticate(@NonNull BiometricTestSession session, int userId) |
| throws Exception { |
| session.acceptAuthentication(userId); |
| mInstrumentation.waitForIdleSync(); |
| waitForStateNotEqual(STATE_AUTH_STARTED_UI_SHOWING); |
| BiometricServiceState state = getCurrentState(); |
| Log.d(TAG, "State after acceptAuthentication: " + state); |
| if (state.mState == STATE_AUTH_PENDING_CONFIRM) { |
| findAndPressButton(BUTTON_ID_CONFIRM); |
| mInstrumentation.waitForIdleSync(); |
| waitForState(STATE_AUTH_IDLE); |
| } else { |
| waitForState(STATE_AUTH_IDLE); |
| } |
| |
| assertEquals("Failed to become idle after authenticating", |
| STATE_AUTH_IDLE, getCurrentState().mState); |
| } |
| |
| protected void successfullyEnterCredential() throws Exception { |
| waitForState(STATE_SHOWING_DEVICE_CREDENTIAL); |
| BiometricServiceState state = getCurrentState(); |
| assertTrue(state.toString(), state.mSensorStates.areAllSensorsIdle()); |
| assertEquals(state.toString(), STATE_SHOWING_DEVICE_CREDENTIAL, state.mState); |
| |
| // Wait for any animations to complete. Ideally, this should be reflected in |
| // STATE_SHOWING_DEVICE_CREDENTIAL, but SysUI and BiometricService are different processes |
| // so we'd need to add some additional plumbing. We can improve this in the future. |
| // TODO(b/152240892) |
| Thread.sleep(1000); |
| |
| // Enter credential. AuthSession done, authentication callback received |
| final UiObject2 passwordField = findView(VIEW_ID_PASSWORD_FIELD); |
| Log.d(TAG, "Focusing, entering, submitting credential"); |
| passwordField.click(); |
| passwordField.setText(LOCK_CREDENTIAL); |
| mDevice.pressEnter(); |
| waitForState(STATE_AUTH_IDLE); |
| |
| state = getCurrentState(); |
| assertEquals(state.toString(), STATE_AUTH_IDLE, state.mState); |
| } |
| |
| protected void cancelAuthentication(@NonNull CancellationSignal cancel) throws Exception { |
| cancel.cancel(); |
| mInstrumentation.waitForIdleSync(); |
| waitForState(STATE_AUTH_IDLE); |
| |
| //TODO(b/152240892): Currently BiometricService does not get a signal from SystemUI |
| // when the dialog finishes animating away. |
| Thread.sleep(1000); |
| |
| BiometricServiceState state = getCurrentState(); |
| assertEquals("Not idle after requesting cancellation", state.mState, STATE_AUTH_IDLE); |
| } |
| |
| protected void waitForAllUnenrolled() throws Exception { |
| for (int i = 0; i < 20; i++) { |
| if (anyEnrollmentsExist()) { |
| Log.d(TAG, "Enrollments still exist.."); |
| Thread.sleep(300); |
| } else { |
| return; |
| } |
| } |
| fail("Some sensors still have enrollments. State: " + getCurrentState()); |
| } |
| |
| /** |
| * Shows a BiometricPrompt that specifies {@link Authenticators#DEVICE_CREDENTIAL}. |
| */ |
| protected void showCredentialOnlyBiometricPrompt( |
| @NonNull BiometricPrompt.AuthenticationCallback callback, |
| @NonNull CancellationSignal cancellationSignal, |
| boolean shouldShow) throws Exception { |
| showCredentialOnlyBiometricPromptWithContents(callback, cancellationSignal, shouldShow, |
| "Title", "Subtitle", "Description"); |
| } |
| |
| /** |
| * Shows a BiometricPrompt that specifies {@link Authenticators#DEVICE_CREDENTIAL} |
| * and the specified contents. |
| */ |
| protected void showCredentialOnlyBiometricPromptWithContents( |
| @NonNull BiometricPrompt.AuthenticationCallback callback, |
| @NonNull CancellationSignal cancellationSignal, boolean shouldShow, |
| @NonNull String title, @NonNull String subtitle, |
| @NonNull String description) throws Exception { |
| final Handler handler = new Handler(Looper.getMainLooper()); |
| final Executor executor = handler::post; |
| final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext) |
| .setTitle(title) |
| .setSubtitle(subtitle) |
| .setDescription(description) |
| .setAllowedAuthenticators(Authenticators.DEVICE_CREDENTIAL) |
| .setAllowBackgroundAuthentication(true) |
| .build(); |
| |
| prompt.authenticate(cancellationSignal, executor, callback); |
| mInstrumentation.waitForIdleSync(); |
| |
| // Wait for any animations to complete. Ideally, this should be reflected in |
| // STATE_SHOWING_DEVICE_CREDENTIAL, but SysUI and BiometricService are different processes |
| // so we'd need to add some additional plumbing. We can improve this in the future. |
| // TODO(b/152240892) |
| Thread.sleep(1000); |
| |
| if (shouldShow) { |
| waitForState(STATE_SHOWING_DEVICE_CREDENTIAL); |
| BiometricServiceState state = getCurrentState(); |
| assertEquals(state.toString(), STATE_SHOWING_DEVICE_CREDENTIAL, state.mState); |
| } else { |
| Utils.waitForIdleService(this::getSensorStates); |
| } |
| } |
| |
| /** |
| * SHows a BiometricPrompt that sets |
| * {@link BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)} to true. |
| */ |
| protected void showDeviceCredentialAllowedBiometricPrompt( |
| @NonNull BiometricPrompt.AuthenticationCallback callback, |
| @NonNull CancellationSignal cancellationSignal, |
| boolean shouldShow) throws Exception { |
| final Handler handler = new Handler(Looper.getMainLooper()); |
| final Executor executor = handler::post; |
| final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext) |
| .setTitle("Title") |
| .setSubtitle("Subtitle") |
| .setDescription("Description") |
| .setDeviceCredentialAllowed(true) |
| .setAllowBackgroundAuthentication(true) |
| .build(); |
| |
| prompt.authenticate(cancellationSignal, executor, callback); |
| mInstrumentation.waitForIdleSync(); |
| |
| // Wait for any animations to complete. Ideally, this should be reflected in |
| // STATE_SHOWING_DEVICE_CREDENTIAL, but SysUI and BiometricService are different processes |
| // so we'd need to add some additional plumbing. We can improve this in the future. |
| // TODO(b/152240892) |
| Thread.sleep(1000); |
| |
| if (shouldShow) { |
| waitForState(STATE_SHOWING_DEVICE_CREDENTIAL); |
| BiometricServiceState state = getCurrentState(); |
| assertEquals(state.toString(), STATE_SHOWING_DEVICE_CREDENTIAL, state.mState); |
| } else { |
| Utils.waitForIdleService(this::getSensorStates); |
| } |
| } |
| |
| protected void showDefaultBiometricPrompt(int sensorId, int userId, |
| boolean requireConfirmation, @NonNull BiometricPrompt.AuthenticationCallback callback, |
| @NonNull CancellationSignal cancellationSignal) throws Exception { |
| final Handler handler = new Handler(Looper.getMainLooper()); |
| final Executor executor = handler::post; |
| final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext) |
| .setTitle("Title") |
| .setSubtitle("Subtitle") |
| .setDescription("Description") |
| .setConfirmationRequired(requireConfirmation) |
| .setNegativeButton("Negative Button", executor, (dialog, which) -> { |
| Log.d(TAG, "Negative button pressed"); |
| }) |
| .setAllowBackgroundAuthentication(true) |
| .setAllowedSensorIds(new ArrayList<>(Collections.singletonList(sensorId))) |
| .build(); |
| prompt.authenticate(cancellationSignal, executor, callback); |
| |
| waitForState(STATE_AUTH_STARTED_UI_SHOWING); |
| } |
| |
| /** |
| * Shows the default BiometricPrompt (sensors meeting BIOMETRIC_WEAK) with a negative button, |
| * but does not complete authentication. In other words, the dialog will stay on the screen. |
| */ |
| protected void showDefaultBiometricPromptWithContents(int sensorId, int userId, |
| boolean requireConfirmation, @NonNull BiometricPrompt.AuthenticationCallback callback, |
| @NonNull String title, @NonNull String subtitle, @NonNull String description, |
| @NonNull String negativeButtonText) throws Exception { |
| final Handler handler = new Handler(Looper.getMainLooper()); |
| final Executor executor = handler::post; |
| final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext) |
| .setTitle(title) |
| .setSubtitle(subtitle) |
| .setDescription(description) |
| .setConfirmationRequired(requireConfirmation) |
| .setNegativeButton(negativeButtonText, executor, (dialog, which) -> { |
| Log.d(TAG, "Negative button pressed"); |
| }) |
| .setAllowBackgroundAuthentication(true) |
| .setAllowedSensorIds(new ArrayList<>(Collections.singletonList(sensorId))) |
| .build(); |
| prompt.authenticate(new CancellationSignal(), executor, callback); |
| |
| waitForState(STATE_AUTH_STARTED_UI_SHOWING); |
| } |
| |
| /** |
| * Shows the default BiometricPrompt (sensors meeting BIOMETRIC_WEAK) with a negative button, |
| * and fakes successful authentication via TestApis. |
| */ |
| protected void showDefaultBiometricPromptAndAuth(@NonNull BiometricTestSession session, |
| int sensorId, int userId) throws Exception { |
| BiometricPrompt.AuthenticationCallback callback = mock( |
| BiometricPrompt.AuthenticationCallback.class); |
| showDefaultBiometricPromptWithContents(sensorId, userId, false /* requireConfirmation */, |
| callback, "Title", "Subtitle", "Description", "Negative Button"); |
| successfullyAuthenticate(session, userId); |
| } |
| |
| protected void showBiometricPromptWithAuthenticators(int authenticators) { |
| final Handler handler = new Handler(Looper.getMainLooper()); |
| final Executor executor = handler::post; |
| final BiometricPrompt prompt = new BiometricPrompt.Builder(mContext) |
| .setTitle("Title") |
| .setSubtitle("Subtitle") |
| .setDescription("Description") |
| .setNegativeButton("Negative Button", executor, (dialog, which) -> { |
| Log.d(TAG, "Negative button pressed"); |
| }) |
| .setAllowBackgroundAuthentication(true) |
| .setAllowedAuthenticators(authenticators) |
| .build(); |
| prompt.authenticate(new CancellationSignal(), executor, |
| new BiometricPrompt.AuthenticationCallback() { |
| @Override |
| public void onAuthenticationError(int errorCode, CharSequence errString) { |
| Log.d(TAG, "onAuthenticationError: " + errorCode); |
| } |
| |
| @Override |
| public void onAuthenticationSucceeded( |
| BiometricPrompt.AuthenticationResult result) { |
| Log.d(TAG, "onAuthenticationSucceeded"); |
| } |
| }); |
| } |
| |
| protected void launchActivityAndWaitForResumed(@NonNull ActivitySession activitySession) { |
| activitySession.start(); |
| mWmState.waitForActivityState(activitySession.getComponentName(), |
| WindowManagerState.STATE_RESUMED); |
| mInstrumentation.waitForIdleSync(); |
| } |
| |
| protected void closeActivity(@NonNull ActivitySession activitySession) throws Exception { |
| activitySession.close(); |
| mInstrumentation.waitForIdleSync(); |
| } |
| |
| protected int getCurrentStrength(int sensorId) throws Exception { |
| final BiometricServiceState serviceState = getCurrentState(); |
| return serviceState.mSensorStates.sensorStates.get(sensorId).getCurrentStrength(); |
| } |
| |
| protected List<Integer> getSensorsOfTargetStrength(int targetStrength) { |
| final List<Integer> sensors = new ArrayList<>(); |
| for (SensorProperties prop : mSensorProperties) { |
| if (prop.getSensorStrength() == targetStrength) { |
| sensors.add(prop.getSensorId()); |
| } |
| } |
| Log.d(TAG, "getSensorsOfTargetStrength: num of target sensors=" + sensors.size()); |
| return sensors; |
| } |
| |
| @NonNull |
| protected static BiometricCallbackHelper.State getCallbackState(@NonNull TestJournal journal) { |
| Utils.waitFor("Waiting for authentication callback", |
| () -> journal.extras.containsKey(BiometricCallbackHelper.KEY), |
| (lastResult) -> fail("authentication callback never received - died waiting")); |
| |
| final Bundle bundle = journal.extras.getBundle(BiometricCallbackHelper.KEY); |
| final BiometricCallbackHelper.State state = |
| BiometricCallbackHelper.State.fromBundle(bundle); |
| |
| // Clear the extras since we want to wait for the journal to sync any new info the next |
| // time it's read |
| journal.extras.clear(); |
| |
| return state; |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| mInstrumentation = getInstrumentation(); |
| mBiometricManager = mInstrumentation.getContext().getSystemService(BiometricManager.class); |
| |
| mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(); |
| mDevice = UiDevice.getInstance(mInstrumentation); |
| mSensorProperties = mBiometricManager.getSensorProperties(); |
| |
| assumeTrue(mInstrumentation.getContext().getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_SECURE_LOCK_SCREEN)); |
| |
| mHasStrongBox = mContext.getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_STRONGBOX_KEYSTORE); |
| |
| // Keep the screen on for the duration of each test, since BiometricPrompt goes away |
| // when screen turns off. |
| final PowerManager pm = mInstrumentation.getContext().getSystemService(PowerManager.class); |
| mWakeLock = pm.newWakeLock(FULL_WAKE_LOCK, TAG); |
| mWakeLock.acquire(); |
| |
| // Turn screen on and dismiss keyguard |
| UiDeviceUtils.pressWakeupButton(); |
| UiDeviceUtils.pressUnlockButton(); |
| } |
| |
| @After |
| public void cleanup() { |
| mInstrumentation.waitForIdleSync(); |
| |
| // Authentication lifecycle is done |
| waitForIdleSensors(); |
| |
| if (mWakeLock != null) { |
| mWakeLock.release(); |
| } |
| mInstrumentation.getUiAutomation().dropShellPermissionIdentity(); |
| } |
| |
| protected void enrollForSensor(@NonNull BiometricTestSession session, int sensorId) |
| throws Exception { |
| Log.d(TAG, "Enrolling for sensor: " + sensorId); |
| final int userId = 0; |
| |
| session.startEnroll(userId); |
| mInstrumentation.waitForIdleSync(); |
| Utils.waitForBusySensor(sensorId, this::getSensorStates); |
| |
| session.finishEnroll(userId); |
| mInstrumentation.waitForIdleSync(); |
| Utils.waitForIdleService(this::getSensorStates); |
| |
| final BiometricServiceState state = getCurrentState(); |
| assertEquals("Sensor: " + sensorId + " should have exactly one enrollment", |
| 1, state.mSensorStates.sensorStates |
| .get(sensorId).getUserStates().get(userId).numEnrolled); |
| } |
| } |