1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cts.verifier.biometrics; 18 19 import android.Manifest; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.app.DialogFragment; 23 import android.app.KeyguardManager; 24 import android.content.DialogInterface; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.hardware.biometrics.BiometricManager; 28 import android.hardware.biometrics.BiometricPrompt; 29 import android.os.Bundle; 30 import android.os.CancellationSignal; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.text.InputType; 34 import android.util.Log; 35 import android.view.View; 36 import android.widget.Button; 37 import android.widget.EditText; 38 import android.widget.LinearLayout; 39 import android.widget.Toast; 40 41 import com.android.cts.verifier.PassFailButtons; 42 import com.android.cts.verifier.R; 43 44 import java.util.Random; 45 import java.util.concurrent.Executor; 46 47 /** 48 * Manual test for BiometricManager and BiometricPrompt. This tests two things currently. 49 * 1) When no biometrics are enrolled, BiometricManager and BiometricPrompt both return consistent 50 * BIOMETRIC_ERROR_NONE_ENROLLED errors). 51 * 2) When biometrics are enrolled, BiometricManager returns BIOMETRIC_SUCCESS and BiometricPrompt 52 * authentication can be successfully completed. 53 */ 54 public class BiometricTest extends PassFailButtons.Activity { 55 56 private static final String TAG = "BiometricTest"; 57 private static final String BIOMETRIC_ENROLL = "android.settings.BIOMETRIC_ENROLL"; 58 private static final int BIOMETRIC_PERMISSION_REQUEST_CODE = 0; 59 60 // Test that BiometricPrompt setAllowDeviceCredentials returns ERROR_NO_DEVICE_CREDENTIAL when 61 // pin, pattern, password is not set. 62 private static final int TEST_NOT_SECURED = 1; 63 // Test that BiometricPrompt returns BIOMETRIC_ERROR_NO_BIOMETRICS when BiometricManager 64 // states BIOMETRIC_ERROR_NONE_ENROLLED. 65 private static final int TEST_NONE_ENROLLED = 2; 66 // Test that BiometricPrompt setAllowDeviceCredentials can authenticate when no biometrics are 67 // enrolled. 68 private static final int TEST_DEVICE_CREDENTIAL = 3; 69 // Test that authentication can succeed when biometrics are enrolled. 70 private static final int TEST_AUTHENTICATE = 4; 71 // Test that the strings set from the public APIs can be seen by the user. 72 private static final int TEST_STRINGS_SEEN = 5; 73 74 private BiometricManager mBiometricManager; 75 private KeyguardManager mKeyguardManager; 76 private Handler mHandler = new Handler(Looper.getMainLooper()); 77 private CancellationSignal mCancellationSignal; 78 private int mCurrentTest; 79 80 private Button mButtonEnroll; 81 private Button mButtonTestNotSecured; 82 private Button mButtonTestNoneEnrolled; 83 private Button mButtonTestCredential; 84 private Button mButtonTestAuthenticate; 85 private Button mButtonTestStringsSeen; 86 87 private String mRandomTitle; 88 private String mRandomSubtitle; 89 private String mRandomDescription; 90 private String mRandomNegativeButtonText; 91 92 private Executor mExecutor = (runnable) -> { 93 mHandler.post(runnable); 94 }; 95 96 private BiometricPrompt.AuthenticationCallback mAuthenticationCallback = 97 new BiometricPrompt.AuthenticationCallback() { 98 @Override 99 public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { 100 if (mCurrentTest == TEST_NOT_SECURED) { 101 showToastAndLog("This should be impossible, please capture a bug report " 102 + mCurrentTest); 103 } else if (mCurrentTest == TEST_NONE_ENROLLED) { 104 showToastAndLog("This should be impossible, please capture a bug report" 105 + mCurrentTest); 106 } else if (mCurrentTest == TEST_DEVICE_CREDENTIAL) { 107 showToastAndLog("Please enroll a biometric and start the next test"); 108 mButtonTestCredential.setEnabled(false); 109 mButtonEnroll.setVisibility(View.VISIBLE); 110 mButtonTestAuthenticate.setVisibility(View.VISIBLE); 111 } else if (mCurrentTest == TEST_AUTHENTICATE) { 112 showToastAndLog("Please start the next test"); 113 mButtonTestAuthenticate.setEnabled(false); 114 mButtonTestStringsSeen.setVisibility(View.VISIBLE); 115 } else if (mCurrentTest == TEST_STRINGS_SEEN) { 116 showCheckStringsDialog(); 117 } 118 } 119 120 @Override 121 public void onAuthenticationError(int errorCode, CharSequence errString) { 122 if (mCurrentTest == TEST_NOT_SECURED) { 123 if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL) { 124 showToastAndLog("Please start the next test"); 125 mButtonTestNotSecured.setEnabled(false); 126 mButtonTestNoneEnrolled.setVisibility(View.VISIBLE); 127 } else { 128 showToastAndLog("Error: " + errorCode + " " + errString); 129 } 130 } else if (mCurrentTest == TEST_NONE_ENROLLED) { 131 if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS) { 132 mButtonTestNoneEnrolled.setEnabled(false); 133 mButtonTestCredential.setVisibility(View.VISIBLE); 134 showToastAndLog("Please start the next test"); 135 } else { 136 showToastAndLog("Error: " + errorCode + " " + errString); 137 } 138 } else if (mCurrentTest == TEST_DEVICE_CREDENTIAL) { 139 showToastAndLog(errString.toString() + " Please try again"); 140 } else if (mCurrentTest == TEST_AUTHENTICATE) { 141 showToastAndLog(errString.toString() + " Please try again"); 142 } 143 } 144 }; 145 146 private DialogInterface.OnClickListener mBiometricPromptButtonListener = (dialog, which) -> { 147 showToastAndLog("Authentication canceled"); 148 }; 149 150 @Override onCreate(Bundle savedInstanceState)151 protected void onCreate(Bundle savedInstanceState) { 152 super.onCreate(savedInstanceState); 153 setContentView(R.layout.biometric_test_main); 154 setPassFailButtonClickListeners(); 155 setInfoResources(R.string.biometric_test, R.string.biometric_test_info, -1); 156 getPassButton().setEnabled(false); 157 158 mBiometricManager = getApplicationContext().getSystemService(BiometricManager.class); 159 mKeyguardManager = getApplicationContext().getSystemService(KeyguardManager.class); 160 mButtonEnroll = findViewById(R.id.biometric_enroll_button); 161 mButtonTestNoneEnrolled = findViewById(R.id.biometric_start_test_none_enrolled); 162 mButtonTestNotSecured = findViewById(R.id.biometric_start_test_not_secured); 163 mButtonTestAuthenticate = findViewById(R.id.biometric_start_test_authenticate_button); 164 mButtonTestCredential = findViewById(R.id.biometric_start_test_credential_button); 165 mButtonTestStringsSeen = findViewById(R.id.biometric_start_test_strings_button); 166 167 PackageManager pm = getApplicationContext().getPackageManager(); 168 if (pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT) 169 || pm.hasSystemFeature(PackageManager.FEATURE_IRIS) 170 || pm.hasSystemFeature(PackageManager.FEATURE_FACE)) { 171 requestPermissions(new String[]{Manifest.permission.USE_BIOMETRIC}, 172 BIOMETRIC_PERMISSION_REQUEST_CODE); 173 174 mButtonTestNotSecured.setEnabled(false); 175 mButtonTestNotSecured.setOnClickListener((view) -> { 176 startTest(TEST_NOT_SECURED); 177 }); 178 mButtonTestNoneEnrolled.setOnClickListener((view) -> { 179 startTest(TEST_NONE_ENROLLED); 180 }); 181 mButtonTestAuthenticate.setOnClickListener((view) -> { 182 startTest(TEST_AUTHENTICATE); 183 }); 184 mButtonEnroll.setOnClickListener((view) -> { 185 final Intent intent = new Intent(); 186 intent.setAction(BIOMETRIC_ENROLL); 187 startActivity(intent); 188 }); 189 mButtonTestCredential.setOnClickListener((view) -> { 190 startTest(TEST_DEVICE_CREDENTIAL); 191 }); 192 mButtonTestStringsSeen.setOnClickListener((view) -> { 193 startTest(TEST_STRINGS_SEEN); 194 }); 195 } else { 196 // NO biometrics available 197 mButtonTestNoneEnrolled.setEnabled(false); 198 getPassButton().setEnabled(true); 199 } 200 } 201 202 @Override onRequestPermissionsResult(int requestCode, String[] permissions, int[] state)203 public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) { 204 if (requestCode == BIOMETRIC_PERMISSION_REQUEST_CODE && 205 state[0] == PackageManager.PERMISSION_GRANTED) { 206 mButtonTestNotSecured.setEnabled(true); 207 } 208 } 209 startTest(int testType)210 private void startTest(int testType) { 211 mCurrentTest = testType; 212 int result = mBiometricManager.canAuthenticate(); 213 214 if (testType == TEST_NOT_SECURED) { 215 if (mKeyguardManager.isDeviceSecure()) { 216 showToastAndLog("Please remove your pin/pattern/password and try again"); 217 } else { 218 showBiometricPrompt(true /* allowCredential */); 219 } 220 } else if (testType == TEST_NONE_ENROLLED) { 221 if (result == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { 222 showBiometricPrompt(false /* allowCredential */); 223 } else { 224 showToastAndLog("Error: " + result + " Please remove all biometrics and try again"); 225 } 226 } else if (testType == TEST_DEVICE_CREDENTIAL) { 227 if (!mKeyguardManager.isDeviceSecure()) { 228 showToastAndLog("Please set up a pin, pattern, or password and try again"); 229 } else if (result != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { 230 showToastAndLog("Error: " + result + " Please remove all biometrics and try again"); 231 } else { 232 showBiometricPrompt(true /* allowCredential */); 233 } 234 } else if (testType == TEST_AUTHENTICATE) { 235 if (result == BiometricManager.BIOMETRIC_SUCCESS) { 236 showBiometricPrompt(false /* allowCredential */); 237 } else if (result == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { 238 showToastAndLog("No biometric features, test passed."); 239 getPassButton().setEnabled(true); 240 } else { 241 showToastAndLog("Error: " + result + 242 " Please ensure at least one biometric is enrolled and try again"); 243 } 244 } else if (testType == TEST_STRINGS_SEEN) { 245 showInstructionDialogForStringsTest(); 246 } else { 247 showToastAndLog("Unknown test type: " + testType); 248 } 249 } 250 showBiometricPrompt(boolean allowCredential)251 private void showBiometricPrompt(boolean allowCredential) { 252 showBiometricPrompt(allowCredential, "Please authenticate", null, null, "Cancel"); 253 } 254 showBiometricPrompt(boolean allowCredential, String title, String subtitle, String description, String negativeButtonText)255 private void showBiometricPrompt(boolean allowCredential, String title, String subtitle, 256 String description, String negativeButtonText) { 257 BiometricPrompt.Builder builder = new BiometricPrompt.Builder(getApplicationContext()) 258 .setTitle(title) 259 .setSubtitle(subtitle) 260 .setDescription(description); 261 if (allowCredential) { 262 builder.setDeviceCredentialAllowed(true); 263 } else { 264 builder.setNegativeButton(negativeButtonText, mExecutor, 265 mBiometricPromptButtonListener); 266 } 267 BiometricPrompt bp = builder.build(); 268 mCancellationSignal = new CancellationSignal(); 269 bp.authenticate(mCancellationSignal, mExecutor, mAuthenticationCallback); 270 } 271 showToastAndLog(String string)272 private void showToastAndLog(String string) { 273 Toast.makeText(getApplicationContext(), string, Toast.LENGTH_SHORT).show(); 274 Log.v(TAG, string); 275 } 276 showInstructionDialogForStringsTest()277 private void showInstructionDialogForStringsTest() { 278 final Random random = new Random(); 279 280 mRandomTitle = String.valueOf(random.nextInt(1000)); 281 mRandomSubtitle = String.valueOf(random.nextInt(1000)); 282 mRandomDescription = String.valueOf(random.nextInt(1000)); 283 mRandomNegativeButtonText = String.valueOf(random.nextInt(1000)); 284 285 AlertDialog.Builder builder = new AlertDialog.Builder(this); 286 builder.setTitle(R.string.biometric_test_strings_title) 287 .setMessage(R.string.biometric_test_strings_instructions) 288 .setCancelable(true) 289 .setPositiveButton("Continue", new DialogInterface.OnClickListener() { 290 @Override 291 public void onClick(DialogInterface dialog, int which) { 292 showBiometricPrompt(false, 293 "Title: " + mRandomTitle, 294 "Subtitle: " + mRandomSubtitle, 295 "Description: " + mRandomDescription, 296 "Negative Button: " + mRandomNegativeButtonText); 297 } 298 }); 299 AlertDialog dialog = builder.create(); 300 dialog.show(); 301 } 302 showCheckStringsDialog()303 private void showCheckStringsDialog() { 304 LinearLayout layout = new LinearLayout(this); 305 layout.setOrientation(LinearLayout.VERTICAL); 306 307 final EditText titleBox = new EditText(this); 308 titleBox.setHint("Title"); 309 titleBox.setInputType(InputType.TYPE_CLASS_NUMBER); 310 layout.addView(titleBox); 311 312 final EditText subtitleBox = new EditText(this); 313 subtitleBox.setHint("Subtitle"); 314 subtitleBox.setInputType(InputType.TYPE_CLASS_NUMBER); 315 layout.addView(subtitleBox); 316 317 final EditText descriptionBox = new EditText(this); 318 descriptionBox.setHint("Description"); 319 descriptionBox.setInputType(InputType.TYPE_CLASS_NUMBER); 320 layout.addView(descriptionBox); 321 322 final EditText negativeBox = new EditText(this); 323 negativeBox.setHint("Negative Button"); 324 negativeBox.setInputType(InputType.TYPE_CLASS_NUMBER); 325 layout.addView(negativeBox); 326 327 AlertDialog.Builder builder = new AlertDialog.Builder(this); 328 builder.setTitle(R.string.biometric_test_strings_verify_title) 329 .setCancelable(true) 330 .setPositiveButton("Continue", new DialogInterface.OnClickListener() { 331 @Override 332 public void onClick(DialogInterface dialog, int which) { 333 final String titleEntered = titleBox.getText().toString(); 334 final String subtitleEntered = subtitleBox.getText().toString(); 335 final String descriptionEntered = descriptionBox.getText().toString(); 336 final String negativeEntered = negativeBox.getText().toString(); 337 338 if (!titleEntered.contentEquals(mRandomTitle)) { 339 showToastAndLog("Title incorrect, " 340 + titleEntered + " " + mRandomTitle); 341 } else if (!subtitleEntered.contentEquals(mRandomSubtitle)) { 342 showToastAndLog("Subtitle incorrect, " 343 + subtitleEntered + " " + mRandomSubtitle); 344 } else if (!descriptionEntered.contentEquals(mRandomDescription)) { 345 showToastAndLog("Description incorrect, " 346 + descriptionEntered + " " + mRandomDescription); 347 } else if (!negativeEntered.contentEquals(mRandomNegativeButtonText)) { 348 showToastAndLog("Negative text incorrect, " 349 + negativeEntered + " " + mRandomNegativeButtonText); 350 } else { 351 mButtonTestStringsSeen.setEnabled(false); 352 getPassButton().setEnabled(true); 353 showToastAndLog("You have passed the test!"); 354 } 355 } 356 }); 357 358 AlertDialog dialog = builder.create(); 359 360 dialog.setView(layout); 361 dialog.show(); 362 } 363 } 364