Hide Udfps Overlay together with system transitions

Prevent the UDFPS overlay from showing when system dialogs are
expected to be closed. Since the UDFPS affordances are not
actually shown in the application's window, we should nicely
hide the UDFPS overlay together with the system transitions.

1) Adds IUdfpsOverlayControllerCallback for SysUI to notify
   FingerprintService of per-session events.
2) Adds AcquisitionClient#onUserCanceled, which notifies the
   caller of ERROR_USER_CANCELED, and then cancels the HAL
   request

Bug: 178403701
Test: Enroll, swipe up. UDFPS affordances animate away together
      with the activity. BiometricScheduler logs also indicate
      enrollment canceled.
Test: atest AcquisitionClientTest
Test: atest UdfpsControllerTest

Change-Id: I0b2c9eb20ef0686e426ea12896769e4df077cadb
This commit is contained in:
Kevin Chyn
2021-03-05 11:30:07 -08:00
parent 58fd45573d
commit 4c4961ab26
13 changed files with 226 additions and 20 deletions

View File

@@ -15,6 +15,8 @@
*/
package android.hardware.fingerprint;
import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
/**
* Interface for interacting with the under-display fingerprint sensor (UDFPS) overlay.
* @hide
@@ -28,7 +30,7 @@ oneway interface IUdfpsOverlayController {
const int REASON_AUTH_FPM_OTHER = 5; // Other FingerprintManager usage
// Shows the overlay.
void showUdfpsOverlay(int sensorId, int reason);
void showUdfpsOverlay(int sensorId, int reason, IUdfpsOverlayControllerCallback callback);
// Hides the overlay.
void hideUdfpsOverlay(int sensorId);

View File

@@ -0,0 +1,24 @@
/*
* 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 android.hardware.fingerprint;
/**
* @hide
*/
oneway interface IUdfpsOverlayControllerCallback {
// Notify system_server if the user cancels a UDFPS-related operation (enroll, auth)
void onUserCanceled();
}

View File

