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