diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 8508878e95d7a..9658a93d7599d 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1626,4 +1626,12 @@ Enable system animations for predictive back. This setting enables system animations for predictive gesture animation. It requires setting per-app "enableOnBackInvokedCallback" to true in the manifest file. + + + + Move left + Move down + Move right + Move up + diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt similarity index 97% rename from packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt rename to packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt index 7f3846ca4e40a..d55a027c2374b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt +++ b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt @@ -1,4 +1,4 @@ -package com.android.systemui.biometrics +package com.android.settingslib.udfps import android.graphics.Rect import android.view.Surface diff --git a/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsUtils.java b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsUtils.java new file mode 100644 index 0000000000000..c966757cc789c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsUtils.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 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.settingslib.udfps; + +import android.content.Context; +import android.graphics.Point; +import android.util.DisplayUtils; +import android.util.Log; +import android.util.RotationUtils; +import android.view.Display; +import android.view.DisplayInfo; +import android.view.MotionEvent; +import android.view.Surface; + +import com.android.settingslib.R; + +/** Utility class for working with udfps. */ +public class UdfpsUtils { + private static final String TAG = "UdfpsUtils"; + + /** + * Gets the scale factor representing the user's current resolution / the stable (default) + * resolution. + * + * @param displayInfo The display information. + */ + public float getScaleFactor(DisplayInfo displayInfo) { + Display.Mode maxDisplayMode = + DisplayUtils.getMaximumResolutionDisplayMode(displayInfo.supportedModes); + float scaleFactor = + DisplayUtils.getPhysicalPixelDisplaySizeRatio( + maxDisplayMode.getPhysicalWidth(), + maxDisplayMode.getPhysicalHeight(), + displayInfo.getNaturalWidth(), + displayInfo.getNaturalHeight() + ); + return (scaleFactor == Float.POSITIVE_INFINITY) ? 1f : scaleFactor; + } + + /** + * Gets the touch in native coordinates. Map the touch to portrait mode if the device is in + * landscape mode. + * + * @param idx The pointer identifier. + * @param event The MotionEvent object containing full information about the event. + * @param udfpsOverlayParams The [UdfpsOverlayParams] used. + * @return The mapped touch event. + */ + public Point getTouchInNativeCoordinates(int idx, MotionEvent event, + UdfpsOverlayParams udfpsOverlayParams) { + Point portraitTouch = new Point((int) event.getRawX(idx), (int) event.getRawY(idx)); + int rot = udfpsOverlayParams.getRotation(); + if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { + RotationUtils.rotatePoint( + portraitTouch, + RotationUtils.deltaRotation(rot, Surface.ROTATION_0), + udfpsOverlayParams.getLogicalDisplayWidth(), + udfpsOverlayParams.getLogicalDisplayHeight() + ); + } + + // Scale the coordinates to native resolution. + float scale = udfpsOverlayParams.getScaleFactor(); + portraitTouch.x = (int) (portraitTouch.x / scale); + portraitTouch.y = (int) (portraitTouch.y / scale); + return portraitTouch; + } + + /** + * This function computes the angle of touch relative to the sensor and maps the angle to a list + * of help messages which are announced if accessibility is enabled. + * + * @return Whether the announcing string is null + */ + public String onTouchOutsideOfSensorArea(boolean touchExplorationEnabled, + Context context, int touchX, int touchY, UdfpsOverlayParams udfpsOverlayParams) { + if (!touchExplorationEnabled) { + return null; + } + + String[] touchHints = context.getResources().getStringArray( + R.array.udfps_accessibility_touch_hints); + if (touchHints.length != 4) { + Log.e(TAG, "expected exactly 4 touch hints, got " + touchHints.length + "?"); + return null; + } + + // Scale the coordinates to native resolution. + float scale = udfpsOverlayParams.getScaleFactor(); + float scaledSensorX = udfpsOverlayParams.getSensorBounds().centerX() / scale; + float scaledSensorY = udfpsOverlayParams.getSensorBounds().centerY() / scale; + String theStr = + onTouchOutsideOfSensorAreaImpl( + touchHints, + touchX, + touchY, + scaledSensorX, + scaledSensorY, + udfpsOverlayParams.getRotation() + ); + Log.v(TAG, "Announcing touch outside : $theStr"); + return theStr; + } + + /** + * This function computes the angle of touch relative to the sensor and maps the angle to a list + * of help messages which are announced if accessibility is enabled. + * + * There are 4 quadrants of the circle (90 degree arcs) + * + * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left" [45, 135) -> + * touchHints[1] = "Move Fingerprint down" And so on. + */ + private String onTouchOutsideOfSensorAreaImpl(String[] touchHints, float touchX, + float touchY, float sensorX, float sensorY, int rotation) { + float xRelativeToSensor = touchX - sensorX; + // Touch coordinates are with respect to the upper left corner, so reverse + // this calculation + float yRelativeToSensor = sensorY - touchY; + var angleInRad = Math.atan2(yRelativeToSensor, xRelativeToSensor); + // If the radians are negative, that means we are counting clockwise. + // So we need to add 360 degrees + if (angleInRad < 0.0) { + angleInRad += 2.0 * Math.PI; + } + // rad to deg conversion + double degrees = Math.toDegrees(angleInRad); + double degreesPerBucket = 360.0 / touchHints.length; + double halfBucketDegrees = degreesPerBucket / 2.0; + // The mapping should be as follows + // [315, 360] && [0, 45] -> 0 + // [45, 135] -> 1 + int index = (int) ((degrees + halfBucketDegrees) % 360 / degreesPerBucket); + index %= touchHints.length; + + // A rotation of 90 degrees corresponds to increasing the index by 1. + if (rotation == Surface.ROTATION_90) { + index = (index + 1) % touchHints.length; + } + if (rotation == Surface.ROTATION_270) { + index = (index + 3) % touchHints.length; + } + return touchHints[index]; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/udfps/UdfpsUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/udfps/UdfpsUtilsTest.java new file mode 100644 index 0000000000000..f4f0ef9a163a4 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/udfps/UdfpsUtilsTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2023 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.settingslib.udfps; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.graphics.Rect; +import android.view.Surface; + +import com.android.settingslib.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + + +@RunWith(RobolectricTestRunner.class) +public class UdfpsUtilsTest { + @Rule + public final MockitoRule rule = MockitoJUnit.rule(); + + private Context mContext; + private String[] mTouchHints; + private UdfpsUtils mUdfpsUtils; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mTouchHints = mContext.getResources().getStringArray( + R.array.udfps_accessibility_touch_hints); + mUdfpsUtils = new UdfpsUtils(); + } + + @Test + public void testTouchOutsideAreaNoRotation() { + int rotation = Surface.ROTATION_0; + // touch at 0 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + 0 /* touchX */, 0/* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[0]); + // touch at 90 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + 0 /* touchX */, -1/* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[1]); + // touch at 180 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + -1 /* touchX */, 0/* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[2]); + // touch at 270 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + 0 /* touchX */, 1/* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[3]); + } + + + @Test + public void testTouchOutsideAreaNoRotation90Degrees() { + int rotation = Surface.ROTATION_90; + // touch at 0 degrees -> 90 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + 0 /* touchX */, 0 /* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[1]); + // touch at 90 degrees -> 180 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + 0 /* touchX */, -1 /* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[2]); + // touch at 180 degrees -> 270 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + -1 /* touchX */, 0 /* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[3]); + // touch at 270 degrees -> 0 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + 0 /* touchX */, 1/* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[0]); + } + + + @Test + public void testTouchOutsideAreaNoRotation270Degrees() { + int rotation = Surface.ROTATION_270; + // touch at 0 degrees -> 270 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + 0 /* touchX */, 0/* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[3]); + // touch at 90 degrees -> 0 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + 0 /* touchX */, -1/* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[0]); + // touch at 180 degrees -> 90 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + -1 /* touchX */, 0/* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[1]); + // touch at 270 degrees -> 180 degrees + assertThat( + mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext, + 0 /* touchX */, 1/* touchY */, + new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation) + ) + ).isEqualTo(mTouchHints[2]); + } +} diff --git a/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml b/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml index de43a5e8b0297..4799f8c5b6687 100644 --- a/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml +++ b/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml @@ -19,4 +19,12 @@ android:id="@+id/udfps_animation_view" android:layout_width="match_parent" android:layout_height="match_parent"> + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 943844f0f8362..83d190dffa492 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -908,14 +908,6 @@ Face recognized - - - Move left - Move down - Move right - Move up - - Swipe up to try again diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index dad6ebe401849..c8cf5d775ddf1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -55,7 +55,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.UserManager; -import android.util.DisplayUtils; import android.util.Log; import android.util.RotationUtils; import android.util.SparseBooleanArray; @@ -69,6 +68,8 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.os.SomeArgs; import com.android.internal.widget.LockPatternUtils; +import com.android.settingslib.udfps.UdfpsOverlayParams; +import com.android.settingslib.udfps.UdfpsUtils; import com.android.systemui.CoreStartable; import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; import com.android.systemui.biometrics.domain.interactor.LogContextInteractor; @@ -169,6 +170,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull private final UserManager mUserManager; @NonNull private final LockPatternUtils mLockPatternUtils; @NonNull private final InteractionJankMonitor mInteractionJankMonitor; + @NonNull private final UdfpsUtils mUdfpsUtils; private final @Background DelayableExecutor mBackgroundExecutor; private final DisplayInfo mCachedDisplayInfo = new DisplayInfo(); @@ -578,17 +580,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, */ private void updateSensorLocations() { mDisplay.getDisplayInfo(mCachedDisplayInfo); - final Display.Mode maxDisplayMode = - DisplayUtils.getMaximumResolutionDisplayMode(mCachedDisplayInfo.supportedModes); - final float scaleFactor = android.util.DisplayUtils.getPhysicalPixelDisplaySizeRatio( - maxDisplayMode.getPhysicalWidth(), maxDisplayMode.getPhysicalHeight(), - mCachedDisplayInfo.getNaturalWidth(), mCachedDisplayInfo.getNaturalHeight()); - if (scaleFactor == Float.POSITIVE_INFINITY) { - mScaleFactor = 1f; - } else { - mScaleFactor = scaleFactor; - } - + mScaleFactor = mUdfpsUtils.getScaleFactor(mCachedDisplayInfo); updateUdfpsLocation(); updateFingerprintLocation(); updateFaceLocation(); @@ -732,7 +724,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull InteractionJankMonitor jankMonitor, @Main Handler handler, @Background DelayableExecutor bgExecutor, - @NonNull VibratorHelper vibrator) { + @NonNull VibratorHelper vibrator, + @NonNull UdfpsUtils udfpsUtils) { mContext = context; mExecution = execution; mUserManager = userManager; @@ -753,6 +746,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mSfpsEnrolledForUser = new SparseBooleanArray(); mFaceEnrolledForUser = new SparseBooleanArray(); mVibratorHelper = vibrator; + mUdfpsUtils = udfpsUtils; mLogContextInteractor = logContextInteractor; mBiometricPromptInteractor = biometricPromptInteractor; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 64d5518cd44d0..074928af13bda 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -50,10 +50,8 @@ import android.os.Trace; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.util.Log; -import android.util.RotationUtils; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.Surface; import android.view.VelocityTracker; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; @@ -66,6 +64,8 @@ import com.android.internal.logging.InstanceId; import com.android.internal.util.LatencyTracker; import com.android.keyguard.FaceAuthApiRequestReason; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.settingslib.udfps.UdfpsOverlayParams; +import com.android.settingslib.udfps.UdfpsUtils; import com.android.systemui.Dumpable; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.biometrics.dagger.BiometricsBackground; @@ -168,6 +168,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull private final SessionTracker mSessionTracker; @NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor; @NonNull private final SecureSettings mSecureSettings; + @NonNull private final UdfpsUtils mUdfpsUtils; // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @@ -266,7 +267,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mUdfpsDisplayMode, mSecureSettings, requestId, reason, callback, (view, event, fromUdfpsView) -> onTouch(requestId, event, fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags, - mPrimaryBouncerInteractor, mAlternateBouncerInteractor))); + mPrimaryBouncerInteractor, mAlternateBouncerInteractor, mUdfpsUtils))); } @Override @@ -475,27 +476,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { && mOverlayParams.getSensorBounds().contains((int) x, (int) y); } - private Point getTouchInNativeCoordinates(@NonNull MotionEvent event, int idx) { - Point portraitTouch = new Point( - (int) event.getRawX(idx), - (int) event.getRawY(idx) - ); - final int rot = mOverlayParams.getRotation(); - if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { - RotationUtils.rotatePoint(portraitTouch, - RotationUtils.deltaRotation(rot, Surface.ROTATION_0), - mOverlayParams.getLogicalDisplayWidth(), - mOverlayParams.getLogicalDisplayHeight() - ); - } - - // Scale the coordinates to native resolution. - final float scale = mOverlayParams.getScaleFactor(); - portraitTouch.x = (int) (portraitTouch.x / scale); - portraitTouch.y = (int) (portraitTouch.y / scale); - return portraitTouch; - } - private void tryDismissingKeyguard() { if (!mOnFingerDown) { playStartHaptic(); @@ -713,7 +693,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { break; } // Map the touch to portrait mode if the device is in landscape mode. - final Point scaledTouch = getTouchInNativeCoordinates(event, idx); + final Point scaledTouch = mUdfpsUtils.getTouchInNativeCoordinates( + idx, event, mOverlayParams); if (actionMoveWithinSensorArea) { if (mVelocityTracker == null) { // touches could be injected, so the velocity tracker may not have @@ -757,16 +738,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { + "but serverRequest is null"); return; } - // Scale the coordinates to native resolution. - final float scale = mOverlayParams.getScaleFactor(); - final float scaledSensorX = - mOverlayParams.getSensorBounds().centerX() / scale; - final float scaledSensorY = - mOverlayParams.getSensorBounds().centerY() / scale; - - mOverlay.onTouchOutsideOfSensorArea( - scaledTouch.x, scaledTouch.y, scaledSensorX, scaledSensorY, - mOverlayParams.getRotation()); + mOverlay.onTouchOutsideOfSensorArea(scaledTouch); }); } } @@ -838,7 +810,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor, @NonNull SessionTracker sessionTracker, @NonNull AlternateBouncerInteractor alternateBouncerInteractor, - @NonNull SecureSettings secureSettings) { + @NonNull SecureSettings secureSettings, + @NonNull UdfpsUtils udfpsUtils) { mContext = context; mExecution = execution; mVibrator = vibrator; @@ -880,6 +853,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mPrimaryBouncerInteractor = primaryBouncerInteractor; mAlternateBouncerInteractor = alternateBouncerInteractor; mSecureSettings = secureSettings; + mUdfpsUtils = udfpsUtils; mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) ? singlePointerTouchProcessor : null; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 45ca24d2df6e1..55bacef0b9774 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import android.annotation.UiThread import android.content.Context import android.graphics.PixelFormat +import android.graphics.Point import android.graphics.Rect import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD @@ -46,6 +47,8 @@ import android.view.accessibility.AccessibilityManager.TouchExplorationStateChan import androidx.annotation.LayoutRes import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor +import com.android.settingslib.udfps.UdfpsUtils +import com.android.settingslib.udfps.UdfpsOverlayParams import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager @@ -101,6 +104,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private val primaryBouncerInteractor: PrimaryBouncerInteractor, private val alternateBouncerInteractor: AlternateBouncerInteractor, private val isDebuggable: Boolean = Build.IS_DEBUGGABLE, + private val udfpsUtils: UdfpsUtils ) { /** The view, when [isShowing], or null. */ var overlayView: UdfpsView? = null @@ -239,7 +243,9 @@ class UdfpsControllerOverlay @JvmOverloads constructor( FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) { // Enroll udfps UI is handled by settings, so use empty view here UdfpsFpmEmptyViewController( - view.addUdfpsView(R.layout.udfps_fpm_empty_view), + view.addUdfpsView(R.layout.udfps_fpm_empty_view){ + updateAccessibilityViewLocation(sensorBounds) + }, statusBarStateController, shadeExpansionStateManager, dialogManager, @@ -348,81 +354,18 @@ class UdfpsControllerOverlay @JvmOverloads constructor( * the angle to a list of help messages which are announced if accessibility is enabled. * */ - fun onTouchOutsideOfSensorArea( - touchX: Float, - touchY: Float, - sensorX: Float, - sensorY: Float, - rotation: Int - ) { - - if (!touchExplorationEnabled) { - return + fun onTouchOutsideOfSensorArea(scaledTouch: Point) { + val theStr = + udfpsUtils.onTouchOutsideOfSensorArea( + touchExplorationEnabled, + context, + scaledTouch.x, + scaledTouch.y, + overlayParams + ) + if (theStr != null) { + animationViewController?.doAnnounceForAccessibility(theStr) } - val touchHints = - context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) - if (touchHints.size != 4) { - Log.e(TAG, "expected exactly 4 touch hints, got $touchHints.size?") - return - } - val theStr = onTouchOutsideOfSensorAreaImpl(touchX, touchY, sensorX, sensorY, rotation) - Log.v(TAG, "Announcing touch outside : " + theStr) - animationViewController?.doAnnounceForAccessibility(theStr) - } - - /** - * This function computes the angle of touch relative to the sensor and maps - * the angle to a list of help messages which are announced if accessibility is enabled. - * - * There are 4 quadrants of the circle (90 degree arcs) - * - * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left" - * [45, 135) -> touchHints[1] = "Move Fingerprint down" - * And so on. - */ - fun onTouchOutsideOfSensorAreaImpl( - touchX: Float, - touchY: Float, - sensorX: Float, - sensorY: Float, - rotation: Int - ): String { - val touchHints = - context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) - - val xRelativeToSensor = touchX - sensorX - // Touch coordinates are with respect to the upper left corner, so reverse - // this calculation - val yRelativeToSensor = sensorY - touchY - - var angleInRad = - Math.atan2(yRelativeToSensor.toDouble(), xRelativeToSensor.toDouble()) - // If the radians are negative, that means we are counting clockwise. - // So we need to add 360 degrees - if (angleInRad < 0.0) { - angleInRad += 2.0 * Math.PI - } - // rad to deg conversion - val degrees = Math.toDegrees(angleInRad) - - val degreesPerBucket = 360.0 / touchHints.size - val halfBucketDegrees = degreesPerBucket / 2.0 - // The mapping should be as follows - // [315, 360] && [0, 45] -> 0 - // [45, 135] -> 1 - var index = (((degrees + halfBucketDegrees) % 360) / degreesPerBucket).toInt() - index %= touchHints.size - - // A rotation of 90 degrees corresponds to increasing the index by 1. - if (rotation == Surface.ROTATION_90) { - index = (index + 1) % touchHints.size - } - - if (rotation == Surface.ROTATION_270) { - index = (index + 3) % touchHints.size - } - - return touchHints[index] } /** Cancel this request. */ diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt index e8f041ec0d713..8352d0aeab350 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt @@ -16,7 +16,11 @@ package com.android.systemui.biometrics import android.content.Context +import android.graphics.Rect import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import com.android.systemui.R /** * View corresponding with udfps_fpm_empty_view.xml @@ -32,4 +36,13 @@ class UdfpsFpmEmptyView( private val fingerprintDrawable: UdfpsFpDrawable = UdfpsFpDrawable(context) override fun getDrawable(): UdfpsDrawable = fingerprintDrawable + + fun updateAccessibilityViewLocation(sensorBounds: Rect) { + val fingerprintAccessibilityView: View = findViewById(R.id.udfps_enroll_accessibility_view) + val params: ViewGroup.LayoutParams = fingerprintAccessibilityView.layoutParams + params.width = sensorBounds.width() + params.height = sensorBounds.height() + fingerprintAccessibilityView.layoutParams = params + fingerprintAccessibilityView.requestLayout() + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt index 802b9b6c02959..079c0b3f69665 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt @@ -32,6 +32,7 @@ import android.view.MotionEvent import android.view.WindowManager import android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY import com.android.keyguard.KeyguardUpdateMonitor +import com.android.settingslib.udfps.UdfpsOverlayParams import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt index 4e6a06b1c44b5..28ca41d166a8f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt @@ -25,6 +25,7 @@ import android.graphics.RectF import android.util.AttributeSet import android.view.MotionEvent import android.widget.FrameLayout +import com.android.settingslib.udfps.UdfpsOverlayParams private const val TAG = "UdfpsOverlayView" private const val POINT_SIZE = 10f diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt index e61c614f0292b..06dee7a2b9f6f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt @@ -26,6 +26,7 @@ import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import android.widget.FrameLayout +import com.android.settingslib.udfps.UdfpsOverlayParams import com.android.systemui.R import com.android.systemui.doze.DozeReceiver diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt index 6f8efba2e84f7..67d2f308d3267 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.dagger +import com.android.settingslib.udfps.UdfpsUtils import com.android.systemui.biometrics.data.repository.PromptRepository import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl import com.android.systemui.biometrics.domain.interactor.CredentialInteractor @@ -54,6 +55,9 @@ interface BiometricsModule { @BiometricsBackground fun providesPluginExecutor(threadFactory: ThreadFactory): Executor = threadFactory.buildExecutorOnNewThread("biometrics") + + @Provides + fun providesUdfpsUtils(): UdfpsUtils = UdfpsUtils() } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt index 39ea9368dacb3..234b383e20671 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt @@ -21,7 +21,7 @@ import android.util.RotationUtils import android.view.MotionEvent import android.view.MotionEvent.INVALID_POINTER_ID import android.view.Surface -import com.android.systemui.biometrics.UdfpsOverlayParams +import com.android.settingslib.udfps.UdfpsOverlayParams import com.android.systemui.biometrics.udfps.TouchProcessorResult.Failure import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch import com.android.systemui.dagger.SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt index ffcebf9cff754..4bf0ef69a4f94 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt @@ -17,7 +17,7 @@ package com.android.systemui.biometrics.udfps import android.view.MotionEvent -import com.android.systemui.biometrics.UdfpsOverlayParams +import com.android.settingslib.udfps.UdfpsOverlayParams /** * Determines whether a finger entered or left the area of the under-display fingerprint sensor diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index 5afe49ea3cb32..0d00e8a85732b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -85,6 +85,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.widget.LockPatternUtils; +import com.android.settingslib.udfps.UdfpsUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; import com.android.systemui.biometrics.domain.interactor.LogContextInteractor; @@ -167,6 +168,8 @@ public class AuthControllerTest extends SysuiTestCase { private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor; @Mock private CredentialViewModel mCredentialViewModel; + @Mock + private UdfpsUtils mUdfpsUtils; @Captor private ArgumentCaptor mFpAuthenticatorsRegisteredCaptor; @@ -958,7 +961,7 @@ public class AuthControllerTest extends SysuiTestCase { mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger, mLogContextInteractor, () -> mBiometricPromptCredentialInteractor, () -> mCredentialViewModel, mInteractionJankMonitor, mHandler, - mBackgroundExecutor, mVibratorHelper); + mBackgroundExecutor, mVibratorHelper, mUdfpsUtils); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index 36ed6d530539e..9866163febd0a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -38,6 +38,8 @@ import android.view.WindowManager import android.view.accessibility.AccessibilityManager import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor +import com.android.settingslib.udfps.UdfpsOverlayParams +import com.android.settingslib.udfps.UdfpsUtils import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator @@ -110,6 +112,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var featureFlags: FeatureFlags @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor + @Mock private lateinit var udfpsUtils: UdfpsUtils @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true } @@ -144,7 +147,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { configurationController, keyguardStateController, unlockedScreenOffAnimationController, udfpsDisplayMode, secureSettings, REQUEST_ID, reason, controllerCallback, onTouch, activityLaunchAnimator, featureFlags, - primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable + primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable, udfpsUtils ) block() } @@ -400,109 +403,6 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { assertThat(controllerOverlay.matchesRequestId(REQUEST_ID)).isTrue() assertThat(controllerOverlay.matchesRequestId(REQUEST_ID + 1)).isFalse() } - - @Test - fun testTouchOutsideAreaNoRotation() = withReason(REASON_ENROLL_ENROLLING) { - val touchHints = - context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) - val rotation = Surface.ROTATION_0 - // touch at 0 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - 0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[0]) - // touch at 90 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - 0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[1]) - // touch at 180 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - -1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[2]) - // touch at 270 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - 0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[3]) - } - - fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) { - val touchHints = - context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) - val rotation = Surface.ROTATION_90 - // touch at 0 degrees -> 90 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - 0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[1]) - // touch at 90 degrees -> 180 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - 0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[2]) - // touch at 180 degrees -> 270 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - -1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[3]) - // touch at 270 degrees -> 0 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - 0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[0]) - } - - fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) { - val touchHints = - context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) - val rotation = Surface.ROTATION_270 - // touch at 0 degrees -> 270 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - 0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[3]) - // touch at 90 degrees -> 0 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - 0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[0]) - // touch at 180 degrees -> 90 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - -1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[1]) - // touch at 270 degrees -> 180 degrees - assertThat( - controllerOverlay.onTouchOutsideOfSensorAreaImpl( - 0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation - ) - ).isEqualTo(touchHints[2]) - } } private class EnrollListener( diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index dd7082ad88fff..17c262dbbd9aa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -71,6 +71,8 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.settingslib.udfps.UdfpsOverlayParams; +import com.android.settingslib.udfps.UdfpsUtils; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; @@ -230,10 +232,12 @@ public class UdfpsControllerTest extends SysuiTestCase { private ScreenLifecycle.Observer mScreenObserver; private FingerprintSensorPropertiesInternal mOpticalProps; private FingerprintSensorPropertiesInternal mUltrasonicProps; + private UdfpsUtils mUdfpsUtils; @Before public void setUp() { Execution execution = new FakeExecution(); + mUdfpsUtils = new UdfpsUtils(); when(mLayoutInflater.inflate(R.layout.udfps_view, null, false)) .thenReturn(mUdfpsView); @@ -305,7 +309,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker, mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor, mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, mSessionTracker, - mAlternateBouncerInteractor, mSecureSettings); + mAlternateBouncerInteractor, mSecureSettings, mUdfpsUtils); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt index 44fa4eb093083..07b4a649a6041 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt @@ -25,6 +25,7 @@ import android.testing.ViewUtils import android.view.LayoutInflater import android.view.Surface import androidx.test.filters.SmallTest +import com.android.settingslib.udfps.UdfpsOverlayParams import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.any diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt index 8e20303fd1892..c40fd4faf0070 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt @@ -23,8 +23,8 @@ import android.view.MotionEvent.PointerProperties import android.view.Surface import android.view.Surface.Rotation import androidx.test.filters.SmallTest +import com.android.settingslib.udfps.UdfpsOverlayParams import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.UdfpsOverlayParams import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith