Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b0d7ee84 authored by Joe Bolinger's avatar Joe Bolinger
Browse files

Prompt for confirmation when sfps is active and the power button is pressed.

Fix: 204380676
Test: atest SideFpsEventHandlerTest
Test: manual (press power while using biometric prompt)
Test: manual (press power while enrolling fingerprint)

Change-Id: Ia9efced4034559ba5d416b6bfa24e4424c1e6a73
parent f9184167
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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) {}
}
+20 −5
Original line number Diff line number Diff line
@@ -3417,20 +3417,35 @@

    <!-- [CHAR LIMIT=40] Title of dialog shown to confirm device going to sleep if the power button
    is pressed during fingerprint enrollment. -->
    <string name="fp_enrollment_powerbutton_intent_title">Turn off screen?</string>
    <string name="fp_power_button_enrollment_title">Continue setup?</string>

    <!-- [CHAR LIMIT=NONE] Message of dialog shown to confirm device going to sleep if the power
    button is pressed during fingerprint enrollment. -->
    <string name="fp_enrollment_powerbutton_intent_message">While setting up your fingerprint, you
        pressed the Power button.\n\nThis usually turns off your screen.</string>
    <string name="fp_power_button_enrollment_message">You pressed the power button — this usually turns off the screen.\n\nTry tapping lightly while setting up your fingerprint.</string>

    <!-- [CHAR LIMIT=20] Positive button of dialog shown to confirm device going to sleep if the
    power button is pressed during fingerprint enrollment. -->
    <string name="fp_enrollment_powerbutton_intent_positive_button">Turn off</string>
    <string name="fp_power_button_enrollment_positive_button">Turn off screen</string>

    <!-- [CHAR LIMIT=20] Negative button of dialog shown to confirm device going to sleep if the
    power button is pressed during fingerprint enrollment. -->
    <string name="fp_enrollment_powerbutton_intent_negative_button">Cancel</string>
    <string name="fp_power_button_enrollment_negative_button">Continue setup</string>

    <!-- [CHAR LIMIT=40] Title of dialog shown to confirm device going to sleep if the power button
    is pressed during biometric prompt when a side fingerprint sensor is present. -->
    <string name="fp_power_button_bp_title">Continue verifying your fingerprint?</string>

    <!-- [CHAR LIMIT=NONE] Message of dialog shown to confirm device going to sleep if the power
    button is pressed during biometric prompt when a side fingerprint sensor is present. -->
    <string name="fp_power_button_bp_message">You pressed the power button — this usually turns off the screen.\n\nTry tapping lightly to verify your fingerprint.</string>

    <!-- [CHAR LIMIT=20] Positive button of dialog shown to confirm device going to sleep if the
    power button is pressed during biometric prompt when a side fingerprint sensor is present. -->
    <string name="fp_power_button_bp_positive_button">Turn off screen</string>

    <!-- [CHAR LIMIT=20] Negative button of dialog shown to confirm device going to sleep if the
    power button is pressed during biometric prompt when a side fingerprint sensor is present. -->
    <string name="fp_power_button_bp_negative_button">Continue</string>

    <!-- Notification text to tell the user that a heavy-weight application is running. -->
    <string name="heavy_weight_notification"><xliff:g id="app">%1$s</xliff:g> running</string>
+8 −4
Original line number Diff line number Diff line
@@ -1834,10 +1834,14 @@
  <java-symbol type="string" name="bugreport_status" />
  <java-symbol type="string" name="bugreport_title" />
  <java-symbol type="string" name="faceunlock_multiple_failures" />
  <java-symbol type="string" name="fp_enrollment_powerbutton_intent_title" />
  <java-symbol type="string" name="fp_enrollment_powerbutton_intent_message" />
  <java-symbol type="string" name="fp_enrollment_powerbutton_intent_positive_button" />
  <java-symbol type="string" name="fp_enrollment_powerbutton_intent_negative_button" />
  <java-symbol type="string" name="fp_power_button_bp_title" />
  <java-symbol type="string" name="fp_power_button_bp_message" />
  <java-symbol type="string" name="fp_power_button_bp_positive_button" />
  <java-symbol type="string" name="fp_power_button_bp_negative_button" />
  <java-symbol type="string" name="fp_power_button_enrollment_title" />
  <java-symbol type="string" name="fp_power_button_enrollment_message" />
  <java-symbol type="string" name="fp_power_button_enrollment_positive_button" />
  <java-symbol type="string" name="fp_power_button_enrollment_negative_button" />
  <java-symbol type="string" name="global_actions" />
  <java-symbol type="string" name="global_action_power_off" />
  <java-symbol type="string" name="global_action_power_options" />
+126 −54
Original line number Diff line number Diff line
@@ -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<AlertDialog.Builder> 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<AlertDialog.Builder> 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;
        }

        switch (mFingerprintState) {
            case STATE_ENROLLING:
            case STATE_BP_AUTH:
                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) {
                    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();
                                    mPowerManager.goToSleep(
                            powerManager.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();
                                }
                        })
                .setNegativeButton(negativeText, (dialog, which) -> dialog.dismiss())
                .setOnDismissListener(dismissListener)
                .setCancelable(false)
                .create();
        confirmScreenOffDialog.getWindow().setType(
                WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
        confirmScreenOffDialog.show();
        });
        return true;

        return confirmScreenOffDialog;
    }

    /**
@@ -116,27 +170,45 @@ 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<FingerprintSensorPropertiesInternal> sensors) {
                        mIsSideFps.set(fingerprintManager.isPowerbuttonFps());
                        FingerprintStateListener fingerprintStateListener =
                        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;
                                            }
                        };
                        fingerprintManager.registerFingerprintStateListener(
                                fingerprintStateListener);
                                        }
                                    });
                            mSideFpsEventHandlerReady.set(true);
                        }
                    }
                });
    }
}
+214 −0
Original line number Diff line number Diff line
/*
 * 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}.
 * <p/>
 * Run with <code>atest SideFpsEventHandlerTest</code>.
 */
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class SideFpsEventHandlerTest {

    private static final List<Integer> 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<IFingerprintAuthenticatorsRegisteredCallback> fpCallbackCaptor =
                ArgumentCaptor.forClass(IFingerprintAuthenticatorsRegisteredCallback.class);
        verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(fpCallbackCaptor.capture());
        if (initialized) {
            fpCallbackCaptor.getValue().onAllAuthenticatorsRegistered(
                    List.of(mock(FingerprintSensorPropertiesInternal.class)));
            if (hasSfps) {
                ArgumentCaptor<FingerprintStateListener> captor = ArgumentCaptor.forClass(
                        FingerprintStateListener.class);
                verify(mFingerprintManager).registerFingerprintStateListener(captor.capture());
                mFingerprintStateListener = captor.getValue();
            }
        }
    }
}