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