| /* |
| * Copyright (C) 2020 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.systemui.biometrics; |
| |
| import static junit.framework.Assert.assertEquals; |
| |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyFloat; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| import android.content.res.TypedArray; |
| import android.hardware.biometrics.ComponentInfoInternal; |
| import android.hardware.biometrics.SensorProperties; |
| 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.os.Vibrator; |
| import android.testing.AndroidTestingRunner; |
| import android.testing.TestableLooper.RunWithLooper; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.WindowManager; |
| import android.view.accessibility.AccessibilityManager; |
| |
| import androidx.test.filters.SmallTest; |
| |
| import com.android.keyguard.KeyguardUpdateMonitor; |
| import com.android.systemui.R; |
| import com.android.systemui.SysuiTestCase; |
| import com.android.systemui.dump.DumpManager; |
| import com.android.systemui.keyguard.KeyguardViewMediator; |
| import com.android.systemui.keyguard.ScreenLifecycle; |
| import com.android.systemui.plugins.FalsingManager; |
| import com.android.systemui.plugins.statusbar.StatusBarStateController; |
| import com.android.systemui.statusbar.phone.StatusBar; |
| import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; |
| import com.android.systemui.util.concurrency.FakeExecutor; |
| import com.android.systemui.util.time.FakeSystemClock; |
| |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Captor; |
| import org.mockito.Mock; |
| import org.mockito.junit.MockitoJUnit; |
| import org.mockito.junit.MockitoRule; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| @SmallTest |
| @RunWith(AndroidTestingRunner.class) |
| @RunWithLooper |
| public class UdfpsControllerTest extends SysuiTestCase { |
| |
| // Use this for inputs going into SystemUI. Use UdfpsController.mUdfpsSensorId for things |
| // leaving SystemUI. |
| private static final int TEST_UDFPS_SENSOR_ID = 1; |
| |
| @Rule |
| public MockitoRule rule = MockitoJUnit.rule(); |
| |
| // Unit under test |
| private UdfpsController mUdfpsController; |
| |
| // Dependencies |
| @Mock |
| private LayoutInflater mLayoutInflater; |
| @Mock |
| private FingerprintManager mFingerprintManager; |
| @Mock |
| private WindowManager mWindowManager; |
| @Mock |
| private StatusBarStateController mStatusBarStateController; |
| @Mock |
| private StatusBar mStatusBar; |
| @Mock |
| private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; |
| @Mock |
| private DumpManager mDumpManager; |
| @Mock |
| private KeyguardUpdateMonitor mKeyguardUpdateMonitor; |
| @Mock |
| private KeyguardViewMediator mKeyguardViewMediator; |
| @Mock |
| private IUdfpsOverlayControllerCallback mUdfpsOverlayControllerCallback; |
| @Mock |
| private FalsingManager mFalsingManager; |
| @Mock |
| private PowerManager mPowerManager; |
| @Mock |
| private AccessibilityManager mAccessibilityManager; |
| @Mock |
| private ScreenLifecycle mScreenLifecycle; |
| @Mock |
| private Vibrator mVibrator; |
| |
| private FakeExecutor mFgExecutor; |
| |
| // Stuff for configuring mocks |
| @Mock |
| private UdfpsView mUdfpsView; |
| @Mock |
| private TypedArray mBrightnessValues; |
| @Mock |
| private TypedArray mBrightnessBacklight; |
| |
| // Capture listeners so that they can be used to send events |
| @Captor private ArgumentCaptor<IUdfpsOverlayController> mOverlayCaptor; |
| private IUdfpsOverlayController mOverlayController; |
| @Captor private ArgumentCaptor<UdfpsView.OnTouchListener> mTouchListenerCaptor; |
| @Captor private ArgumentCaptor<Runnable> mOnIlluminatedRunnableCaptor; |
| @Captor private ArgumentCaptor<ScreenLifecycle.Observer> mScreenObserverCaptor; |
| private ScreenLifecycle.Observer mScreenObserver; |
| |
| @Before |
| public void setUp() { |
| setUpResources(); |
| when(mLayoutInflater.inflate(R.layout.udfps_view, null, false)).thenReturn(mUdfpsView); |
| final List<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); |
| |
| final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); |
| componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, |
| "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, |
| "00000001" /* serialNumber */, "" /* softwareVersion */)); |
| componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, |
| "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, |
| "vendor/version/revision" /* softwareVersion */)); |
| |
| props.add(new FingerprintSensorPropertiesInternal(TEST_UDFPS_SENSOR_ID, |
| SensorProperties.STRENGTH_STRONG, |
| 5 /* maxEnrollmentsPerUser */, |
| componentInfo, |
| FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, |
| true /* resetLockoutRequiresHardwareAuthToken */)); |
| when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(props); |
| mFgExecutor = new FakeExecutor(new FakeSystemClock()); |
| mUdfpsController = new UdfpsController( |
| mContext, |
| mLayoutInflater, |
| mFingerprintManager, |
| mWindowManager, |
| mStatusBarStateController, |
| mFgExecutor, |
| mStatusBar, |
| mStatusBarKeyguardViewManager, |
| mDumpManager, |
| mKeyguardUpdateMonitor, |
| mKeyguardViewMediator, |
| mFalsingManager, |
| mPowerManager, |
| mAccessibilityManager, |
| mScreenLifecycle, |
| mVibrator); |
| verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); |
| mOverlayController = mOverlayCaptor.getValue(); |
| verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); |
| mScreenObserver = mScreenObserverCaptor.getValue(); |
| |
| assertEquals(TEST_UDFPS_SENSOR_ID, mUdfpsController.mSensorProps.sensorId); |
| } |
| |
| private void setUpResources() { |
| when(mBrightnessValues.length()).thenReturn(2); |
| when(mBrightnessValues.getFloat(0, PowerManager.BRIGHTNESS_OFF_FLOAT)).thenReturn(1f); |
| when(mBrightnessValues.getFloat(1, PowerManager.BRIGHTNESS_OFF_FLOAT)).thenReturn(2f); |
| when(mBrightnessBacklight.length()).thenReturn(2); |
| when(mBrightnessBacklight.getFloat(0, PowerManager.BRIGHTNESS_OFF_FLOAT)).thenReturn(1f); |
| when(mBrightnessBacklight.getFloat(1, PowerManager.BRIGHTNESS_OFF_FLOAT)).thenReturn(2f); |
| } |
| |
| @Test |
| public void dozeTimeTick() throws RemoteException { |
| mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, |
| IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback); |
| mFgExecutor.runAllReady(); |
| mUdfpsController.dozeTimeTick(); |
| verify(mUdfpsView).dozeTimeTick(); |
| } |
| |
| @Test |
| public void showUdfpsOverlay_addsViewToWindow() throws RemoteException { |
| mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, |
| IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback); |
| mFgExecutor.runAllReady(); |
| verify(mWindowManager).addView(eq(mUdfpsView), any()); |
| } |
| |
| @Test |
| public void hideUdfpsOverlay_removesViewFromWindow() throws RemoteException { |
| mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, |
| IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback); |
| mOverlayController.hideUdfpsOverlay(TEST_UDFPS_SENSOR_ID); |
| mFgExecutor.runAllReady(); |
| verify(mWindowManager).removeView(eq(mUdfpsView)); |
| } |
| |
| @Test |
| public void fingerDown() throws RemoteException { |
| // Configure UdfpsView to accept the ACTION_DOWN event |
| when(mUdfpsView.isIlluminationRequested()).thenReturn(false); |
| when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); |
| |
| // GIVEN that the overlay is showing |
| mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, |
| IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback); |
| mFgExecutor.runAllReady(); |
| // WHEN ACTION_DOWN is received |
| verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); |
| MotionEvent downEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); |
| mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); |
| downEvent.recycle(); |
| MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); |
| mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); |
| moveEvent.recycle(); |
| // THEN FingerprintManager is notified about onPointerDown |
| verify(mFingerprintManager).onPointerDown(eq(mUdfpsController.mSensorProps.sensorId), eq(0), |
| eq(0), eq(0f), eq(0f)); |
| // AND illumination begins |
| verify(mUdfpsView).startIllumination(mOnIlluminatedRunnableCaptor.capture()); |
| // AND onIlluminatedRunnable notifies FingerprintManager about onUiReady |
| mOnIlluminatedRunnableCaptor.getValue().run(); |
| verify(mFingerprintManager).onUiReady(eq(mUdfpsController.mSensorProps.sensorId)); |
| } |
| |
| @Test |
| public void aodInterrupt() throws RemoteException { |
| // GIVEN that the overlay is showing and screen is on |
| mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, |
| IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback); |
| mScreenObserver.onScreenTurnedOn(); |
| mFgExecutor.runAllReady(); |
| // WHEN fingerprint is requested because of AOD interrupt |
| mUdfpsController.onAodInterrupt(0, 0, 2f, 3f); |
| // THEN illumination begins |
| // AND onIlluminatedRunnable that notifies FingerprintManager is set |
| verify(mUdfpsView).startIllumination(mOnIlluminatedRunnableCaptor.capture()); |
| mOnIlluminatedRunnableCaptor.getValue().run(); |
| verify(mFingerprintManager).onPointerDown(eq(mUdfpsController.mSensorProps.sensorId), eq(0), |
| eq(0), eq(3f) /* minor */, eq(2f) /* major */); |
| } |
| |
| @Test |
| public void cancelAodInterrupt() throws RemoteException { |
| // GIVEN AOD interrupt |
| mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, |
| IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback); |
| mScreenObserver.onScreenTurnedOn(); |
| mFgExecutor.runAllReady(); |
| mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); |
| // WHEN it is cancelled |
| mUdfpsController.onCancelUdfps(); |
| // THEN the illumination is hidden |
| verify(mUdfpsView).stopIllumination(); |
| } |
| |
| @Test |
| public void aodInterruptTimeout() throws RemoteException { |
| // GIVEN AOD interrupt |
| mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, |
| IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback); |
| mScreenObserver.onScreenTurnedOn(); |
| mFgExecutor.runAllReady(); |
| mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); |
| // WHEN it times out |
| mFgExecutor.advanceClockToNext(); |
| mFgExecutor.runAllReady(); |
| // THEN the illumination is hidden |
| verify(mUdfpsView).stopIllumination(); |
| } |
| |
| @Test |
| public void aodInterruptScreenOff() throws RemoteException { |
| // GIVEN screen off |
| mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, |
| IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback); |
| mScreenObserver.onScreenTurnedOff(); |
| mFgExecutor.runAllReady(); |
| |
| // WHEN aod interrupt is received |
| mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); |
| |
| // THEN no illumination because screen is off |
| verify(mUdfpsView, never()).startIllumination(any()); |
| } |
| |
| @Test |
| public void playHapticOnTouchUdfpsArea() throws RemoteException { |
| // Configure UdfpsView to accept the ACTION_DOWN event |
| when(mUdfpsView.isIlluminationRequested()).thenReturn(false); |
| when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); |
| |
| // GIVEN that the overlay is showing |
| mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, |
| IUdfpsOverlayController.REASON_AUTH_FPM_KEYGUARD, mUdfpsOverlayControllerCallback); |
| mFgExecutor.runAllReady(); |
| |
| // WHEN ACTION_DOWN is received |
| verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); |
| MotionEvent downEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); |
| mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); |
| downEvent.recycle(); |
| MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); |
| mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); |
| moveEvent.recycle(); |
| |
| // THEN click haptic is played |
| verify(mVibrator).vibrate(mUdfpsController.mEffectClick, |
| UdfpsController.VIBRATION_SONIFICATION_ATTRIBUTES); |
| } |
| } |