Introduce testable UDFPS touch architecture
Bug: 218388821 Bug: 218374828 Test: atest SinglePointerUdfpsTouchProcessorTest Change-Id: I5654550873f580db07d708e3de1a13e45f70b052
This commit is contained in:
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) }
|
||||
Reference in New Issue
Block a user