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.shared.model.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 =
31 setOf(Surface.ROTATION_90, Surface.ROTATION_270, Surface.ROTATION_180)
32
33 /**
34 * TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations.
35 */
36 @SysUISingleton
37 class SinglePointerTouchProcessor @Inject constructor(val overlapDetector: OverlapDetector) :
38 TouchProcessor {
39
processTouchnull40 override fun processTouch(
41 event: MotionEvent,
42 previousPointerOnSensorId: Int,
43 overlayParams: UdfpsOverlayParams,
44 ): TouchProcessorResult {
45 fun preprocess(): PreprocessedTouch {
46 val touchData = List(event.pointerCount) { event.normalize(it, overlayParams) }
47 val pointersOnSensor =
48 touchData
49 .filter {
50 overlapDetector.isGoodOverlap(
51 it,
52 overlayParams.nativeSensorBounds,
53 overlayParams.nativeOverlayBounds
54 )
55 }
56 .map { it.pointerId }
57 return PreprocessedTouch(touchData, previousPointerOnSensorId, pointersOnSensor)
58 }
59
60 return when (event.actionMasked) {
61 MotionEvent.ACTION_DOWN,
62 MotionEvent.ACTION_POINTER_DOWN,
63 MotionEvent.ACTION_MOVE,
64 MotionEvent.ACTION_HOVER_ENTER,
65 MotionEvent.ACTION_HOVER_MOVE -> processActionMove(preprocess())
66 MotionEvent.ACTION_UP,
67 MotionEvent.ACTION_POINTER_UP,
68 MotionEvent.ACTION_HOVER_EXIT ->
69 processActionUp(preprocess(), event.getPointerId(event.actionIndex))
70 MotionEvent.ACTION_CANCEL -> processActionCancel(NormalizedTouchData())
71 else ->
72 Failure("Unsupported MotionEvent." + MotionEvent.actionToString(event.actionMasked))
73 }
74 }
75 }
76
77 /**
78 * [data] contains a list of NormalizedTouchData for pointers in the motionEvent ordered by
79 * pointerIndex
80 *
81 * [previousPointerOnSensorId] the pointerId of the previous pointer on the sensor,
82 * [MotionEvent.INVALID_POINTER_ID] if none
83 *
84 * [pointersOnSensor] contains a list of ids of pointers on the sensor
85 */
86 private data class PreprocessedTouch(
87 val data: List<NormalizedTouchData>,
88 val previousPointerOnSensorId: Int,
89 val pointersOnSensor: List<Int>,
90 )
91
processActionMovenull92 private fun processActionMove(touch: PreprocessedTouch): TouchProcessorResult {
93 val hadPointerOnSensor = touch.previousPointerOnSensorId != INVALID_POINTER_ID
94 val hasPointerOnSensor = touch.pointersOnSensor.isNotEmpty()
95 val pointerOnSensorId = touch.pointersOnSensor.firstOrNull() ?: INVALID_POINTER_ID
96
97 return if (!hadPointerOnSensor && hasPointerOnSensor) {
98 val data = touch.data.find { it.pointerId == pointerOnSensorId } ?: NormalizedTouchData()
99 ProcessedTouch(InteractionEvent.DOWN, data.pointerId, data)
100 } else if (hadPointerOnSensor && !hasPointerOnSensor) {
101 val data =
102 touch.data.find { it.pointerId == touch.previousPointerOnSensorId }
103 ?: NormalizedTouchData()
104 ProcessedTouch(InteractionEvent.UP, INVALID_POINTER_ID, data)
105 } else {
106 val data =
107 touch.data.find { it.pointerId == pointerOnSensorId }
108 ?: touch.data.firstOrNull() ?: NormalizedTouchData()
109 ProcessedTouch(InteractionEvent.UNCHANGED, pointerOnSensorId, data)
110 }
111 }
112
processActionUpnull113 private fun processActionUp(touch: PreprocessedTouch, actionId: Int): TouchProcessorResult {
114 // Finger lifted and it was the only finger on the sensor
115 return if (touch.pointersOnSensor.size == 1 && touch.pointersOnSensor.contains(actionId)) {
116 val data = touch.data.find { it.pointerId == actionId } ?: NormalizedTouchData()
117 ProcessedTouch(InteractionEvent.UP, pointerOnSensorId = INVALID_POINTER_ID, data)
118 } else {
119 // Pick new pointerOnSensor that's not the finger that was lifted
120 val pointerOnSensorId = touch.pointersOnSensor.find { it != actionId } ?: INVALID_POINTER_ID
121 val data =
122 touch.data.find { it.pointerId == pointerOnSensorId }
123 ?: touch.data.firstOrNull() ?: NormalizedTouchData()
124 ProcessedTouch(InteractionEvent.UNCHANGED, pointerOnSensorId, data)
125 }
126 }
127
processActionCancelnull128 private fun processActionCancel(data: NormalizedTouchData): TouchProcessorResult {
129 return ProcessedTouch(InteractionEvent.CANCEL, pointerOnSensorId = INVALID_POINTER_ID, data)
130 }
131
132 /**
133 * Returns the touch information from the given [MotionEvent] with the relevant fields mapped to
134 * natural orientation and native resolution.
135 */
normalizenull136 private fun MotionEvent.normalize(
137 pointerIndex: Int,
138 overlayParams: UdfpsOverlayParams
139 ): NormalizedTouchData {
140 val naturalTouch: PointF = rotateToNaturalOrientation(pointerIndex, overlayParams)
141 val nativeX = naturalTouch.x / overlayParams.scaleFactor
142 val nativeY = naturalTouch.y / overlayParams.scaleFactor
143 val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor
144 val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor
145 var nativeOrientation: Float = getOrientation(pointerIndex)
146 if (SUPPORTED_ROTATIONS.contains(overlayParams.rotation)) {
147 nativeOrientation = toRadVerticalFromRotated(nativeOrientation.toDouble()).toFloat()
148 }
149 return NormalizedTouchData(
150 pointerId = getPointerId(pointerIndex),
151 x = nativeX,
152 y = nativeY,
153 minor = nativeMinor,
154 major = nativeMajor,
155 orientation = nativeOrientation,
156 time = eventTime,
157 gestureStart = downTime,
158 )
159 }
160
toRadVerticalFromRotatednull161 private fun toRadVerticalFromRotated(rad: Double): Double {
162 val piBound = ((rad % Math.PI) + Math.PI / 2) % Math.PI
163 return if (piBound < Math.PI / 2.0) piBound else piBound - Math.PI
164 }
165
166 /**
167 * Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device
168 * is in the [Surface.ROTATION_0] orientation.
169 */
rotateToNaturalOrientationnull170 private fun MotionEvent.rotateToNaturalOrientation(
171 pointerIndex: Int,
172 overlayParams: UdfpsOverlayParams
173 ): PointF {
174 val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex))
175 val rot = overlayParams.rotation
176 if (SUPPORTED_ROTATIONS.contains(rot)) {
177 RotationUtils.rotatePointF(
178 touchPoint,
179 RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
180 overlayParams.logicalDisplayWidth.toFloat(),
181 overlayParams.logicalDisplayHeight.toFloat()
182 )
183 }
184 return touchPoint
185 }
186