diff options
| -rw-r--r-- | packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java | 71 | ||||
| -rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java | 210 |
2 files changed, 254 insertions, 27 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 06c190f1964c..ba78485438ea 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -16,17 +16,16 @@ package com.android.systemui.biometrics; -import android.annotation.NonNull; +import static com.android.internal.util.Preconditions.checkNotNull; + import android.annotation.SuppressLint; -import android.content.ContentResolver; import android.content.Context; +import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.PixelFormat; import android.graphics.Point; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.IUdfpsOverlayController; -import android.os.Handler; -import android.os.Looper; import android.os.PowerManager; import android.os.UserHandle; import android.provider.Settings; @@ -38,10 +37,16 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.WindowManager; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.internal.BrightnessSynchronizer; import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.util.settings.SystemSettings; import java.io.FileWriter; import java.io.IOException; @@ -60,8 +65,8 @@ class UdfpsController implements DozeReceiver { private final FingerprintManager mFingerprintManager; private final WindowManager mWindowManager; - private final ContentResolver mContentResolver; - private final Handler mHandler; + private final SystemSettings mSystemSettings; + private final DelayableExecutor mFgExecutor; private final WindowManager.LayoutParams mLayoutParams; private final UdfpsView mView; // Debugfs path to control the high-brightness mode. @@ -88,7 +93,7 @@ class UdfpsController implements DozeReceiver { // interrupt is being tracked and a timeout is used as a last resort to turn off high brightness // mode. private boolean mIsAodInterruptActive; - private final Runnable mAodInterruptTimeoutAction = this::onCancelAodInterrupt; + @Nullable private Runnable mCancelAodTimeoutAction; public class UdfpsOverlayController extends IUdfpsOverlayController.Stub { @Override @@ -138,18 +143,27 @@ class UdfpsController implements DozeReceiver { @Inject UdfpsController(@NonNull Context context, - @NonNull StatusBarStateController statusBarStateController) { - mFingerprintManager = context.getSystemService(FingerprintManager.class); - mWindowManager = context.getSystemService(WindowManager.class); - mContentResolver = context.getContentResolver(); - mHandler = new Handler(Looper.getMainLooper()); + @Main Resources resources, + LayoutInflater inflater, + @Nullable FingerprintManager fingerprintManager, + PowerManager powerManager, + WindowManager windowManager, + SystemSettings systemSettings, + @NonNull StatusBarStateController statusBarStateController, + @Main DelayableExecutor fgExecutor) { + // The fingerprint manager is queried for UDFPS before this class is constructed, so the + // fingerprint manager should never be null. + mFingerprintManager = checkNotNull(fingerprintManager); + mWindowManager = windowManager; + mSystemSettings = systemSettings; + mFgExecutor = fgExecutor; mLayoutParams = createLayoutParams(context); - mView = (UdfpsView) LayoutInflater.from(context).inflate(R.layout.udfps_view, null, false); + mView = (UdfpsView) inflater.inflate(R.layout.udfps_view, null, false); - mHbmPath = context.getResources().getString(R.string.udfps_hbm_sysfs_path); - mHbmEnableCommand = context.getResources().getString(R.string.udfps_hbm_enable_command); - mHbmDisableCommand = context.getResources().getString(R.string.udfps_hbm_disable_command); + mHbmPath = resources.getString(R.string.udfps_hbm_sysfs_path); + mHbmEnableCommand = resources.getString(R.string.udfps_hbm_enable_command); + mHbmDisableCommand = resources.getString(R.string.udfps_hbm_disable_command); mHbmSupported = !TextUtils.isEmpty(mHbmPath); mView.setHbmSupported(mHbmSupported); @@ -157,11 +171,11 @@ class UdfpsController implements DozeReceiver { // This range only consists of the minimum and maximum values, which only cover // non-high-brightness mode. - float[] nitsRange = toFloatArray(context.getResources().obtainTypedArray( + float[] nitsRange = toFloatArray(resources.obtainTypedArray( com.android.internal.R.array.config_screenBrightnessNits)); // The last value of this range corresponds to the high-brightness mode. - float[] nitsAutoBrightnessValues = toFloatArray(context.getResources().obtainTypedArray( + float[] nitsAutoBrightnessValues = toFloatArray(resources.obtainTypedArray( com.android.internal.R.array.config_autoBrightnessDisplayValuesNits)); mHbmNits = nitsAutoBrightnessValues[nitsAutoBrightnessValues.length - 1]; @@ -170,12 +184,12 @@ class UdfpsController implements DozeReceiver { // This range only consists of the minimum and maximum backlight values, which only apply // in non-high-brightness mode. float[] normalizedBacklightRange = normalizeBacklightRange( - context.getResources().getIntArray( + resources.getIntArray( com.android.internal.R.array.config_screenBrightnessBacklight)); mBacklightToNitsSpline = Spline.createSpline(normalizedBacklightRange, nitsRange); mNitsToHbmBacklightSpline = Spline.createSpline(hbmNitsRange, normalizedBacklightRange); - mDefaultBrightness = obtainDefaultBrightness(context); + mDefaultBrightness = obtainDefaultBrightness(powerManager); // TODO(b/160025856): move to the "dump" method. Log.v(TAG, String.format("ctor | mNitsRange: [%f, %f]", nitsRange[0], nitsRange[1])); @@ -194,7 +208,7 @@ class UdfpsController implements DozeReceiver { } private void showUdfpsOverlay() { - mHandler.post(() -> { + mFgExecutor.execute(() -> { if (!mIsOverlayShowing) { try { Log.v(TAG, "showUdfpsOverlay | adding window"); @@ -211,7 +225,7 @@ class UdfpsController implements DozeReceiver { } private void hideUdfpsOverlay() { - mHandler.post(() -> { + mFgExecutor.execute(() -> { if (mIsOverlayShowing) { Log.v(TAG, "hideUdfpsOverlay | removing window"); mView.setOnTouchListener(null); @@ -228,7 +242,7 @@ class UdfpsController implements DozeReceiver { // Returns a value in the range of [0, 255]. private int computeScrimOpacity() { // Backlight setting can be NaN, -1.0f, and [0.0f, 1.0f]. - float backlightSetting = Settings.System.getFloatForUser(mContentResolver, + float backlightSetting = mSystemSettings.getFloatForUser( Settings.System.SCREEN_BRIGHTNESS_FLOAT, mDefaultBrightness, UserHandle.USER_CURRENT); @@ -265,7 +279,8 @@ class UdfpsController implements DozeReceiver { // Since the sensor that triggers the AOD interrupt doesn't provide ACTION_UP/ACTION_CANCEL, // we need to be careful about not letting the screen accidentally remain in high brightness // mode. As a mitigation, queue a call to cancel the fingerprint scan. - mHandler.postDelayed(mAodInterruptTimeoutAction, AOD_INTERRUPT_TIMEOUT_MILLIS); + mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelAodInterrupt, + AOD_INTERRUPT_TIMEOUT_MILLIS); // using a hard-coded value for major and minor until it is available from the sensor onFingerDown(screenX, screenY, 13.0f, 13.0f); } @@ -280,7 +295,10 @@ class UdfpsController implements DozeReceiver { if (!mIsAodInterruptActive) { return; } - mHandler.removeCallbacks(mAodInterruptTimeoutAction); + if (mCancelAodTimeoutAction != null) { + mCancelAodTimeoutAction.run(); + mCancelAodTimeoutAction = null; + } mIsAodInterruptActive = false; onFingerUp(); } @@ -338,8 +356,7 @@ class UdfpsController implements DozeReceiver { return lp; } - private static float obtainDefaultBrightness(Context context) { - PowerManager powerManager = context.getSystemService(PowerManager.class); + private static float obtainDefaultBrightness(PowerManager powerManager) { if (powerManager == null) { Log.e(TAG, "PowerManager is unavailable. Can't obtain default brightness."); return 0f; diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java new file mode 100644 index 000000000000..87ec72fe0a01 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -0,0 +1,210 @@ +/* + * 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 org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.IUdfpsOverlayController; +import android.os.PowerManager; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper.RunWithLooper; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.settings.FakeSettings; +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; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class UdfpsControllerTest extends SysuiTestCase { + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + // Unit under test + private UdfpsController mUdfpsController; + + // Dependencies + @Mock + private Resources mResources; + @Mock + private LayoutInflater mLayoutInflater; + @Mock + private FingerprintManager mFingerprintManager; + @Mock + private PowerManager mPowerManager; + @Mock + private WindowManager mWindowManager; + @Mock + private StatusBarStateController mStatusBarStateController; + private FakeSettings mSystemSettings; + 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; + + @Before + public void setUp() { + setUpResources(); + when(mLayoutInflater.inflate(R.layout.udfps_view, null, false)).thenReturn(mUdfpsView); + mSystemSettings = new FakeSettings(); + mFgExecutor = new FakeExecutor(new FakeSystemClock()); + mUdfpsController = new UdfpsController( + mContext, + mResources, + mLayoutInflater, + mFingerprintManager, + mPowerManager, + mWindowManager, + mSystemSettings, + mStatusBarStateController, + mFgExecutor); + verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); + mOverlayController = mOverlayCaptor.getValue(); + } + + 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(mResources.obtainTypedArray(com.android.internal.R.array.config_screenBrightnessNits)) + .thenReturn(mBrightnessValues); + 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); + when(mResources.obtainTypedArray( + com.android.internal.R.array.config_autoBrightnessDisplayValuesNits)) + .thenReturn(mBrightnessBacklight); + when(mResources.getIntArray(com.android.internal.R.array.config_screenBrightnessBacklight)) + .thenReturn(new int[]{1, 2}); + } + + @Test + public void dozeTimeTick() { + mUdfpsController.dozeTimeTick(); + verify(mUdfpsView).dozeTimeTick(); + } + + @Test + public void showUdfpsOverlay_addsViewToWindow() throws RemoteException { + mOverlayController.showUdfpsOverlay(); + mFgExecutor.runAllReady(); + verify(mWindowManager).addView(eq(mUdfpsView), any()); + } + + @Test + public void hideUdfpsOverlay_removesViewFromWindow() throws RemoteException { + mOverlayController.showUdfpsOverlay(); + mOverlayController.hideUdfpsOverlay(); + mFgExecutor.runAllReady(); + verify(mWindowManager).removeView(eq(mUdfpsView)); + } + + @Test + public void fingerDown() throws RemoteException { + // Configure UdfpsView to accept the ACTION_DOWN event + when(mUdfpsView.isScrimShowing()).thenReturn(false); + when(mUdfpsView.isValidTouch(anyFloat(), anyFloat(), anyFloat())).thenReturn(true); + + // GIVEN that the overlay is showing + mOverlayController.showUdfpsOverlay(); + mFgExecutor.runAllReady(); + // WHEN ACTION_DOWN is received + verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); + MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); + event.recycle(); + // THEN the event is passed to the FingerprintManager + verify(mFingerprintManager).onFingerDown(eq(0), eq(0), eq(0f), eq(0f)); + // AND the scrim and dot is shown + verify(mUdfpsView).showScrimAndDot(); + } + + @Test + public void aodInterrupt() throws RemoteException { + // GIVEN that the overlay is showing + mOverlayController.showUdfpsOverlay(); + mFgExecutor.runAllReady(); + // WHEN fingerprint is requested because of AOD interrupt + mUdfpsController.onAodInterrupt(0, 0); + // THEN the event is passed to the FingerprintManager + verify(mFingerprintManager).onFingerDown(eq(0), eq(0), anyFloat(), anyFloat()); + // AND the scrim and dot is shown + verify(mUdfpsView).showScrimAndDot(); + } + + @Test + public void cancelAodInterrupt() throws RemoteException { + // GIVEN AOD interrupt + mOverlayController.showUdfpsOverlay(); + mFgExecutor.runAllReady(); + mUdfpsController.onAodInterrupt(0, 0); + // WHEN it is cancelled + mUdfpsController.onCancelAodInterrupt(); + // THEN the scrim and dot is hidden + verify(mUdfpsView).hideScrimAndDot(); + } + + @Test + public void aodInterruptTimeout() throws RemoteException { + // GIVEN AOD interrupt + mOverlayController.showUdfpsOverlay(); + mFgExecutor.runAllReady(); + mUdfpsController.onAodInterrupt(0, 0); + // WHEN it times out + mFgExecutor.advanceClockToNext(); + mFgExecutor.runAllReady(); + // THEN the scrim and dot is hidden + verify(mUdfpsView).hideScrimAndDot(); + } +} |