Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ea6cdd17 authored by Ilya Matyukhin's avatar Ilya Matyukhin
Browse files

Introduce testable UDFPS touch architecture

Bug: 218388821
Bug: 218374828
Test: atest SinglePointerUdfpsTouchProcessorTest
Change-Id: I5654550873f580db07d708e3de1a13e45f70b052
parent 3318c87c
Loading
Loading
Loading
Loading
+50 −0
Original line number Diff line number Diff line
/*
 * 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,
}
+64 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+168 −0
Original line number Diff line number Diff line
/*
 * 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
}
+47 −0
Original line number Diff line number Diff line
/*
 * 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
}
+42 −0
Original line number Diff line number Diff line
/*
 * 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()
}
Loading