1 /* 2 * Copyright (C) 2020 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 android.hardware.biometrics; 18 19 import static android.Manifest.permission.TEST_BIOMETRIC; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.RequiresPermission; 24 import android.annotation.TestApi; 25 import android.content.Context; 26 import android.os.RemoteException; 27 import android.util.ArraySet; 28 import android.util.Log; 29 30 import java.util.concurrent.CountDownLatch; 31 import java.util.concurrent.TimeUnit; 32 33 /** 34 * Common set of interfaces to test biometric-related APIs, including {@link BiometricPrompt} and 35 * {@link android.hardware.fingerprint.FingerprintManager}. 36 * @hide 37 */ 38 @TestApi 39 public class BiometricTestSession implements AutoCloseable { 40 private static final String BASE_TAG = "BiometricTestSession"; 41 42 /** 43 * @hide 44 */ 45 public interface TestSessionProvider { 46 @NonNull createTestSession(@onNull Context context, int sensorId, @NonNull ITestSessionCallback callback)47 ITestSession createTestSession(@NonNull Context context, int sensorId, 48 @NonNull ITestSessionCallback callback) throws RemoteException; 49 } 50 51 private final Context mContext; 52 private final int mSensorId; 53 private final ITestSession mTestSession; 54 55 // Keep track of users that were tested, which need to be cleaned up when finishing. 56 @NonNull private final ArraySet<Integer> mTestedUsers; 57 58 // Track the users currently cleaning up, and provide a latch that gets notified when all 59 // users have finished cleaning up. This is an imperfect system, as there can technically be 60 // multiple cleanups per user. Theoretically we should track the cleanup's BaseClientMonitor's 61 // unique ID, but it's complicated to plumb it through. This should be fine for now. 62 @Nullable private CountDownLatch mCloseLatch; 63 @NonNull private final ArraySet<Integer> mUsersCleaningUp; 64 65 private final ITestSessionCallback mCallback = new ITestSessionCallback.Stub() { 66 @Override 67 public void onCleanupStarted(int userId) { 68 Log.d(getTag(), "onCleanupStarted, sensor: " + mSensorId + ", userId: " + userId); 69 } 70 71 @Override 72 public void onCleanupFinished(int userId) { 73 Log.d(getTag(), "onCleanupFinished, sensor: " + mSensorId 74 + ", userId: " + userId 75 + ", remaining users: " + mUsersCleaningUp.size()); 76 mUsersCleaningUp.remove(userId); 77 78 if (mUsersCleaningUp.isEmpty() && mCloseLatch != null) { 79 mCloseLatch.countDown(); 80 } 81 } 82 }; 83 84 /** 85 * @hide 86 */ BiometricTestSession(@onNull Context context, int sensorId, @NonNull TestSessionProvider testSessionProvider)87 public BiometricTestSession(@NonNull Context context, int sensorId, 88 @NonNull TestSessionProvider testSessionProvider) throws RemoteException { 89 mContext = context; 90 mSensorId = sensorId; 91 mTestSession = testSessionProvider.createTestSession(context, sensorId, mCallback); 92 mTestedUsers = new ArraySet<>(); 93 mUsersCleaningUp = new ArraySet<>(); 94 setTestHalEnabled(true); 95 } 96 97 /** 98 * Switches the specified sensor to use a test HAL. In this mode, the framework will not invoke 99 * any methods on the real HAL implementation. This allows the framework to test a substantial 100 * portion of the framework code that would otherwise require human interaction. Note that 101 * secure pathways such as HAT/Keystore are not testable, since they depend on the TEE or its 102 * equivalent for the secret key. 103 * 104 * @param enabled If true, enable testing with a fake HAL instead of the real HAL. 105 */ 106 @RequiresPermission(TEST_BIOMETRIC) setTestHalEnabled(boolean enabled)107 private void setTestHalEnabled(boolean enabled) { 108 try { 109 Log.w(getTag(), "setTestHalEnabled, sensor: " + mSensorId + " enabled: " + enabled); 110 mTestSession.setTestHalEnabled(enabled); 111 } catch (RemoteException e) { 112 throw e.rethrowFromSystemServer(); 113 } 114 } 115 116 /** 117 * Starts the enrollment process. This should generally be used when the test HAL is enabled. 118 * 119 * @param userId User that this command applies to. 120 */ 121 @RequiresPermission(TEST_BIOMETRIC) startEnroll(int userId)122 public void startEnroll(int userId) { 123 try { 124 mTestedUsers.add(userId); 125 mTestSession.startEnroll(userId); 126 } catch (RemoteException e) { 127 throw e.rethrowFromSystemServer(); 128 } 129 } 130 131 /** 132 * Finishes the enrollment process. Simulates the HAL's callback. 133 * 134 * @param userId User that this command applies to. 135 */ 136 @RequiresPermission(TEST_BIOMETRIC) finishEnroll(int userId)137 public void finishEnroll(int userId) { 138 try { 139 mTestedUsers.add(userId); 140 mTestSession.finishEnroll(userId); 141 } catch (RemoteException e) { 142 throw e.rethrowFromSystemServer(); 143 } 144 } 145 146 /** 147 * Simulates a successful authentication, but does not provide a valid HAT. 148 * 149 * @param userId User that this command applies to. 150 */ 151 @RequiresPermission(TEST_BIOMETRIC) acceptAuthentication(int userId)152 public void acceptAuthentication(int userId) { 153 try { 154 mTestSession.acceptAuthentication(userId); 155 } catch (RemoteException e) { 156 throw e.rethrowFromSystemServer(); 157 } 158 } 159 160 /** 161 * Simulates a rejected attempt. 162 * 163 * @param userId User that this command applies to. 164 */ 165 @RequiresPermission(TEST_BIOMETRIC) rejectAuthentication(int userId)166 public void rejectAuthentication(int userId) { 167 try { 168 mTestSession.rejectAuthentication(userId); 169 } catch (RemoteException e) { 170 throw e.rethrowFromSystemServer(); 171 } 172 } 173 174 /** 175 * Simulates an acquired message from the HAL. 176 * 177 * @param userId User that this command applies to. 178 * @param acquireInfo See 179 * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationAcquired(int)} and 180 * {@link FingerprintManager.AuthenticationCallback#onAuthenticationAcquired(int)} 181 */ 182 @RequiresPermission(TEST_BIOMETRIC) notifyAcquired(int userId, int acquireInfo)183 public void notifyAcquired(int userId, int acquireInfo) { 184 try { 185 mTestSession.notifyAcquired(userId, acquireInfo); 186 } catch (RemoteException e) { 187 throw e.rethrowFromSystemServer(); 188 } 189 } 190 191 /** 192 * Simulates an error message from the HAL. 193 * 194 * @param userId User that this command applies to. 195 * @param errorCode See 196 * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, CharSequence)} and 197 * {@link FingerprintManager.AuthenticationCallback#onAuthenticationError(int, CharSequence)} 198 */ 199 @RequiresPermission(TEST_BIOMETRIC) notifyError(int userId, int errorCode)200 public void notifyError(int userId, int errorCode) { 201 try { 202 mTestSession.notifyError(userId, errorCode); 203 } catch (RemoteException e) { 204 throw e.rethrowFromSystemServer(); 205 } 206 } 207 208 /** 209 * Matches the framework's cached enrollments against the HAL's enrollments. Any enrollment 210 * that isn't known by both sides are deleted. This should generally be used when the test 211 * HAL is disabled (e.g. to clean up after a test). 212 * 213 * @param userId User that this command applies to. 214 */ 215 @RequiresPermission(TEST_BIOMETRIC) cleanupInternalState(int userId)216 public void cleanupInternalState(int userId) { 217 try { 218 if (mUsersCleaningUp.contains(userId)) { 219 Log.w(getTag(), "Cleanup already in progress for user: " + userId); 220 } 221 222 mUsersCleaningUp.add(userId); 223 mTestSession.cleanupInternalState(userId); 224 } catch (RemoteException e) { 225 throw e.rethrowFromSystemServer(); 226 } 227 } 228 229 @Override 230 @RequiresPermission(TEST_BIOMETRIC) close()231 public void close() { 232 Log.d(getTag(), "Close, mTestedUsers size; " + mTestedUsers.size()); 233 // Cleanup can be performed using the test HAL, since it always responds to enumerate with 234 // zero enrollments. 235 if (!mTestedUsers.isEmpty()) { 236 mCloseLatch = new CountDownLatch(1); 237 for (int user : mTestedUsers) { 238 cleanupInternalState(user); 239 } 240 241 try { 242 Log.d(getTag(), "Awaiting latch..."); 243 mCloseLatch.await(3, TimeUnit.SECONDS); 244 Log.d(getTag(), "Finished awaiting"); 245 } catch (InterruptedException e) { 246 Log.e(getTag(), "Latch interrupted", e); 247 } 248 } 249 250 if (!mUsersCleaningUp.isEmpty()) { 251 // TODO(b/186600837): this seems common on multi sensor devices 252 Log.e(getTag(), "Cleanup not finished before shutdown - pending: " 253 + mUsersCleaningUp.size()); 254 } 255 256 // Disable the test HAL after the sensor becomes idle. 257 setTestHalEnabled(false); 258 } 259 getTag()260 private String getTag() { 261 return BASE_TAG + "_" + mSensorId; 262 } 263 } 264