Introduce testable UDFPS touch architecture

Bug: 218388821
Bug: 218374828
Test: atest SinglePointerUdfpsTouchProcessorTest
Change-Id: I5654550873f580db07d708e3de1a13e45f70b052
This commit is contained in:
Ilya Matyukhin
2022-11-12 07:21:15 +00:00
parent 3318c87c48
commit ea6cdd1759
7 changed files with 963 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2022 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.udfps
import android.view.MotionEvent
/** Interaction event between a finger and the under-display fingerprint sensor (UDFPS). */
enum class InteractionEvent {
/**
* A finger entered the sensor area. This can originate from either [MotionEvent.ACTION_DOWN] or
* [MotionEvent.ACTION_MOVE].
*/
DOWN,
/**
* A finger left the sensor area. This can originate from either [MotionEvent.ACTION_UP] or
* [MotionEvent.ACTION_MOVE].
*/
UP,
/**
* The touch reporting has stopped. This corresponds to [MotionEvent.ACTION_CANCEL]. This should
* not be confused with [UP]. If there was a finger on the sensor, it may or may not still be on
* the sensor.
*/
CANCEL,
/**
* The interaction hasn't changed since the previous event. The can originate from any of
* [MotionEvent.ACTION_DOWN], [MotionEvent.ACTION_MOVE], or [MotionEvent.ACTION_UP] if one of
* these is true:
* - There was previously a finger on the sensor, and that finger is still on the sensor.
* - There was previously no finger on the sensor, and there still isn't.
*/
UNCHANGED,
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2022 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.udfps
import android.graphics.RectF
import android.view.MotionEvent
import com.android.systemui.biometrics.UdfpsOverlayParams
/** Touch data in natural orientation and native resolution. */
data class NormalizedTouchData(
/**
* Value obtained from [MotionEvent.getPointerId], or [MotionEvent.INVALID_POINTER_ID] if the ID
* is not available.
*/
val pointerId: Int,
/** [MotionEvent.getRawX] mapped to natural orientation and native resolution. */
val x: Float,
/** [MotionEvent.getRawY] mapped to natural orientation and native resolution. */
val y: Float,
/** [MotionEvent.getTouchMinor] mapped to natural orientation and native resolution. */
val minor: Float,
/** [MotionEvent.getTouchMajor] mapped to natural orientation and native resolution. */
val major: Float,
/** [MotionEvent.getOrientation] mapped to natural orientation. */
val orientation: Float,
/** [MotionEvent.getEventTime]. */
val time: Long,
/** [MotionEvent.getDownTime]. */
val gestureStart: Long,
) {
/**
* [overlayParams] contains the location and dimensions of the sensor area, as well as the scale
* factor and orientation of the overlay. See [UdfpsOverlayParams].
*
* Returns whether the given pointer is within the sensor's bounding box.
*/
fun isWithinSensor(overlayParams: UdfpsOverlayParams): Boolean {
val r = RectF(overlayParams.sensorBounds).apply { scale(1f / overlayParams.scaleFactor) }
return r.left <= x && r.right >= x && r.top <= y && r.bottom >= y
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright (C) 2022 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.udfps
import android.graphics.PointF
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.systemui.biometrics.udfps.TouchProcessorResult.Failure
import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch
// TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations.
class SinglePointerTouchProcessor : TouchProcessor {
override fun processTouch(
event: MotionEvent,
previousPointerOnSensorId: Int,
overlayParams: UdfpsOverlayParams,
): TouchProcessorResult {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN ->
processActionDown(event.preprocess(previousPointerOnSensorId, overlayParams))
MotionEvent.ACTION_MOVE ->
processActionMove(event.preprocess(previousPointerOnSensorId, overlayParams))
MotionEvent.ACTION_UP ->
processActionUp(event.preprocess(previousPointerOnSensorId, overlayParams))
MotionEvent.ACTION_CANCEL ->
processActionCancel(event.preprocess(previousPointerOnSensorId, overlayParams))
else ->
Failure("Unsupported MotionEvent." + MotionEvent.actionToString(event.actionMasked))
}
}
}
private data class PreprocessedTouch(
val data: NormalizedTouchData,
val previousPointerOnSensorId: Int,
val isWithinSensor: Boolean,
)
private fun processActionDown(touch: PreprocessedTouch): TouchProcessorResult {
return if (touch.isWithinSensor) {
ProcessedTouch(InteractionEvent.DOWN, pointerOnSensorId = touch.data.pointerId, touch.data)
} else {
val event =
if (touch.data.pointerId == touch.previousPointerOnSensorId) {
InteractionEvent.UP
} else {
InteractionEvent.UNCHANGED
}
ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data)
}
}
private fun processActionMove(touch: PreprocessedTouch): TouchProcessorResult {
val hadPointerOnSensor = touch.previousPointerOnSensorId != INVALID_POINTER_ID
val interactionEvent =
when {
touch.isWithinSensor && !hadPointerOnSensor -> InteractionEvent.DOWN
!touch.isWithinSensor && hadPointerOnSensor -> InteractionEvent.UP
else -> InteractionEvent.UNCHANGED
}
val pointerOnSensorId =
when (interactionEvent) {
InteractionEvent.UNCHANGED -> touch.previousPointerOnSensorId
InteractionEvent.DOWN -> touch.data.pointerId
else -> INVALID_POINTER_ID
}
return ProcessedTouch(interactionEvent, pointerOnSensorId, touch.data)
}
private fun processActionUp(touch: PreprocessedTouch): TouchProcessorResult {
return if (touch.isWithinSensor) {
ProcessedTouch(InteractionEvent.UP, pointerOnSensorId = INVALID_POINTER_ID, touch.data)
} else {
val event =
if (touch.previousPointerOnSensorId != INVALID_POINTER_ID) {
InteractionEvent.UP
} else {
InteractionEvent.UNCHANGED
}
ProcessedTouch(event, pointerOnSensorId = INVALID_POINTER_ID, touch.data)
}
}
private fun processActionCancel(touch: PreprocessedTouch): TouchProcessorResult {
return ProcessedTouch(
InteractionEvent.CANCEL,
pointerOnSensorId = INVALID_POINTER_ID,
touch.data
)
}
/** Returns [PreprocessedTouch], which is an input to all the action-specific functions. */
private fun MotionEvent.preprocess(
previousPointerOnSensorId: Int,
overlayParams: UdfpsOverlayParams
): PreprocessedTouch {
// TODO(b/253085297): Add multitouch support. pointerIndex can be > 0 for ACTION_MOVE.
val pointerIndex = 0
val touchData = normalize(pointerIndex, overlayParams)
val isWithinSensor = touchData.isWithinSensor(overlayParams)
return PreprocessedTouch(touchData, previousPointerOnSensorId, isWithinSensor)
}
/**
* Returns the touch information from the given [MotionEvent] with the relevant fields mapped to
* natural orientation and native resolution.
*/
private fun MotionEvent.normalize(
pointerIndex: Int,
overlayParams: UdfpsOverlayParams
): NormalizedTouchData {
val naturalTouch: PointF = rotateToNaturalOrientation(pointerIndex, overlayParams)
val nativeX = naturalTouch.x / overlayParams.scaleFactor
val nativeY = naturalTouch.y / overlayParams.scaleFactor
val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor
val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor
return NormalizedTouchData(
pointerId = getPointerId(pointerIndex),
x = nativeX,
y = nativeY,
minor = nativeMinor,
major = nativeMajor,
// TODO(b/259311354): touch orientation should be reported relative to Surface.ROTATION_O.
orientation = getOrientation(pointerIndex),
time = eventTime,
gestureStart = downTime,
)
}
/**
* Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device
* is in the [Surface.ROTATION_0] orientation.
*/
private fun MotionEvent.rotateToNaturalOrientation(
pointerIndex: Int,
overlayParams: UdfpsOverlayParams
): PointF {
val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex))
val rot = overlayParams.rotation
if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
RotationUtils.rotatePointF(
touchPoint,
RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
overlayParams.logicalDisplayWidth.toFloat(),
overlayParams.logicalDisplayHeight.toFloat()
)
}
return touchPoint
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2022 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.udfps
import android.view.MotionEvent
import com.android.systemui.biometrics.UdfpsOverlayParams
/**
* Determines whether a finger entered or left the area of the under-display fingerprint sensor
* (UDFPS). Maps the touch information from a [MotionEvent] to the orientation and scale independent
* [NormalizedTouchData].
*/
interface TouchProcessor {
/**
* [event] touch event to be processed.
*
* [previousPointerOnSensorId] pointerId for the finger that was on the sensor prior to this
* event. See [MotionEvent.getPointerId]. If there was no finger on the sensor, this should be
* set to [MotionEvent.INVALID_POINTER_ID].
*
* [overlayParams] contains the location and dimensions of the sensor area, as well as the scale
* factor and orientation of the overlay. See [UdfpsOverlayParams].
*
* Returns [TouchProcessorResult.ProcessedTouch] on success, and [TouchProcessorResult.Failure]
* on failure.
*/
fun processTouch(
event: MotionEvent,
previousPointerOnSensorId: Int,
overlayParams: UdfpsOverlayParams,
): TouchProcessorResult
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2022 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.udfps
import android.view.MotionEvent
/** Contains all the possible returns types for [TouchProcessor.processTouch] */
sealed class TouchProcessorResult {
/**
* [event] whether a finger entered or left the sensor area. See [InteractionEvent].
*
* [pointerOnSensorId] pointerId fof the finger that's currently on the sensor. See
* [MotionEvent.getPointerId]. If there is no finger on the sensor, the value is set to
* [MotionEvent.INVALID_POINTER_ID].
*
* [touchData] relevant data from the MotionEvent, mapped to natural orientation and native
* resolution. See [NormalizedTouchData].
*/
data class ProcessedTouch(
val event: InteractionEvent,
val pointerOnSensorId: Int,
val touchData: NormalizedTouchData
) : TouchProcessorResult()
/** [reason] the reason for the failure. */
data class Failure(val reason: String = "") : TouchProcessorResult()
}

View File

@@ -0,0 +1,92 @@
package com.android.systemui.biometrics.udfps
import android.graphics.Rect
import androidx.test.filters.SmallTest
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
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
@SmallTest
@RunWith(Parameterized::class)
class NormalizedTouchDataTest(val testCase: TestCase) : SysuiTestCase() {
@Test
fun isWithinSensor() {
val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat())
val actual = touchData.isWithinSensor(OVERLAY_PARAMS)
assertThat(actual).isEqualTo(testCase.expected)
}
data class TestCase(val x: Int, val y: Int, val expected: Boolean)
companion object {
@Parameters(name = "{0}")
@JvmStatic
fun data(): List<TestCase> =
listOf(
genPositiveTestCases(
validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()),
validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY())
),
genNegativeTestCases(
invalidXs = listOf(SENSOR.left - 1, SENSOR.right + 1),
invalidYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1),
validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()),
validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY())
)
)
.flatten()
}
}
/* Placeholder touch parameters. */
private const val POINTER_ID = 42
private const val NATIVE_MINOR = 2.71828f
private const val NATIVE_MAJOR = 3.14f
private const val ORIENTATION = 1.23f
private const val TIME = 12345699L
private const val GESTURE_START = 12345600L
/* Template [NormalizedTouchData]. */
private val TOUCH_DATA =
NormalizedTouchData(
POINTER_ID,
x = 0f,
y = 0f,
NATIVE_MINOR,
NATIVE_MAJOR,
ORIENTATION,
TIME,
GESTURE_START
)
private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 500 /* bottom */)
private val OVERLAY_PARAMS = UdfpsOverlayParams(sensorBounds = SENSOR)
private fun genTestCases(
xs: List<Int>,
ys: List<Int>,
expected: Boolean
): List<NormalizedTouchDataTest.TestCase> {
return xs.flatMap { x -> ys.map { y -> NormalizedTouchDataTest.TestCase(x, y, expected) } }
}
private fun genPositiveTestCases(
validXs: List<Int>,
validYs: List<Int>,
) = genTestCases(validXs, validYs, expected = true)
private fun genNegativeTestCases(
invalidXs: List<Int>,
invalidYs: List<Int>,
validXs: List<Int>,
validYs: List<Int>,
): List<NormalizedTouchDataTest.TestCase> {
return genTestCases(invalidXs, validYs, expected = false) +
genTestCases(validXs, invalidYs, expected = false)
}

