• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
238                 showToastAndLog("Error: " + result +
239                         " Please ensure at least one biometric is enrolled and try again");
240             }
241         } else if (testType == TEST_STRINGS_SEEN) {
242             showInstructionDialogForStringsTest();
243         } else {
244             showToastAndLog("Unknown test type: " + testType);
245         }
246     }
247 
showBiometricPrompt(boolean allowCredential)248     private void showBiometricPrompt(boolean allowCredential) {
249         showBiometricPrompt(allowCredential, "Please authenticate", null, null, "Cancel");
250     }
251 
showBiometricPrompt(boolean allowCredential, String title, String subtitle, String description, String negativeButtonText)252     private void showBiometricPrompt(boolean allowCredential, String title, String subtitle,
253             String description, String negativeButtonText) {
254         BiometricPrompt.Builder builder = new BiometricPrompt.Builder(getApplicationContext())
255                 .setTitle(title)
256                 .setSubtitle(subtitle)
257                 .setDescription(description);
258         if (allowCredential) {
259             builder.setDeviceCredentialAllowed(true);
260         } else {
261             builder.setNegativeButton(negativeButtonText, mExecutor,
262                     mBiometricPromptButtonListener);
263         }
264         BiometricPrompt bp = builder.build();
265         mCancellationSignal = new CancellationSignal();
266         bp.authenticate(mCancellationSignal, mExecutor, mAuthenticationCallback);
267     }
268 
showToastAndLog(String string)269     private void showToastAndLog(String string) {
270         Toast.makeText(getApplicationContext(), string, Toast.LENGTH_SHORT).show();
271         Log.v(TAG, string);
272     }
273 
showInstructionDialogForStringsTest()274     private void showInstructionDialogForStringsTest() {
275         final Random random = new Random();
276 
277         mRandomTitle = String.valueOf(random.nextInt(1000));
278         mRandomSubtitle = String.valueOf(random.nextInt(1000));
279         mRandomDescription = String.valueOf(random.nextInt(1000));
280         mRandomNegativeButtonText = String.valueOf(random.nextInt(1000));
281 
282         AlertDialog.Builder builder = new AlertDialog.Builder(this);
283         builder.setTitle(R.string.biometric_test_strings_title)
284                 .setMessage(R.string.biometric_test_strings_instructions)
285                 .setCancelable(true)
286                 .setPositiveButton("Continue", new DialogInterface.OnClickListener() {
287                     @Override
288                     public void onClick(DialogInterface dialog, int which) {
289                         showBiometricPrompt(false,
290                                 "Title: " + mRandomTitle,
291                                 "Subtitle: " + mRandomSubtitle,
292                                 "Description: " + mRandomDescription,
293                                 "Negative Button: " + mRandomNegativeButtonText);
294                     }
295                 });
296         AlertDialog dialog = builder.create();
297         dialog.show();
298     }
299 
showCheckStringsDialog()300     private void showCheckStringsDialog() {
301         LinearLayout layout = new LinearLayout(this);
302         layout.setOrientation(LinearLayout.VERTICAL);
303 
304         final EditText titleBox = new EditText(this);
305         titleBox.setHint("Title");
306         titleBox.setInputType(InputType.TYPE_CLASS_NUMBER);
307         layout.addView(titleBox);
308 
309         final EditText subtitleBox = new EditText(this);
310         subtitleBox.setHint("Subtitle");
311         subtitleBox.setInputType(InputType.TYPE_CLASS_NUMBER);
312         layout.addView(subtitleBox);
313 
314         final EditText descriptionBox = new EditText(this);
315         descriptionBox.setHint("Description");
316         descriptionBox.setInputType(InputType.TYPE_CLASS_NUMBER);
317         layout.addView(descriptionBox);
318 
319         final EditText negativeBox = new EditText(this);
320         negativeBox.setHint("Negative Button");
321         negativeBox.setInputType(InputType.TYPE_CLASS_NUMBER);
322         layout.addView(negativeBox);
323 
324         AlertDialog.Builder builder = new AlertDialog.Builder(this);
325         builder.setTitle(R.string.biometric_test_strings_verify_title)
326                 .setCancelable(true)
327                 .setPositiveButton("Continue", new DialogInterface.OnClickListener() {
328                     @Override
329                     public void onClick(DialogInterface dialog, int which) {
330                         final String titleEntered = titleBox.getText().toString();
331                         final String subtitleEntered = subtitleBox.getText().toString();
332                         final String descriptionEntered = descriptionBox.getText().toString();
333                         final String negativeEntered = negativeBox.getText().toString();
334 
335                         if (!titleEntered.contentEquals(mRandomTitle)) {
336                             showToastAndLog("Title incorrect, "
337                                     + titleEntered + " " + mRandomTitle);
338                         } else if (!subtitleEntered.contentEquals(mRandomSubtitle)) {
339                             showToastAndLog("Subtitle incorrect, "
340                                     + subtitleEntered + " " + mRandomSubtitle);
341                         } else if (!descriptionEntered.contentEquals(mRandomDescription)) {
342                             showToastAndLog("Description incorrect, "
343                                     + descriptionEntered + " " + mRandomDescription);
344                         } else if (!negativeEntered.contentEquals(mRandomNegativeButtonText)) {
345                             showToastAndLog("Negative text incorrect, "
346                                     + negativeEntered + " " + mRandomNegativeButtonText);
347                         } else {
348                             mButtonTestStringsSeen.setEnabled(false);
349                             getPassButton().setEnabled(true);
350                             showToastAndLog("You have passed the test!");
351                         }
352                     }
353                 });
354 
355         AlertDialog dialog = builder.create();
356 
357         dialog.setView(layout);
358         dialog.show();
359     }
360 }
361