1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.M; 4 import static android.os.Build.VERSION_CODES.N_MR1; 5 import static android.os.Build.VERSION_CODES.P; 6 7 import android.hardware.fingerprint.Fingerprint; 8 import android.hardware.fingerprint.FingerprintManager; 9 import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback; 10 import android.hardware.fingerprint.FingerprintManager.AuthenticationResult; 11 import android.hardware.fingerprint.FingerprintManager.CryptoObject; 12 import android.os.CancellationSignal; 13 import android.os.Handler; 14 import android.util.Log; 15 import java.util.ArrayList; 16 import java.util.Arrays; 17 import java.util.Collections; 18 import java.util.List; 19 import java.util.stream.IntStream; 20 import org.robolectric.RuntimeEnvironment; 21 import org.robolectric.annotation.HiddenApi; 22 import org.robolectric.annotation.Implementation; 23 import org.robolectric.annotation.Implements; 24 import org.robolectric.util.ReflectionHelpers; 25 import org.robolectric.util.ReflectionHelpers.ClassParameter; 26 27 /** Provides testing APIs for {@link FingerprintManager} */ 28 @SuppressWarnings("NewApi") 29 @Implements(FingerprintManager.class) 30 public class ShadowFingerprintManager { 31 32 private static final String TAG = "ShadowFingerprintManager"; 33 34 private boolean isHardwareDetected; 35 private CryptoObject pendingCryptoObject; 36 private AuthenticationCallback pendingCallback; 37 private List<Fingerprint> fingerprints = Collections.emptyList(); 38 39 /** 40 * Simulates a successful fingerprint authentication. An authentication request must have been 41 * issued with {@link FingerprintManager#authenticate(CryptoObject, CancellationSignal, int, AuthenticationCallback, Handler)} and not cancelled. 42 */ 43 public void authenticationSucceeds() { 44 if (pendingCallback == null) { 45 throw new IllegalStateException("No active fingerprint authentication request."); 46 } 47 48 AuthenticationResult result; 49 if (RuntimeEnvironment.getApiLevel() >= N_MR1) { 50 result = new AuthenticationResult(pendingCryptoObject, null, 0); 51 } else { 52 result = ReflectionHelpers.callConstructor(AuthenticationResult.class, 53 ClassParameter.from(CryptoObject.class, pendingCryptoObject), 54 ClassParameter.from(Fingerprint.class, null)); 55 } 56 57 pendingCallback.onAuthenticationSucceeded(result); 58 } 59 60 /** 61 * Simulates a failed fingerprint authentication. An authentication request must have been 62 * issued with {@link FingerprintManager#authenticate(CryptoObject, CancellationSignal, int, AuthenticationCallback, Handler)} and not cancelled. 63 */ 64 public void authenticationFails() { 65 if (pendingCallback == null) { 66 throw new IllegalStateException("No active fingerprint authentication request."); 67 } 68 69 pendingCallback.onAuthenticationFailed(); 70 } 71 72 /** 73 * Success or failure can be simulated with a subsequent call to {@link #authenticationSucceeds()} 74 * or {@link #authenticationFails()}. 75 */ 76 @Implementation(minSdk = M) 77 protected void authenticate( 78 CryptoObject crypto, 79 CancellationSignal cancel, 80 int flags, 81 AuthenticationCallback callback, 82 Handler handler) { 83 if (callback == null) { 84 throw new IllegalArgumentException("Must supply an authentication callback"); 85 } 86 87 if (cancel != null) { 88 if (cancel.isCanceled()) { 89 Log.w(TAG, "authentication already canceled"); 90 return; 91 } else { 92 cancel.setOnCancelListener(() -> { 93 this.pendingCallback = null; 94 this.pendingCryptoObject = null; 95 }); 96 } 97 } 98 99 this.pendingCryptoObject = crypto; 100 this.pendingCallback = callback; 101 } 102 103 /** 104 * Sets the return value of {@link FingerprintManager#hasEnrolledFingerprints()}. 105 * 106 * @deprecated use {@link #setDefaultFingerprints} instead. 107 */ 108 @Deprecated 109 public void setHasEnrolledFingerprints(boolean hasEnrolledFingerprints) { 110 setDefaultFingerprints(hasEnrolledFingerprints ? 1 : 0); 111 } 112 113 /** 114 * Returns {@code false} by default, or the value specified via 115 * {@link #setHasEnrolledFingerprints(boolean)}. 116 */ 117 @Implementation(minSdk = M) 118 protected boolean hasEnrolledFingerprints() { 119 return !fingerprints.isEmpty(); 120 } 121 122 /** 123 * @return lists of current fingerprint items, the list be set via {@link #setDefaultFingerprints} 124 */ 125 @HiddenApi 126 @Implementation(minSdk = M) 127 protected List<Fingerprint> getEnrolledFingerprints() { 128 return new ArrayList<>(fingerprints); 129 } 130 131 /** 132 * @return Returns the finger ID for the given index. 133 */ 134 public int getFingerprintId(int index) { 135 return ReflectionHelpers.callInstanceMethod( 136 getEnrolledFingerprints().get(index), 137 RuntimeEnvironment.getApiLevel() > P ? "getBiometricId" : "getFingerId"); 138 } 139 140 /** 141 * Enrolls the given number of fingerprints, which will be returned in {@link 142 * #getEnrolledFingerprints}. 143 * 144 * @param num the quantity of fingerprint item. 145 */ 146 public void setDefaultFingerprints(int num) { 147 setEnrolledFingerprints( 148 IntStream.range(0, num) 149 .mapToObj( 150 i -> 151 new Fingerprint( 152 /* name= */ "Fingerprint " + i, 153 /* groupId= */ 0, 154 /* fingerId= */ i, 155 /* deviceId= */ 0)) 156 .toArray(Fingerprint[]::new)); 157 } 158 159 private void setEnrolledFingerprints(Fingerprint... fingerprints) { 160 this.fingerprints = Arrays.asList(fingerprints); 161 } 162 163 /** 164 * Sets the return value of {@link FingerprintManager#isHardwareDetected()}. 165 */ 166 public void setIsHardwareDetected(boolean isHardwareDetected) { 167 this.isHardwareDetected = isHardwareDetected; 168 } 169 170 /** 171 * @return `false` by default, or the value specified via {@link #setIsHardwareDetected(boolean)} 172 */ 173 @Implementation(minSdk = M) 174 protected boolean isHardwareDetected() { 175 return this.isHardwareDetected; 176 } 177 } 178