Announce feedback for udfps misses during enroll

The following change now announces a hint message if
during the accessibility enrollment, a user places
their fingerprint outside of the view.

Test: atest UdfpsControllerOverlayTest
Test: Verified with multiple devices & different
display sizes.
Fixes: 186873966

Change-Id: I89bb327f45a4b3a37b4d587856e0b754e9e3eade
This commit is contained in:
Joshua McCloskey
2022-05-12 19:30:33 +00:00
committed by Joshua Mccloskey
parent 6f2afd1474
commit 82628cdfaf
6 changed files with 218 additions and 19 deletions

View File

@@ -804,6 +804,14 @@
<!-- Message shown when non-bypass face authentication succeeds and UDFPS is supported. Provides extra instructions for how the user can enter their device [CHAR LIMIT=60] -->
<string name="keyguard_face_successful_unlock_press">Unlocked by face. Press the unlock icon to open.</string>
<!-- Messages shown when users press outside of udfps region during -->
<string-array name="udfps_accessibility_touch_hints">
<item>Move left</item>
<item>Move down</item>
<item>Move right</item>
<item>Move up</item>
</string-array>
<!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
<string name="keyguard_retry">Swipe up to try again</string>

View File

@@ -192,4 +192,9 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>(
* Called on touches outside of the view if listenForTouchesOutsideView returns true
*/
open fun onTouchOutsideView() {}
/**
* Called when a view should announce an accessibility event.
*/
open fun doAnnounceForAccessibility(str: String) {}
}

View File

@@ -381,6 +381,27 @@ public class UdfpsController implements DozeReceiver {
&& 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;
}
@VisibleForTesting
boolean onTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) {
if (mOverlay == null) {
@@ -434,6 +455,7 @@ public class UdfpsController implements DozeReceiver {
mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */);
mAttemptedToDismissKeyguard = true;
}
Trace.endSection();
break;
@@ -457,6 +479,8 @@ public class UdfpsController implements DozeReceiver {
mAttemptedToDismissKeyguard = true;
break;
}
// Map the touch to portrait mode if the device is in landscape mode.
final Point scaledTouch = getTouchInNativeCoordinates(event, idx);
if (actionMoveWithinSensorArea) {
if (mVelocityTracker == null) {
// touches could be injected, so the velocity tracker may not have
@@ -477,28 +501,13 @@ public class UdfpsController implements DozeReceiver {
final long sinceLastLog = mSystemClock.elapsedRealtime() - mTouchLogTime;
if (!isIlluminationRequested && !mAcquiredReceived
&& !exceedsVelocityThreshold) {
// Map the touch to portrait mode if the device is in landscape mode.
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();
int scaledX = (int) (portraitTouch.x / scale);
int scaledY = (int) (portraitTouch.y / scale);
float scaledMinor = minor / scale;
float scaledMajor = major / scale;
onFingerDown(requestId, scaledX, scaledY, scaledMinor, scaledMajor);
onFingerDown(requestId, scaledTouch.x, scaledTouch.y, scaledMinor,
scaledMajor);
Log.v(TAG, "onTouch | finger down: " + touchInfo);
mTouchLogTime = mSystemClock.elapsedRealtime();
mPowerManager.userActivity(mSystemClock.uptimeMillis(),
@@ -511,6 +520,24 @@ public class UdfpsController implements DozeReceiver {
} else {
Log.v(TAG, "onTouch | finger outside");
onFingerUp(requestId, udfpsView);
// Maybe announce for accessibility.
mFgExecutor.execute(() -> {
if (mOverlay == null) {
Log.e(TAG, "touch outside sensor area received"
+ "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());
});
}
}
Trace.endSection();

View File

@@ -130,6 +130,8 @@ class UdfpsControllerOverlay(
val animationViewController: UdfpsAnimationViewController<*>?
get() = overlayView?.animationViewController
private var touchExplorationEnabled = false
/** Show the overlay or return false and do nothing if it is already showing. */
@SuppressLint("ClickableViewAccessibility")
fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
@@ -154,14 +156,16 @@ class UdfpsControllerOverlay(
}
windowManager.addView(this, coreLayoutParams.updateDimensions(animation))
touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
overlayTouchListener = TouchExplorationStateChangeListener {
if (accessibilityManager.isTouchExplorationEnabled) {
setOnHoverListener { v, event -> onTouch(v, event, true) }
setOnTouchListener(null)
touchExplorationEnabled = true
} else {
setOnHoverListener(null)
setOnTouchListener { v, event -> onTouch(v, event, true) }
touchExplorationEnabled = false
}
}
accessibilityManager.addTouchExplorationStateChangeListener(
@@ -179,7 +183,7 @@ class UdfpsControllerOverlay(
return false
}
private fun inflateUdfpsAnimation(
fun inflateUdfpsAnimation(
view: UdfpsView,
controller: UdfpsController
): UdfpsAnimationViewController<*>? {
@@ -278,6 +282,88 @@ class UdfpsControllerOverlay(
enrollHelper?.onEnrollmentHelp()
}
/**
* 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.
*
*/
fun onTouchOutsideOfSensorArea(
touchX: Float,
touchY: Float,
sensorX: Float,
sensorY: Float,
rotation: Int
) {
if (!touchExplorationEnabled) {
return
}
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. */
fun cancel() {
try {

View File

@@ -99,4 +99,10 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp
public int getPaddingY() {
return mEnrollProgressBarRadius;
}
@Override
public void doAnnounceForAccessibility(String str) {
mView.announceForAccessibility(str);
}
}

View File

@@ -367,6 +367,73 @@ 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(