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 f6a0e615e247c..688bced8b14ce 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3505,20 +3505,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 0495122eaee79..76e977468cff1 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1840,10 +1840,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(); + } + } + } +}