blob: 1bb7eeaeebeba385a116d28fe354b6245f3be08a [file] [log] [blame]
/*
* 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);
}
}