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