1 /*
2  * Copyright (C) 2023 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.google.android.car.kitchensink.biometrics;
18 
19 import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
20 import static android.hardware.biometrics.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
21 
22 import static com.google.android.car.kitchensink.KitchenSinkActivity.DUMP_ARG_CMD;
23 
24 import android.app.Activity;
25 import android.app.KeyguardManager;
26 import android.content.Intent;
27 import android.hardware.biometrics.BiometricPrompt;
28 import android.os.Bundle;
29 import android.os.CancellationSignal;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.text.Editable;
33 import android.text.method.ScrollingMovementMethod;
34 import android.util.Log;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.TextView;
39 
40 import androidx.activity.result.ActivityResultLauncher;
41 import androidx.activity.result.contract.ActivityResultContracts;
42 import androidx.annotation.Nullable;
43 import androidx.fragment.app.Fragment;
44 
45 import com.google.android.car.kitchensink.R;
46 
47 import java.io.FileDescriptor;
48 import java.io.PrintWriter;
49 import java.util.Arrays;
50 import java.util.concurrent.CountDownLatch;
51 import java.util.concurrent.Executor;
52 
53 /**
54  * This uses {@link BiometricPrompt} API to verify the device screen lock UI.
55  *
56  * <p>Once the activity hosting this fragment is launched, it can be controlled using {@code adb}.
57  * Example:
58  *
59  * <pre><code>
60  adb shell 'am start -n com.google.android.car.kitchensink/.KitchenSinkActivity \
61     --es select "BiometricPrompt"'
62  adb shell 'dumpsys activity com.google.android.car.kitchensink/.KitchenSinkActivity \
63     fragment "BiometricPrompt" cmd device'
64  * </code></pre>
65  */
66 public final class BiometricPromptTestFragment extends Fragment {
67 
68     private static final String TAG = BiometricPromptTestFragment.class.getSimpleName();
69 
70     public static final String FRAGMENT_NAME = "BiometricPrompt";
71 
72     private static final String CMD_HELP = "help";
73     private static final String CMD_DEVICE = "device";
74     private static final String CMD_BIOMETRIC = "biometric";
75     private static final String CMD_INTENT = "intent";
76 
77     private final Handler mHandler = new Handler(Looper.getMainLooper());
78     private final CountDownLatch mLatch = new CountDownLatch(1);
79     private final Executor mExecutor = mHandler::post;
80     private Editable mEditable;
81 
82     private final BiometricPrompt.AuthenticationCallback mAuthenticationCallback =
83             new BiometricPrompt.AuthenticationCallback() {
84                 @Override
85                 public void onAuthenticationError(int errorCode, CharSequence errString) {
86                     mLatch.countDown();
87                     logMessage("onAuthenticationError: " + errorCode);
88                 }
89 
90                 @Override
91                 public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
92                     logMessage("onAuthenticationHelp: " + helpString);
93                 }
94 
95                 @Override
96                 public void onAuthenticationFailed() {
97                     logMessage("onAuthenticationFailed");
98                 }
99 
100                 @Override
101                 public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
102                     logMessage("onAuthenticationSucceeded: " + result);
103                 }
104             };
105 
106     private final ActivityResultLauncher<Intent> mStartForResult =
107             registerForActivityResult(
108                     new ActivityResultContracts.StartActivityForResult(),
109                     result -> {
110                         if (result.getResultCode() == Activity.RESULT_OK) {
111                             Intent intent = result.getData();
112                             logMessage("ConfirmDeviceCredential OK: " + intent);
113                         } else {
114                             logMessage("ConfirmDeviceCredential not OK: " + result.getResultCode());
115                         }
116                     });
117 
118     @Override
onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)119     public View onCreateView(
120             LayoutInflater inflater,
121             @Nullable ViewGroup container,
122             @Nullable Bundle savedInstanceState) {
123         View view = inflater.inflate(R.layout.biometric_prompt_fragment, container, false);
124         TextView textView = (TextView) view.findViewById(R.id.messages);
125         assert textView != null;
126         textView.setMovementMethod(new ScrollingMovementMethod());
127         mEditable = textView.getEditableText();
128 
129         view.findViewById(R.id.button_auth_device_credential)
130                 .setOnClickListener(v -> authenticate(DEVICE_CREDENTIAL | BIOMETRIC_WEAK));
131         view.findViewById(R.id.button_auth_biometric)
132                 .setOnClickListener(v -> authenticate(BIOMETRIC_WEAK));
133         view.findViewById(R.id.button_auth_device_credential_intent)
134                 .setOnClickListener(v -> confirmWithDeviceCredentialIntent());
135         return view;
136     }
137 
138     @Override
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)139     public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
140         Log.v(TAG, "dump(): " + Arrays.toString(args));
141 
142         if (args != null && args.length > 0 && args[0].equals(DUMP_ARG_CMD)) {
143             runCmd(writer, args);
144             return;
145         }
146     }
147 
148     // Authentication by BiometricPrompt API
authenticate(int authenticators)149     private void authenticate(int authenticators) {
150         String title = "Title";
151         String subtitle = "Subtitle";
152         String description = "Description";
153         String negativeButtonText = "Negative Button";
154         BiometricPrompt.Builder builder =
155                 new BiometricPrompt.Builder(getContext())
156                         .setTitle(title)
157                         .setSubtitle(subtitle)
158                         .setDescription(description)
159                         .setConfirmationRequired(false)
160                         .setAllowedAuthenticators(authenticators);
161 
162         if ((authenticators & DEVICE_CREDENTIAL) != DEVICE_CREDENTIAL) {
163             // Can't have both negative button behavior and device credential enabled
164             builder.setNegativeButton(
165                     negativeButtonText,
166                     mExecutor,
167                     (dialog, which) -> {
168                         Log.d(TAG, "No opt on NegativeButton.");
169                     });
170         }
171         BiometricPrompt prompt = builder.build();
172         CancellationSignal cancellationSignal = new CancellationSignal();
173         logMessage("BiometricPrompt.authenticate with: " + prompt);
174         prompt.authenticate(cancellationSignal, mExecutor, mAuthenticationCallback);
175     }
176 
177     // Authentication by Keyguard API for deprecated in API 29
confirmWithDeviceCredentialIntent()178     private void confirmWithDeviceCredentialIntent() {
179         KeyguardManager keyguardManager;
180         keyguardManager = getContext().getSystemService(KeyguardManager.class);
181         if (keyguardManager == null) {
182             logMessage("Failed to get the KeyguardManager service.");
183             return;
184         }
185         Intent intent =
186                 keyguardManager.createConfirmDeviceCredentialIntent(
187                         "Title", "createConfirmDeviceCredentialIntent");
188         if (intent == null) {
189             logMessage("Failed to get the KeyguardManager service.");
190             return;
191         }
192         mStartForResult.launch(intent);
193     }
194 
logMessage(CharSequence message)195     private void logMessage(CharSequence message) {
196         mEditable.insert(0, message + "\n");
197         Log.d(TAG, message.toString());
198     }
199 
runCmd(PrintWriter writer, String[] args)200     private void runCmd(PrintWriter writer, String[] args) {
201         if (args.length < 2) {
202             writer.println("missing command\n");
203             return;
204         }
205         String cmd = args[1];
206         switch (cmd) {
207             case CMD_HELP:
208                 cmdShowHelp(writer);
209                 break;
210             case CMD_DEVICE:
211                 authenticate(DEVICE_CREDENTIAL | BIOMETRIC_WEAK);
212                 break;
213             case CMD_BIOMETRIC:
214                 authenticate(BIOMETRIC_WEAK);
215                 break;
216             case CMD_INTENT:
217                 confirmWithDeviceCredentialIntent();
218                 break;
219 
220             default:
221                 cmdShowHelp(writer);
222                 writer.printf("Invalid cmd: %s\n", Arrays.toString(args));
223         }
224         return;
225     }
226 
cmdShowHelp(PrintWriter writer)227     private void cmdShowHelp(PrintWriter writer) {
228         writer.println("Available commands:\n");
229         showCommandHelp(writer, "Shows this help message.", CMD_HELP);
230         showCommandHelp(writer,
231                 "BiometricPrompt#authenticate by DEVICE_CREDENTIAL | BIOMETRIC_WEAK.",
232                 CMD_DEVICE);
233         showCommandHelp(writer,
234                 "BiometricPrompt#authenticate by BIOMETRIC_WEAK.",
235                 CMD_BIOMETRIC);
236         showCommandHelp(writer,
237                 "Authenticates by KeyguardManager#createConfirmDeviceCredentialIntent.",
238                 CMD_INTENT);
239     }
240 
showCommandHelp(PrintWriter writer, String description, String cmd, String... args)241     private void showCommandHelp(PrintWriter writer, String description, String cmd,
242             String... args) {
243         writer.printf("%s", cmd);
244         if (args != null) {
245             for (String arg : args) {
246                 writer.printf(" %s", arg);
247             }
248         }
249         writer.println(":");
250         writer.printf("  %s\n\n", description);
251     }
252 }
253