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

Commit beedf62a authored by Joe Bolinger's avatar Joe Bolinger Committed by Android (Google) Code Review
Browse files

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

parents efdc32ca b0d7ee84
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
@@ -3505,20 +3505,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
@@ -1840,10 +1840,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();
            }
        }
    }
}