View File

@@ -0,0 +1,500 @@
/*
* Copyright (C) 2022 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.udfps
import android.graphics.Rect
import android.view.MotionEvent
import android.view.MotionEvent.INVALID_POINTER_ID
import android.view.MotionEvent.PointerProperties
import android.view.Surface
import android.view.Surface.Rotation
import androidx.test.filters.SmallTest
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
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
@SmallTest
@RunWith(Parameterized::class)
class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() {
private val underTest = SinglePointerTouchProcessor()
@Test
fun processTouch() {
val actual =
underTest.processTouch(
testCase.event,
testCase.previousPointerOnSensorId,
testCase.overlayParams,
)
assertThat(actual).isInstanceOf(testCase.expected.javaClass)
if (actual is TouchProcessorResult.ProcessedTouch) {
assertThat(actual).isEqualTo(testCase.expected)
}
}
data class TestCase(
val event: MotionEvent,
val previousPointerOnSensorId: Int,
val overlayParams: UdfpsOverlayParams,
val expected: TouchProcessorResult,
) {
override fun toString(): String {
val expectedOutput =
if (expected is TouchProcessorResult.ProcessedTouch) {
expected.event.toString() +
", (x: ${expected.touchData.x}, y: ${expected.touchData.y})" +
", pointerOnSensorId: ${expected.pointerOnSensorId}" +
", ..."
} else {
TouchProcessorResult.Failure().toString()
}
return "{" +
MotionEvent.actionToString(event.action) +
", (x: ${event.x}, y: ${event.y})" +
", scale: ${overlayParams.scaleFactor}" +
", rotation: " +
Surface.rotationToString(overlayParams.rotation) +
", previousPointerOnSensorId: $previousPointerOnSensorId" +
", ...} expected: {$expectedOutput}"
}
}
companion object {
@Parameters(name = "{0}")
@JvmStatic
fun data(): List<TestCase> =
listOf(
// MotionEvent.ACTION_DOWN
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_DOWN,
previousPointerOnSensorId = INVALID_POINTER_ID,
isWithinSensor = true,
expectedInteractionEvent = InteractionEvent.DOWN,
expectedPointerOnSensorId = POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_DOWN,
previousPointerOnSensorId = POINTER_ID,
isWithinSensor = true,
expectedInteractionEvent = InteractionEvent.DOWN,
expectedPointerOnSensorId = POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_DOWN,
previousPointerOnSensorId = INVALID_POINTER_ID,
isWithinSensor = false,
expectedInteractionEvent = InteractionEvent.UNCHANGED,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_DOWN,
previousPointerOnSensorId = POINTER_ID,
isWithinSensor = false,
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
// MotionEvent.ACTION_MOVE
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_MOVE,
previousPointerOnSensorId = INVALID_POINTER_ID,
isWithinSensor = true,
expectedInteractionEvent = InteractionEvent.DOWN,
expectedPointerOnSensorId = POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_MOVE,
previousPointerOnSensorId = POINTER_ID,
isWithinSensor = true,
expectedInteractionEvent = InteractionEvent.UNCHANGED,
expectedPointerOnSensorId = POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_MOVE,
previousPointerOnSensorId = INVALID_POINTER_ID,
isWithinSensor = false,
expectedInteractionEvent = InteractionEvent.UNCHANGED,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_MOVE,
previousPointerOnSensorId = POINTER_ID,
isWithinSensor = false,
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
// MotionEvent.ACTION_UP
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_UP,
previousPointerOnSensorId = INVALID_POINTER_ID,
isWithinSensor = true,
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_UP,
previousPointerOnSensorId = POINTER_ID,
isWithinSensor = true,
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_UP,
previousPointerOnSensorId = INVALID_POINTER_ID,
isWithinSensor = false,
expectedInteractionEvent = InteractionEvent.UNCHANGED,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_UP,
previousPointerOnSensorId = POINTER_ID,
isWithinSensor = false,
expectedInteractionEvent = InteractionEvent.UP,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
// MotionEvent.ACTION_CANCEL
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_CANCEL,
previousPointerOnSensorId = INVALID_POINTER_ID,
isWithinSensor = true,
expectedInteractionEvent = InteractionEvent.CANCEL,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_CANCEL,
previousPointerOnSensorId = POINTER_ID,
isWithinSensor = true,
expectedInteractionEvent = InteractionEvent.CANCEL,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_CANCEL,
previousPointerOnSensorId = INVALID_POINTER_ID,
isWithinSensor = false,
expectedInteractionEvent = InteractionEvent.CANCEL,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
genPositiveTestCases(
motionEventAction = MotionEvent.ACTION_CANCEL,
previousPointerOnSensorId = POINTER_ID,
isWithinSensor = false,
expectedInteractionEvent = InteractionEvent.CANCEL,
expectedPointerOnSensorId = INVALID_POINTER_ID,
),
)
.flatten() +
listOf(
// Unsupported MotionEvent actions.
genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_DOWN),
genTestCasesForUnsupportedAction(MotionEvent.ACTION_POINTER_UP),
genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_ENTER),
genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_MOVE),
genTestCasesForUnsupportedAction(MotionEvent.ACTION_HOVER_EXIT),
)
.flatten()
}
}
/* Display dimensions in native resolution and natural orientation. */
private const val ROTATION_0_NATIVE_DISPLAY_WIDTH = 400
private const val ROTATION_0_NATIVE_DISPLAY_HEIGHT = 600
/*
* ROTATION_0 map:
* _ _ _ _
* _ _ O _
* _ _ _ _
* _ S _ _
* _ S _ _
* _ _ _ _
*
* (_) empty space
* (S) sensor
* (O) touch outside of the sensor
*/
private val ROTATION_0_NATIVE_SENSOR_BOUNDS =
Rect(
100, /* left */
300, /* top */
200, /* right */
500, /* bottom */
)
private val ROTATION_0_INPUTS =
OrientationBasedInputs(
rotation = Surface.ROTATION_0,
nativeXWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterX(),
nativeYWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterY(),
nativeXOutsideSensor = 250f,
nativeYOutsideSensor = 150f,
)
/*
* ROTATION_90 map:
* _ _ _ _ _ _
* _ O _ _ _ _
* _ _ _ S S _
* _ _ _ _ _ _
*
* (_) empty space
* (S) sensor
* (O) touch outside of the sensor
*/
private val ROTATION_90_NATIVE_SENSOR_BOUNDS =
Rect(
300, /* left */
200, /* top */
500, /* right */
300, /* bottom */
)
private val ROTATION_90_INPUTS =
OrientationBasedInputs(
rotation = Surface.ROTATION_90,
nativeXWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterX(),
nativeYWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterY(),
nativeXOutsideSensor = 150f,
nativeYOutsideSensor = 150f,
)
/* ROTATION_180 is not supported. It's treated the same as ROTATION_0. */
private val ROTATION_180_INPUTS =
ROTATION_0_INPUTS.copy(
rotation = Surface.ROTATION_180,
)
/*
* ROTATION_270 map:
* _ _ _ _ _ _
* _ S S _ _ _
* _ _ _ _ O _
* _ _ _ _ _ _
*
* (_) empty space
* (S) sensor
* (O) touch outside of the sensor
*/
private val ROTATION_270_NATIVE_SENSOR_BOUNDS =
Rect(
100, /* left */
100, /* top */
300, /* right */
200, /* bottom */
)
private val ROTATION_270_INPUTS =
OrientationBasedInputs(
rotation = Surface.ROTATION_270,
nativeXWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterX(),
nativeYWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterY(),
nativeXOutsideSensor = 450f,
nativeYOutsideSensor = 250f,
)
/* Placeholder touch parameters. */
private const val POINTER_ID = 42
private const val NATIVE_MINOR = 2.71828f
private const val NATIVE_MAJOR = 3.14f
private const val ORIENTATION = 1.23f
private const val TIME = 12345699L
private const val GESTURE_START = 12345600L
/* Template [MotionEvent]. */
private val MOTION_EVENT =
obtainMotionEvent(
action = 0,
pointerId = POINTER_ID,
x = 0f,
y = 0f,
minor = 0f,
major = 0f,
orientation = ORIENTATION,
time = TIME,
gestureStart = GESTURE_START,
)
/* Template [NormalizedTouchData]. */
private val NORMALIZED_TOUCH_DATA =
NormalizedTouchData(
POINTER_ID,
x = 0f,
y = 0f,
NATIVE_MINOR,
NATIVE_MAJOR,
ORIENTATION,
TIME,
GESTURE_START
)
/*
* Contains test inputs that are tied to a particular device orientation.
*
* "native" means in native resolution (not scaled).
*/
private data class OrientationBasedInputs(
@Rotation val rotation: Int,
val nativeXWithinSensor: Float,
val nativeYWithinSensor: Float,
val nativeXOutsideSensor: Float,
val nativeYOutsideSensor: Float,
) {
fun toOverlayParams(scaleFactor: Float): UdfpsOverlayParams =
UdfpsOverlayParams(
sensorBounds = ROTATION_0_NATIVE_SENSOR_BOUNDS.scaled(scaleFactor),
overlayBounds = ROTATION_0_NATIVE_SENSOR_BOUNDS.scaled(scaleFactor),
naturalDisplayHeight = (ROTATION_0_NATIVE_DISPLAY_HEIGHT * scaleFactor).toInt(),
naturalDisplayWidth = (ROTATION_0_NATIVE_DISPLAY_WIDTH * scaleFactor).toInt(),
scaleFactor = scaleFactor,
rotation = rotation
)
fun getNativeX(isWithinSensor: Boolean): Float {
return if (isWithinSensor) nativeXWithinSensor else nativeXOutsideSensor
}
fun getNativeY(isWithinSensor: Boolean): Float {
return if (isWithinSensor) nativeYWithinSensor else nativeYOutsideSensor
}
}
private fun genPositiveTestCases(
motionEventAction: Int,
previousPointerOnSensorId: Int,
isWithinSensor: Boolean,
expectedInteractionEvent: InteractionEvent,
expectedPointerOnSensorId: Int
): List<SinglePointerTouchProcessorTest.TestCase> {
val scaleFactors = listOf(0.75f, 1f, 1.5f)
val orientations =
listOf(
ROTATION_0_INPUTS,
ROTATION_90_INPUTS,
ROTATION_180_INPUTS,
ROTATION_270_INPUTS,
)
return scaleFactors.flatMap { scaleFactor ->
orientations.map { orientation ->
val overlayParams = orientation.toOverlayParams(scaleFactor)
val nativeX = orientation.getNativeX(isWithinSensor)
val nativeY = orientation.getNativeY(isWithinSensor)
val event =
MOTION_EVENT.copy(
action = motionEventAction,
x = nativeX * scaleFactor,
y = nativeY * scaleFactor,
minor = NATIVE_MINOR * scaleFactor,
major = NATIVE_MAJOR * scaleFactor,
)
val expectedTouchData =
NORMALIZED_TOUCH_DATA.copy(
x = ROTATION_0_INPUTS.getNativeX(isWithinSensor),
y = ROTATION_0_INPUTS.getNativeY(isWithinSensor),
)
val expected =
TouchProcessorResult.ProcessedTouch(
event = expectedInteractionEvent,
pointerOnSensorId = expectedPointerOnSensorId,
touchData = expectedTouchData,
)
SinglePointerTouchProcessorTest.TestCase(
event = event,
previousPointerOnSensorId = previousPointerOnSensorId,
overlayParams = overlayParams,
expected = expected,
)
}
}
}
private fun genTestCasesForUnsupportedAction(
motionEventAction: Int
): List<SinglePointerTouchProcessorTest.TestCase> {
val isWithinSensor = true
val previousPointerOnSensorIds = listOf(INVALID_POINTER_ID, POINTER_ID)
return previousPointerOnSensorIds.map { previousPointerOnSensorId ->
val overlayParams = ROTATION_0_INPUTS.toOverlayParams(scaleFactor = 1f)
val nativeX = ROTATION_0_INPUTS.getNativeX(isWithinSensor)
val nativeY = ROTATION_0_INPUTS.getNativeY(isWithinSensor)
val event =
MOTION_EVENT.copy(
action = motionEventAction,
x = nativeX,
y = nativeY,
minor = NATIVE_MINOR,
major = NATIVE_MAJOR,
)
SinglePointerTouchProcessorTest.TestCase(
event = event,
previousPointerOnSensorId = previousPointerOnSensorId,
overlayParams = overlayParams,
expected = TouchProcessorResult.Failure(),
)
}
}
private fun obtainMotionEvent(
action: Int,
pointerId: Int,
x: Float,
y: Float,
minor: Float,
major: Float,
orientation: Float,
time: Long,
gestureStart: Long,
): MotionEvent {
val pp = PointerProperties()
pp.id = pointerId
val pc = MotionEvent.PointerCoords()
pc.x = x
pc.y = y
pc.touchMinor = minor
pc.touchMajor = major
pc.orientation = orientation
return MotionEvent.obtain(
gestureStart /* downTime */,
time /* eventTime */,
action /* action */,
1 /* pointerCount */,
arrayOf(pp) /* pointerProperties */,
arrayOf(pc) /* pointerCoords */,
0 /* metaState */,
0 /* buttonState */,
1f /* xPrecision */,
1f /* yPrecision */,
0 /* deviceId */,
0 /* edgeFlags */,
0 /* source */,
0 /* flags */
)
}
private fun MotionEvent.copy(
action: Int = this.action,
pointerId: Int = this.getPointerId(0),
x: Float = this.rawX,
y: Float = this.rawY,
minor: Float = this.touchMinor,
major: Float = this.touchMajor,
orientation: Float = this.orientation,
time: Long = this.eventTime,
gestureStart: Long = this.downTime,
) = obtainMotionEvent(action, pointerId, x, y, minor, major, orientation, time, gestureStart)
private fun Rect.scaled(scaleFactor: Float) = Rect(this).apply { scale(scaleFactor) }