@@ -20,7 +20,10 @@ import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkNotNull;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Point;
@@ -29,6 +32,8 @@ import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.IUdfpsOverlayController;
import android.os.SystemClock;
import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
import android.os.RemoteException;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -110,10 +115,13 @@ public class UdfpsController implements DozeReceiver, HbmCallback {
private static class ServerRequest {
// Reason the overlay has been requested. See IUdfpsOverlayController for definitions.
final int mRequestReason;
@NonNull final IUdfpsOverlayControllerCallback mCallback;
@Nullable final UdfpsEnrollHelper mEnrollHelper;
ServerRequest(int requestReason, @Nullable UdfpsEnrollHelper enrollHelper) {
ServerRequest(int requestReason, @NonNull IUdfpsOverlayControllerCallback callback,
@Nullable UdfpsEnrollHelper enrollHelper) {
mRequestReason = requestReason;
mCallback = callback;
mEnrollHelper = enrollHelper;
}
@@ -128,11 +136,20 @@ public class UdfpsController implements DozeReceiver, HbmCallback {
mEnrollHelper.onEnrollmentHelp();
}
}
void onUserCanceled() {
try {
mCallback.onUserCanceled();
} catch (RemoteException e) {
Log.e(TAG, "Remote exception", e);
}
}
}
public class UdfpsOverlayController extends IUdfpsOverlayController.Stub {
@Override
public void showUdfpsOverlay(int sensorId, int reason) {
public void showUdfpsOverlay(int sensorId, int reason,
@NonNull IUdfpsOverlayControllerCallback callback) {
final UdfpsEnrollHelper enrollHelper;
if (reason == IUdfpsOverlayController.REASON_ENROLL_FIND_SENSOR
|| reason == IUdfpsOverlayController.REASON_ENROLL_ENROLLING) {
@@ -141,7 +158,7 @@ public class UdfpsController implements DozeReceiver, HbmCallback {
enrollHelper = null;
}
mServerRequest = new ServerRequest(reason, enrollHelper);
mServerRequest = new ServerRequest(reason, callback, enrollHelper);
updateOverlay();
}
@@ -195,6 +212,19 @@ public class UdfpsController implements DozeReceiver, HbmCallback {
return (float) Math.sqrt(Math.pow(vx, 2.0) + Math.pow(vy, 2.0));
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (mServerRequest != null
&& Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
Log.d(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received");
mServerRequest.onUserCanceled();
mServerRequest = null;
updateOverlay();
}
}
};
@SuppressLint("ClickableViewAccessibility")
private final UdfpsView.OnTouchListener mOnTouchListener = (view, event) -> {
UdfpsView udfpsView = (UdfpsView) view;
@@ -310,6 +340,10 @@ public class UdfpsController implements DozeReceiver, HbmCallback {
mCoreLayoutParams.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
mFingerprintManager.setUdfpsOverlayController(new UdfpsOverlayController());
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
context.registerReceiver(mBroadcastReceiver, filter);
}
@Nullable

View File

@@ -31,6 +31,7 @@ import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.IUdfpsOverlayController;
import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
import android.os.PowerManager;
import android.os.RemoteException;
import android.testing.AndroidTestingRunner;
@@ -89,6 +90,8 @@ public class UdfpsControllerTest extends SysuiTestCase {
private StatusBarStateController mStatusBarStateController;
@Mock
private StatusBar mStatusBar;
@Mock
private IUdfpsOverlayControllerCallback mUdfpsOverlayControllerCallback;
private FakeExecutor mFgExecutor;
@@ -152,7 +155,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
@Test
public void dozeTimeTick() throws RemoteException {
mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD);
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
mFgExecutor.runAllReady();
mUdfpsController.dozeTimeTick();
verify(mUdfpsView).dozeTimeTick();
@@ -161,7 +164,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
@Test
public void showUdfpsOverlay_addsViewToWindow() throws RemoteException {
mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD);
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
mFgExecutor.runAllReady();
verify(mWindowManager).addView(eq(mUdfpsView), any());
}
@@ -169,7 +172,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
@Test
public void hideUdfpsOverlay_removesViewFromWindow() throws RemoteException {
mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD);
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
mOverlayController.hideUdfpsOverlay(TEST_UDFPS_SENSOR_ID);
mFgExecutor.runAllReady();
verify(mWindowManager).removeView(eq(mUdfpsView));
@@ -183,7 +186,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
// GIVEN that the overlay is showing
mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD);
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
mFgExecutor.runAllReady();
// WHEN ACTION_DOWN is received
verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
@@ -205,7 +208,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
public void aodInterrupt() throws RemoteException {
// GIVEN that the overlay is showing
mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD);
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
mFgExecutor.runAllReady();
// WHEN fingerprint is requested because of AOD interrupt
mUdfpsController.onAodInterrupt(0, 0, 2f, 3f);
@@ -221,7 +224,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
public void cancelAodInterrupt() throws RemoteException {
// GIVEN AOD interrupt
mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD);
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
mFgExecutor.runAllReady();
mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
// WHEN it is cancelled
@@ -234,7 +237,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
public void aodInterruptTimeout() throws RemoteException {
// GIVEN AOD interrupt
mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD);
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
mFgExecutor.runAllReady();
mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
// WHEN it times out
@@ -247,7 +250,7 @@ public class UdfpsControllerTest extends SysuiTestCase {
@Test
public void registersAndUnregistersViewForCallbacks() throws RemoteException {
mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD);
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback);
mFgExecutor.runAllReady();
verify(mStatusBarStateController).addCallback(mUdfpsController.mStatusBarStateListener);
verify(mStatusBar).addExpansionChangedListener(

View File

@@ -80,6 +80,18 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement
onErrorInternal(errorCode, vendorCode, true /* finish */);
}
/**
* Notifies the caller that the operation was canceled by the user. Note that the actual
* operation still needs to wait for the HAL to send ERROR_CANCELED.
*/
public void onUserCanceled() {
// Send USER_CANCELED, but do not finish. Wait for the HAL to respond with ERROR_CANCELED,
// which then finishes the AcquisitionClient's lifecycle.
onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED, 0 /* vendorCode */,
false /* finish */);
stopHalOperation();
}
protected void onErrorInternal(int errorCode, int vendorCode, boolean finish) {
// In some cases, the framework will send an error to the caller before a true terminal
// case (success, failure, or error) is received from the HAL (e.g. versions of fingerprint

View File

@@ -22,9 +22,12 @@ import android.content.Context;
import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.IUdfpsOverlayController;
import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
import android.os.RemoteException;
import android.util.Slog;
import com.android.server.biometrics.sensors.AcquisitionClient;
/**
* Contains helper methods for under-display fingerprint HIDL.
*/
@@ -77,12 +80,22 @@ public class UdfpsHelper {
}
public static void showUdfpsOverlay(int sensorId, int reason,
@Nullable IUdfpsOverlayController udfpsOverlayController) {
@Nullable IUdfpsOverlayController udfpsOverlayController,
@NonNull AcquisitionClient<?> client) {
if (udfpsOverlayController == null) {
return;
}
final IUdfpsOverlayControllerCallback callback =
new IUdfpsOverlayControllerCallback.Stub() {
@Override
public void onUserCanceled() {
client.onUserCanceled();
}
};
try {
udfpsOverlayController.showUdfpsOverlay(sensorId, reason);
udfpsOverlayController.showUdfpsOverlay(sensorId, reason, callback);
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception when showing the UDFPS overlay", e);
}

View File

@@ -89,7 +89,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient<ISession> imp
@Override
protected void startHalOperation() {
UdfpsHelper.showUdfpsOverlay(getSensorId(), Utils.getUdfpsAuthReason(this),
mUdfpsOverlayController);
mUdfpsOverlayController, this);
try {
mCancellationSignal = getFreshDaemon().authenticate(mSequentialId, mOperationId);
} catch (RemoteException e) {

View File

@@ -72,7 +72,7 @@ class FingerprintDetectClient extends AcquisitionClient<ISession> {
protected void startHalOperation() {
UdfpsHelper.showUdfpsOverlay(getSensorId(),
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD,
mUdfpsOverlayController);
mUdfpsOverlayController, this);
try {
mCancellationSignal = getFreshDaemon().detectInteraction(mSequentialId);
} catch (RemoteException e) {

View File

@@ -121,7 +121,7 @@ class FingerprintEnrollClient extends EnrollClient<ISession> implements Udfps {
protected void startHalOperation() {
UdfpsHelper.showUdfpsOverlay(getSensorId(),
UdfpsHelper.getReasonFromEnrollReason(mEnrollReason),
mUdfpsOverlayController);
mUdfpsOverlayController, this);
try {
mCancellationSignal = getFreshDaemon().enroll(mSequentialId,
HardwareAuthTokenUtils.toHardwareAuthToken(mHardwareAuthToken));

View File

@@ -120,7 +120,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi
@Override
protected void startHalOperation() {
UdfpsHelper.showUdfpsOverlay(getSensorId(), Utils.getUdfpsAuthReason(this),
mUdfpsOverlayController);
mUdfpsOverlayController, this);
try {
// GroupId was never used. In fact, groupId is always the same as userId.
getFreshDaemon().authenticate(mOperationId, getTargetUserId());

View File

@@ -85,7 +85,7 @@ class FingerprintDetectClient extends AcquisitionClient<IBiometricsFingerprint>
protected void startHalOperation() {
UdfpsHelper.showUdfpsOverlay(getSensorId(),
IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD,
mUdfpsOverlayController);
mUdfpsOverlayController, this);
try {
getFreshDaemon().authenticate(0 /* operationId */, getTargetUserId());
} catch (RemoteException e) {

View File

@@ -84,7 +84,7 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint
protected void startHalOperation() {
UdfpsHelper.showUdfpsOverlay(getSensorId(),
UdfpsHelper.getReasonFromEnrollReason(mEnrollReason),
mUdfpsOverlayController);
mUdfpsOverlayController, this);
try {
// GroupId was never used. In fact, groupId is always the same as userId.
getFreshDaemon().enroll(mHardwareAuthToken, getTargetUserId(), mTimeoutSec);

View File

@@ -0,0 +1,118 @@
/*
* 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.biometrics.sensors;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.Context;
import android.hardware.biometrics.BiometricConstants;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@Presubmit
@SmallTest
public class AcquisitionClientTest {
private static final int TEST_SENSOR_ID = 100;
@Mock
private Context mContext;
@Mock
private IBinder mToken;
@Mock
private ClientMonitorCallbackConverter mClientCallback;
@Mock
private BaseClientMonitor.Callback mSchedulerCallback;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testUserCanceled() throws Exception {
// Start an AcquisitionClient
final TestAcquisitionClient client = new TestAcquisitionClient(mContext, Object::new,
mToken, mClientCallback);
client.start(mSchedulerCallback);
assertTrue(client.mHalOperationRunning);
verify(mSchedulerCallback).onClientStarted(eq(client));
// Pretend that it got canceled by the user.
client.onUserCanceled();
verify(mSchedulerCallback, never()).onClientFinished(any(), anyBoolean());
verify(mClientCallback).onError(eq(TEST_SENSOR_ID),
anyInt(),
eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED),
eq(0) /* vendorCode */);
assertFalse(client.mHalOperationRunning);
// Pretend that the HAL responded with ERROR_CANCELED
client.onError(BiometricConstants.BIOMETRIC_ERROR_CANCELED, 0 /* vendorCode */);
verifyNoMoreInteractions(mClientCallback);
verify(mSchedulerCallback).onClientFinished(eq(client), anyBoolean());
}
private static class TestAcquisitionClient extends AcquisitionClient<Object> {
boolean mHalOperationRunning;
public TestAcquisitionClient(@NonNull Context context,
@NonNull LazyDaemon<Object> lazyDaemon, @NonNull IBinder token,
@NonNull ClientMonitorCallbackConverter callback) {
super(context, lazyDaemon, token, callback, 0 /* userId */, "Test", 0 /* cookie */,
TEST_SENSOR_ID /* sensorId */, 0 /* statsModality */, 0 /* statsAction */,
0 /* statsClient */);
}
@Override
public void start(@NonNull Callback callback) {
super.start(callback);
startHalOperation();
}
@Override
protected void stopHalOperation() {
mHalOperationRunning = false;
}
@Override
protected void startHalOperation() {
mHalOperationRunning = true;
}
@Override
public int getProtoEnum() {
return 0;
}
}
}