1 /*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.biometrics.udfps
18
19 import android.graphics.PointF
20 import android.util.RotationUtils
21 import android.view.MotionEvent
22 import android.view.MotionEvent.INVALID_POINTER_ID
23 import android.view.Surface
24 import com.android.systemui.biometrics.UdfpsOverlayParams
25 import com.android.systemui.biometrics.udfps.TouchProcessorResult.Failure
26 import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch
27 import com.android.systemui.dagger.SysUISingleton
28 import javax.inject.Inject
29
30 private val SUPPORTED_ROTATIONS = setOf(Surface.ROTATION_90, Surface.ROTATION_270)
31
32 /**
33 * TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations.
34 */
35 @SysUISingleton
36 class SinglePointerTouchProcessor @Inject constructor(val overlapDetector: OverlapDetector) :
37 TouchProcessor {
38
processTouchnull39 override fun processTouch(
40 event: MotionEvent,
41 previousPointerOnSensorId: Int,
42 overlayParams: UdfpsOverlayParams,
43 ): TouchProcessorResult {
44
45 fun preprocess(): PreprocessedTouch {
46 val touchData = List(event.pointerCount) { event.normalize(it, overlayParams) }
47 val pointersOnSensor =
48 touchData
49 .filter { overlapDetector.isGoodOverlap(it, overlayParams.nativeSensorBounds) }
50 .map { it.pointerId }
51 return PreprocessedTouch(touchData, previousPointerOnSensorId, pointersOnSensor)
52 }
53
54 return when (event.actionMasked) {
55 MotionEvent.ACTION_DOWN,
56 MotionEvent.ACTION_POINTER_DOWN,
57 MotionEvent.ACTION_MOVE,
58 MotionEvent.ACTION_HOVER_ENTER,
59 MotionEvent.ACTION_HOVER_MOVE -> processActionMove(preprocess())
60 MotionEvent.ACTION_UP,
61 MotionEvent.ACTION_POINTER_UP,
62 MotionEvent.ACTION_HOVER_EXIT ->
63 processActionUp(preprocess(), event.getPointerId(event.actionIndex))
64 MotionEvent.ACTION_CANCEL -> processActionCancel(NormalizedTouchData())
65 else ->
66 Failure("Unsupported MotionEvent." + MotionEvent.actionToString(event.actionMasked))
67 }
68 }
69 }
70
71 /**
72 * [data] contains a list of NormalizedTouchData for pointers in the motionEvent ordered by
73 * pointerIndex
74 *
75 * [previousPointerOnSensorId] the pointerId of the previous pointer on the sensor,
76 * [MotionEvent.INVALID_POINTER_ID] if none
77 *
78 * [pointersOnSensor] contains a list of ids of pointers on the sensor
79 */
80 private data class PreprocessedTouch(
81 val data: List<NormalizedTouchData>,
82 val previousPointerOnSensorId: Int,
83 val pointersOnSensor: List<Int>,
84 )
85
processActionMovenull86 private fun processActionMove(touch: PreprocessedTouch): TouchProcessorResult {
87 val hadPointerOnSensor = touch.previousPointerOnSensorId != INVALID_POINTER_ID
88 val hasPointerOnSensor = touch.pointersOnSensor.isNotEmpty()
89 val pointerOnSensorId = touch.pointersOnSensor.firstOrNull() ?: INVALID_POINTER_ID
90
91 return if (!hadPointerOnSensor && hasPointerOnSensor) {
92 val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
93 ProcessedTouch(InteractionEvent.DOWN, data.pointerId, data)
94 } else if (hadPointerOnSensor && !hasPointerOnSensor) {
95 ProcessedTouch(InteractionEvent.UP, INVALID_POINTER_ID, NormalizedTouchData())
96 } else {
97 val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
98 ProcessedTouch(InteractionEvent.UNCHANGED, pointerOnSensorId, data)
99 }
100 }
101
processActionUpnull102 private fun processActionUp(touch: PreprocessedTouch, actionId: Int): TouchProcessorResult {
103 // Finger lifted and it was the only finger on the sensor
104 return if (touch.pointersOnSensor.size == 1 && touch.pointersOnSensor.contains(actionId)) {
105 ProcessedTouch(
106 InteractionEvent.UP,
107 pointerOnSensorId = INVALID_POINTER_ID,
108 NormalizedTouchData()
109 )
110 } else {
111 // Pick new pointerOnSensor that's not the finger that was lifted
112 val pointerOnSensorId = touch.pointersOnSensor.find { it != actionId } ?: INVALID_POINTER_ID
113 val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
114 ProcessedTouch(InteractionEvent.UNCHANGED, data.pointerId, data)
115 }
116 }
117
processActionCancelnull118 private fun processActionCancel(data: NormalizedTouchData): TouchProcessorResult {
119 return ProcessedTouch(InteractionEvent.CANCEL, pointerOnSensorId = INVALID_POINTER_ID, data)
120 }
121
122 /**
123 * Returns the touch information from the given [MotionEvent] with the relevant fields mapped to
124 * natural orientation and native resolution.
125 */
normalizenull126 private fun MotionEvent.normalize(
127 pointerIndex: Int,
128 overlayParams: UdfpsOverlayParams
129 ): NormalizedTouchData {
130 val naturalTouch: PointF = rotateToNaturalOrientation(pointerIndex, overlayParams)
131 val nativeX = naturalTouch.x / overlayParams.scaleFactor
132 val nativeY = naturalTouch.y / overlayParams.scaleFactor
133 val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor
134 val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor
135 var nativeOrientation: Float = getOrientation(pointerIndex)
136 if (SUPPORTED_ROTATIONS.contains(overlayParams.rotation)) {
137 nativeOrientation = toRadVerticalFromRotated(nativeOrientation.toDouble()).toFloat()
138 }
139 return NormalizedTouchData(
140 pointerId = getPointerId(pointerIndex),
141 x = nativeX,
142 y = nativeY,
143 minor = nativeMinor,
144 major = nativeMajor,
145 orientation = nativeOrientation,
146 time = eventTime,
147 gestureStart = downTime,
148 )
149 }
150
toRadVerticalFromRotatednull151 private fun toRadVerticalFromRotated(rad: Double): Double {
152 val piBound = ((rad % Math.PI) + Math.PI / 2) % Math.PI
153 return if (piBound < Math.PI / 2.0) piBound else piBound - Math.PI
154 }
155
156 /**
157 * Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device
158 * is in the [Surface.ROTATION_0] orientation.
159 */
rotateToNaturalOrientationnull160 private fun MotionEvent.rotateToNaturalOrientation(
161 pointerIndex: Int,
162 overlayParams: UdfpsOverlayParams
163 ): PointF {
164 val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex))
165 val rot = overlayParams.rotation
166 if (SUPPORTED_ROTATIONS.contains(rot)) {
167 RotationUtils.rotatePointF(
168 touchPoint,
169 RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
170 overlayParams.logicalDisplayWidth.toFloat(),
171 overlayParams.logicalDisplayHeight.toFloat()
172 )
173 }
174 return touchPoint
175 }
176