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.settingslib.udfps.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 39 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 { 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 92 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 113 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 128 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 */ 136 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 161 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 */ 170 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