Don't crash on illegal biometric states am: 3c4ebbbbc1

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19043499

Change-Id: I185be75529b55dc55fc4da45fd187f132b673291
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Ilya Matyukhin
2022-06-29 22:42:58 +00:00
committed by Automerger Merge Worker
2 changed files with 163 additions and 22 deletions

View File

@@ -20,14 +20,18 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.biometrics.BiometricConstants;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.function.BooleanSupplier;
/**
* Contains all the necessary information for a HAL operation.
@@ -84,6 +88,8 @@ public class BiometricSchedulerOperation {
private final BaseClientMonitor mClientMonitor;
@Nullable
private final ClientMonitorCallback mClientCallback;
@NonNull
private final BooleanSupplier mIsDebuggable;
@Nullable
private ClientMonitorCallback mOnStartCallback;
@OperationState
@@ -99,14 +105,33 @@ public class BiometricSchedulerOperation {
this(clientMonitor, callback, STATE_WAITING_IN_QUEUE);
}
@VisibleForTesting
BiometricSchedulerOperation(
@NonNull BaseClientMonitor clientMonitor,
@Nullable ClientMonitorCallback callback,
@NonNull BooleanSupplier isDebuggable
) {
this(clientMonitor, callback, STATE_WAITING_IN_QUEUE, isDebuggable);
}
protected BiometricSchedulerOperation(
@NonNull BaseClientMonitor clientMonitor,
@Nullable ClientMonitorCallback callback,
@OperationState int state
) {
this(clientMonitor, callback, state, Build::isDebuggable);
}
private BiometricSchedulerOperation(
@NonNull BaseClientMonitor clientMonitor,
@Nullable ClientMonitorCallback callback,
@OperationState int state,
@NonNull BooleanSupplier isDebuggable
) {
mClientMonitor = clientMonitor;
mClientCallback = callback;
mState = state;
mIsDebuggable = isDebuggable;
mCancelWatchdog = () -> {
if (!isFinished()) {
Slog.e(TAG, "[Watchdog Triggered]: " + this);
@@ -144,13 +169,19 @@ public class BiometricSchedulerOperation {
* @return if this operation started
*/
public boolean start(@NonNull ClientMonitorCallback callback) {
checkInState("start",
if (errorWhenNoneOf("start",
STATE_WAITING_IN_QUEUE,
STATE_WAITING_FOR_COOKIE,
STATE_WAITING_IN_QUEUE_CANCELING);
STATE_WAITING_IN_QUEUE_CANCELING)) {
return false;
}
if (mClientMonitor.getCookie() != 0) {
throw new IllegalStateException("operation requires cookie");
String err = "operation requires cookie";
if (mIsDebuggable.getAsBoolean()) {
throw new IllegalStateException(err);
}
Slog.e(TAG, err);
}
return doStart(callback);
@@ -164,16 +195,18 @@ public class BiometricSchedulerOperation {
* @return if this operation started
*/
public boolean startWithCookie(@NonNull ClientMonitorCallback callback, int cookie) {
checkInState("start",
STATE_WAITING_IN_QUEUE,
STATE_WAITING_FOR_COOKIE,
STATE_WAITING_IN_QUEUE_CANCELING);
if (mClientMonitor.getCookie() != cookie) {
Slog.e(TAG, "Mismatched cookie for operation: " + this + ", received: " + cookie);
return false;
}
if (errorWhenNoneOf("start",
STATE_WAITING_IN_QUEUE,
STATE_WAITING_FOR_COOKIE,
STATE_WAITING_IN_QUEUE_CANCELING)) {
return false;
}
return doStart(callback);
}
@@ -217,10 +250,12 @@ public class BiometricSchedulerOperation {
* immediately abort the operation and notify the client that it has finished unsuccessfully.
*/
public void abort() {
checkInState("cannot abort a non-pending operation",
if (errorWhenNoneOf("abort",
STATE_WAITING_IN_QUEUE,
STATE_WAITING_FOR_COOKIE,
STATE_WAITING_IN_QUEUE_CANCELING);
STATE_WAITING_IN_QUEUE_CANCELING)) {
return;
}
if (isHalOperation()) {
((HalClientMonitor<?>) mClientMonitor).unableToStart();
@@ -247,7 +282,9 @@ public class BiometricSchedulerOperation {
* the callback used from {@link #start(ClientMonitorCallback)} is used)
*/
public void cancel(@NonNull Handler handler, @NonNull ClientMonitorCallback callback) {
checkNotInState("cancel", STATE_FINISHED);
if (errorWhenOneOf("cancel", STATE_FINISHED)) {
return;
}
final int currentState = mState;
if (!isInterruptable()) {
@@ -402,21 +439,28 @@ public class BiometricSchedulerOperation {
return mClientMonitor;
}
private void checkNotInState(String message, @OperationState int... states) {
for (int state : states) {
if (mState == state) {
throw new IllegalStateException(message + ": illegal state= " + state);
private boolean errorWhenOneOf(String op, @OperationState int... states) {
final boolean isError = ArrayUtils.contains(states, mState);
if (isError) {
String err = op + ": mState must not be " + mState;
if (mIsDebuggable.getAsBoolean()) {
throw new IllegalStateException(err);
}
Slog.e(TAG, err);
}
return isError;
}
private void checkInState(String message, @OperationState int... states) {
for (int state : states) {
if (mState == state) {
return;
private boolean errorWhenNoneOf(String op, @OperationState int... states) {
final boolean isError = !ArrayUtils.contains(states, mState);
if (isError) {
String err = op + ": mState=" + mState + " must be one of " + Arrays.toString(states);
if (mIsDebuggable.getAsBoolean()) {
throw new IllegalStateException(err);
}
Slog.e(TAG, err);
}
throw new IllegalStateException(message + ": illegal state= " + mState);
return isError;
}
@Override

View File

@@ -80,11 +80,14 @@ public class BiometricSchedulerOperationTest {
private Handler mHandler;
private BiometricSchedulerOperation mOperation;
private boolean mIsDebuggable;
@Before
public void setUp() {
mHandler = new Handler(TestableLooper.get(this).getLooper());
mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback);
mIsDebuggable = false;
mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback,
() -> mIsDebuggable);
}
@Test
@@ -125,6 +128,34 @@ public class BiometricSchedulerOperationTest {
verify(mClientMonitor, never()).start(any());
}
@Test
public void testSecondStartWithCookieCrashesWhenDebuggable() {
final int cookie = 5;
mIsDebuggable = true;
when(mClientMonitor.getCookie()).thenReturn(cookie);
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie);
assertThat(started).isTrue();
assertThrows(IllegalStateException.class,
() -> mOperation.startWithCookie(mOnStartCallback, cookie));
}
@Test
public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() {
final int cookie = 5;
mIsDebuggable = false;
when(mClientMonitor.getCookie()).thenReturn(cookie);
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie);
assertThat(started).isTrue();
final boolean startedAgain = mOperation.startWithCookie(mOnStartCallback, cookie);
assertThat(startedAgain).isFalse();
}
@Test
public void startsWhenReadyAndHalAvailable() {
when(mClientMonitor.getCookie()).thenReturn(0);
@@ -169,8 +200,35 @@ public class BiometricSchedulerOperationTest {
verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(false));
}
@Test
public void secondStartCrashesWhenDebuggable() {
mIsDebuggable = true;
when(mClientMonitor.getCookie()).thenReturn(0);
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
final boolean started = mOperation.start(mOnStartCallback);
assertThat(started).isTrue();
assertThrows(IllegalStateException.class, () -> mOperation.start(mOnStartCallback));
}
@Test
public void secondStartFailsNicelyWhenNotDebuggable() {
mIsDebuggable = false;
when(mClientMonitor.getCookie()).thenReturn(0);
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
final boolean started = mOperation.start(mOnStartCallback);
assertThat(started).isTrue();
final boolean startedAgain = mOperation.start(mOnStartCallback);
assertThat(startedAgain).isFalse();
}
@Test
public void doesNotStartWithCookie() {
// This class only throws exceptions when debuggable.
mIsDebuggable = true;
when(mClientMonitor.getCookie()).thenReturn(9);
assertThrows(IllegalStateException.class,
() -> mOperation.start(mock(ClientMonitorCallback.class)));
@@ -178,6 +236,8 @@ public class BiometricSchedulerOperationTest {
@Test
public void cannotRestart() {
// This class only throws exceptions when debuggable.
mIsDebuggable = true;
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
mOperation.start(mOnStartCallback);
@@ -188,6 +248,8 @@ public class BiometricSchedulerOperationTest {
@Test
public void abortsNotRunning() {
// This class only throws exceptions when debuggable.
mIsDebuggable = true;
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
mOperation.abort();
@@ -200,7 +262,8 @@ public class BiometricSchedulerOperationTest {
}
@Test
public void cannotAbortRunning() {
public void abortCrashesWhenDebuggableIfOperationIsRunning() {
mIsDebuggable = true;
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
mOperation.start(mOnStartCallback);
@@ -208,6 +271,16 @@ public class BiometricSchedulerOperationTest {
assertThrows(IllegalStateException.class, () -> mOperation.abort());
}
@Test
public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() {
mIsDebuggable = false;
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
mOperation.start(mOnStartCallback);
mOperation.abort();
}
@Test
public void cancel() {
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
@@ -253,6 +326,30 @@ public class BiometricSchedulerOperationTest {
verify(mClientMonitor).destroy();
}
@Test
public void cancelCrashesWhenDebuggableIfOperationIsFinished() {
mIsDebuggable = true;
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
mOperation.abort();
assertThat(mOperation.isFinished()).isTrue();
final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
assertThrows(IllegalStateException.class, () -> mOperation.cancel(mHandler, cancelCb));
}
@Test
public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() {
mIsDebuggable = false;
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
mOperation.abort();
assertThat(mOperation.isFinished()).isTrue();
final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
mOperation.cancel(mHandler, cancelCb);
}
@Test
public void markCanceling() {
when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);