Home | History | Annotate | Download | only in shadows
      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