diff --git a/core/java/android/hardware/fingerprint/FingerprintStateListener.java b/core/java/android/hardware/fingerprint/FingerprintStateListener.java
index cf914c5acd1a1..551f5120ce2a0 100644
--- a/core/java/android/hardware/fingerprint/FingerprintStateListener.java
+++ b/core/java/android/hardware/fingerprint/FingerprintStateListener.java
@@ -49,10 +49,10 @@ public abstract class FingerprintStateListener extends IFingerprintStateListener
* Defines behavior in response to state update
* @param newState new state of fingerprint sensor
*/
- public void onStateChanged(@FingerprintStateListener.State int newState) {};
+ public void onStateChanged(@FingerprintStateListener.State int newState) {}
/**
* Invoked when enrollment state changes for the specified user
*/
- public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) {};
+ public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) {}
}
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 391c7dc3f6951..80abc48a021f1 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3417,20 +3417,35 @@
- Turn off screen?
+ Continue setup?
- While setting up your fingerprint, you
- pressed the Power button.\n\nThis usually turns off your screen.
+ You pressed the power button — this usually turns off the screen.\n\nTry tapping lightly while setting up your fingerprint.
- Turn off
+ Turn off screen
- Cancel
+ Continue setup
+
+
+ Continue verifying your fingerprint?
+
+
+ You pressed the power button — this usually turns off the screen.\n\nTry tapping lightly to verify your fingerprint.
+
+
+ Turn off screen
+
+
+ Continue
%1$s running
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 5208c4a9230db..956268b7ea54f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1834,10 +1834,14 @@
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/services/core/java/com/android/server/policy/SideFpsEventHandler.java b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
index 7c0005cf9e803..f36869859170f 100644
--- a/services/core/java/com/android/server/policy/SideFpsEventHandler.java
+++ b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
@@ -16,14 +16,19 @@
package com.android.server.policy;
+import static android.hardware.fingerprint.FingerprintStateListener.STATE_BP_AUTH;
import static android.hardware.fingerprint.FingerprintStateListener.STATE_ENROLLING;
import static android.hardware.fingerprint.FingerprintStateListener.STATE_IDLE;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.AlertDialog;
import android.app.Dialog;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
@@ -34,9 +39,11 @@ import android.os.PowerManager;
import android.view.WindowManager;
import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
/**
* Defines behavior for handling interactions between power button events and
@@ -44,68 +51,115 @@ import java.util.concurrent.atomic.AtomicBoolean;
* lives on the power button.
*/
public class SideFpsEventHandler {
+
+ private static final int DEBOUNCE_DELAY_MILLIS = 500;
+
@NonNull private final Context mContext;
@NonNull private final Handler mHandler;
@NonNull private final PowerManager mPowerManager;
- @NonNull private final AtomicBoolean mIsSideFps;
+ @NonNull private final Supplier mDialogSupplier;
@NonNull private final AtomicBoolean mSideFpsEventHandlerReady;
+ @Nullable private Dialog mDialog;
+ @NonNull private final DialogInterface.OnDismissListener mDialogDismissListener = (dialog) -> {
+ if (mDialog == dialog) {
+ mDialog = null;
+ }
+ };
+
private @FingerprintStateListener.State int mFingerprintState;
SideFpsEventHandler(Context context, Handler handler, PowerManager powerManager) {
+ this(context, handler, powerManager, () -> new AlertDialog.Builder(context));
+ }
+
+ @VisibleForTesting
+ SideFpsEventHandler(Context context, Handler handler, PowerManager powerManager,
+ Supplier dialogSupplier) {
mContext = context;
mHandler = handler;
mPowerManager = powerManager;
+ mDialogSupplier = dialogSupplier;
mFingerprintState = STATE_IDLE;
- mIsSideFps = new AtomicBoolean(false);
mSideFpsEventHandlerReady = new AtomicBoolean(false);
+
+ // ensure dialog is dismissed if screen goes off for unrelated reasons
+ context.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ mDialog = null;
+ }
+ }
+ }, new IntentFilter(Intent.ACTION_SCREEN_OFF));
}
/**
- * Called from {@link PhoneWindowManager} after power button is pressed. Checks fingerprint
- * sensor state and if mFingerprintState = STATE_ENROLLING, displays a dialog confirming intent
- * to turn screen off. If confirmed, the device goes to sleep, and if canceled, the dialog is
- * dismissed.
+ * Called from {@link PhoneWindowManager} after the power button is pressed and displays a
+ * dialog confirming the user's intent to turn screen off if a fingerprint operation is
+ * active. The device goes to sleep if confirmed otherwise the dialog is dismissed.
+ *
* @param eventTime powerPress event time
* @return true if powerPress was consumed, false otherwise
*/
public boolean onSinglePressDetected(long eventTime) {
- if (!mSideFpsEventHandlerReady.get() || !mIsSideFps.get()
- || mFingerprintState != STATE_ENROLLING) {
+ if (!mSideFpsEventHandlerReady.get()) {
return false;
}
- mHandler.post(() -> {
- Dialog confirmScreenOffDialog = new AlertDialog.Builder(mContext)
- .setTitle(R.string.fp_enrollment_powerbutton_intent_title)
- .setMessage(R.string.fp_enrollment_powerbutton_intent_message)
- .setPositiveButton(
- R.string.fp_enrollment_powerbutton_intent_positive_button,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- mPowerManager.goToSleep(
- eventTime,
- PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON,
- 0 /* flags */
- );
- }
- })
- .setNegativeButton(
- R.string.fp_enrollment_powerbutton_intent_negative_button,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- })
- .setCancelable(false)
- .create();
- confirmScreenOffDialog.getWindow().setType(
- WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
- confirmScreenOffDialog.show();
- });
- return true;
+
+ switch (mFingerprintState) {
+ case STATE_ENROLLING:
+ case STATE_BP_AUTH:
+ mHandler.post(() -> {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ mDialog = showConfirmDialog(mDialogSupplier.get(),
+ mPowerManager, eventTime, mFingerprintState, mDialogDismissListener);
+ });
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @NonNull
+ private static Dialog showConfirmDialog(@NonNull AlertDialog.Builder dialogBuilder,
+ @NonNull PowerManager powerManager, long eventTime,
+ @FingerprintStateListener.State int fingerprintState,
+ @NonNull DialogInterface.OnDismissListener dismissListener) {
+ final boolean enrolling = fingerprintState == STATE_ENROLLING;
+ final int title = enrolling ? R.string.fp_power_button_enrollment_title
+ : R.string.fp_power_button_bp_title;
+ final int message = enrolling ? R.string.fp_power_button_enrollment_message
+ : R.string.fp_power_button_bp_message;
+ final int positiveText = enrolling ? R.string.fp_power_button_enrollment_positive_button
+ : R.string.fp_power_button_bp_positive_button;
+ final int negativeText = enrolling ? R.string.fp_power_button_enrollment_negative_button
+ : R.string.fp_power_button_bp_negative_button;
+
+ final Dialog confirmScreenOffDialog = dialogBuilder
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(positiveText,
+ (dialog, which) -> {
+ dialog.dismiss();
+ powerManager.goToSleep(
+ eventTime,
+ PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON,
+ 0 /* flags */
+ );
+ })
+ .setNegativeButton(negativeText, (dialog, which) -> dialog.dismiss())
+ .setOnDismissListener(dismissListener)
+ .setCancelable(false)
+ .create();
+ confirmScreenOffDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+ confirmScreenOffDialog.show();
+
+ return confirmScreenOffDialog;
}
/**
@@ -116,26 +170,44 @@ public class SideFpsEventHandler {
*/
public void onFingerprintSensorReady() {
final PackageManager pm = mContext.getPackageManager();
- if (!pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) return;
- FingerprintManager fingerprintManager =
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
+ return;
+ }
+
+ final FingerprintManager fingerprintManager =
mContext.getSystemService(FingerprintManager.class);
fingerprintManager.addAuthenticatorsRegisteredCallback(
new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@Override
public void onAllAuthenticatorsRegistered(
List sensors) {
- mIsSideFps.set(fingerprintManager.isPowerbuttonFps());
- FingerprintStateListener fingerprintStateListener =
- new FingerprintStateListener() {
- @Override
- public void onStateChanged(
- @FingerprintStateListener.State int newState) {
- mFingerprintState = newState;
- }
- };
- fingerprintManager.registerFingerprintStateListener(
- fingerprintStateListener);
- mSideFpsEventHandlerReady.set(true);
+ if (fingerprintManager.isPowerbuttonFps()) {
+ fingerprintManager.registerFingerprintStateListener(
+ new FingerprintStateListener() {
+ @Nullable private Runnable mStateRunnable = null;
+
+ @Override
+ public void onStateChanged(
+ @FingerprintStateListener.State int newState) {
+ if (mStateRunnable != null) {
+ mHandler.removeCallbacks(mStateRunnable);
+ mStateRunnable = null;
+ }
+
+ // When the user hits the power button the events can
+ // arrive in any order (success auth & power). Add a
+ // damper when moving to idle in case auth is first
+ if (newState == STATE_IDLE) {
+ mStateRunnable = () -> mFingerprintState = newState;
+ mHandler.postDelayed(mStateRunnable,
+ DEBOUNCE_DELAY_MILLIS);
+ } else {
+ mFingerprintState = newState;
+ }
+ }
+ });
+ mSideFpsEventHandlerReady.set(true);
+ }
}
});
}
diff --git a/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java b/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java
new file mode 100644
index 0000000000000..41c7e31147a54
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlertDialog;
+import android.content.pm.PackageManager;
+import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.FingerprintStateListener;
+import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.test.TestLooper;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.view.Window;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.util.List;
+
+/**
+ * Unit tests for {@link SideFpsEventHandler}.
+ *
+ * Run with atest SideFpsEventHandlerTest.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class SideFpsEventHandlerTest {
+
+ private static final List sAllStates = List.of(
+ FingerprintStateListener.STATE_IDLE,
+ FingerprintStateListener.STATE_ENROLLING,
+ FingerprintStateListener.STATE_KEYGUARD_AUTH,
+ FingerprintStateListener.STATE_BP_AUTH,
+ FingerprintStateListener.STATE_AUTH_OTHER);
+
+ @Rule
+ public TestableContext mContext =
+ new TestableContext(InstrumentationRegistry.getContext(), null);
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private FingerprintManager mFingerprintManager;
+ @Spy
+ private AlertDialog.Builder mDialogBuilder = new AlertDialog.Builder(mContext);
+ @Mock
+ private AlertDialog mAlertDialog;
+ @Mock
+ private Window mWindow;
+
+ private TestLooper mLooper = new TestLooper();
+ private SideFpsEventHandler mEventHandler;
+ private FingerprintStateListener mFingerprintStateListener;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext.addMockSystemService(PackageManager.class, mPackageManager);
+ mContext.addMockSystemService(FingerprintManager.class, mFingerprintManager);
+
+ when(mDialogBuilder.create()).thenReturn(mAlertDialog);
+ when(mAlertDialog.getWindow()).thenReturn(mWindow);
+
+ mEventHandler = new SideFpsEventHandler(
+ mContext, new Handler(mLooper.getLooper()),
+ mContext.getSystemService(PowerManager.class), () -> mDialogBuilder);
+ }
+
+ @Test
+ public void ignoresWithoutFingerprintFeature() {
+ when(mPackageManager.hasSystemFeature(eq(PackageManager.FEATURE_FINGERPRINT)))
+ .thenReturn(false);
+
+ assertThat(mEventHandler.onSinglePressDetected(60L)).isFalse();
+
+ mLooper.dispatchAll();
+ verify(mAlertDialog, never()).show();
+ }
+
+ @Test
+ public void ignoresWithoutSfps() throws Exception {
+ setupWithSensor(false /* hasSfps */, true /* initialized */);
+
+ for (int state : sAllStates) {
+ setFingerprintState(state);
+ assertThat(mEventHandler.onSinglePressDetected(200L)).isFalse();
+
+ mLooper.dispatchAll();
+ verify(mAlertDialog, never()).show();
+ }
+ }
+
+ @Test
+ public void ignoresWhileWaitingForSfps() throws Exception {
+ setupWithSensor(true /* hasSfps */, false /* initialized */);
+
+ for (int state : sAllStates) {
+ setFingerprintState(state);
+ assertThat(mEventHandler.onSinglePressDetected(400L)).isFalse();
+
+ mLooper.dispatchAll();
+ verify(mAlertDialog, never()).show();
+ }
+ }
+
+ @Test
+ public void ignoresWhenIdleOrUnknown() throws Exception {
+ setupWithSensor(true /* hasSfps */, true /* initialized */);
+
+ setFingerprintState(FingerprintStateListener.STATE_IDLE);
+ assertThat(mEventHandler.onSinglePressDetected(80000L)).isFalse();
+
+ setFingerprintState(FingerprintStateListener.STATE_AUTH_OTHER);
+ assertThat(mEventHandler.onSinglePressDetected(90000L)).isFalse();
+
+ mLooper.dispatchAll();
+ verify(mAlertDialog, never()).show();
+ }
+
+ @Test
+ public void ignoresOnKeyguard() throws Exception {
+ setupWithSensor(true /* hasSfps */, true /* initialized */);
+
+ setFingerprintState(FingerprintStateListener.STATE_KEYGUARD_AUTH);
+ assertThat(mEventHandler.onSinglePressDetected(80000L)).isFalse();
+
+ mLooper.dispatchAll();
+ verify(mAlertDialog, never()).show();
+ }
+
+ @Test
+ public void promptsWhenBPisActive() throws Exception {
+ setupWithSensor(true /* hasSfps */, true /* initialized */);
+
+ setFingerprintState(FingerprintStateListener.STATE_BP_AUTH);
+ assertThat(mEventHandler.onSinglePressDetected(80000L)).isTrue();
+
+ mLooper.dispatchAll();
+ verify(mAlertDialog).show();
+ }
+
+ @Test
+ public void promptsWhenEnrolling() throws Exception {
+ setupWithSensor(true /* hasSfps */, true /* initialized */);
+
+ setFingerprintState(FingerprintStateListener.STATE_ENROLLING);
+ assertThat(mEventHandler.onSinglePressDetected(80000L)).isTrue();
+
+ mLooper.dispatchAll();
+ verify(mAlertDialog).show();
+ }
+
+ private void setFingerprintState(@FingerprintStateListener.State int newState) {
+ if (mFingerprintStateListener != null) {
+ mFingerprintStateListener.onStateChanged(newState);
+ mLooper.dispatchAll();
+ }
+ }
+
+ private void setupWithSensor(boolean hasSfps, boolean initialized) throws Exception {
+ when(mPackageManager.hasSystemFeature(eq(PackageManager.FEATURE_FINGERPRINT)))
+ .thenReturn(true);
+ when(mFingerprintManager.isPowerbuttonFps()).thenReturn(hasSfps);
+ mEventHandler.onFingerprintSensorReady();
+
+ ArgumentCaptor fpCallbackCaptor =
+ ArgumentCaptor.forClass(IFingerprintAuthenticatorsRegisteredCallback.class);
+ verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(fpCallbackCaptor.capture());
+ if (initialized) {
+ fpCallbackCaptor.getValue().onAllAuthenticatorsRegistered(
+ List.of(mock(FingerprintSensorPropertiesInternal.class)));
+ if (hasSfps) {
+ ArgumentCaptor captor = ArgumentCaptor.forClass(
+ FingerprintStateListener.class);
+ verify(mFingerprintManager).registerFingerprintStateListener(captor.capture());
+ mFingerprintStateListener = captor.getValue();
+ }
+ }
+ }
+}