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.Point 20 import android.graphics.Rect 21 import android.util.Log 22 import com.android.systemui.biometrics.EllipseOverlapDetectorParams 23 import com.android.systemui.dagger.SysUISingleton 24 import kotlin.math.cos 25 import kotlin.math.sin 26 27 private enum class SensorPixelPosition { 28 OUTSIDE, // Pixel that falls outside of sensor circle 29 SENSOR, // Pixel within sensor circle 30 TARGET // Pixel within sensor center target 31 } 32 33 private const val isDebug = false 34 private const val TAG = "EllipseOverlapDetector" 35 36 /** 37 * Approximates the touch as an ellipse and determines whether the ellipse has a sufficient overlap 38 * with the sensor. 39 */ 40 @SysUISingleton 41 class EllipseOverlapDetector(private val params: EllipseOverlapDetectorParams) : OverlapDetector { isGoodOverlapnull42 override fun isGoodOverlap( 43 touchData: NormalizedTouchData, 44 nativeSensorBounds: Rect, 45 nativeOverlayBounds: Rect, 46 ): Boolean { 47 // First, check if touch is within bounding box to exit early 48 if (touchData.isWithinBounds(nativeSensorBounds)) { 49 return true 50 } 51 52 // Check touch is within overlay bounds, not worth checking if outside 53 if (!touchData.isWithinBounds(nativeOverlayBounds)) { 54 return false 55 } 56 57 var isTargetTouched = false 58 var sensorPixels = 0 59 var coveredPixels = 0 60 for (y in nativeSensorBounds.top..nativeSensorBounds.bottom step params.stepSize) { 61 for (x in nativeSensorBounds.left..nativeSensorBounds.right step params.stepSize) { 62 // Check where pixel is within the sensor TODO: (b/265836919) This could be improved 63 // by precomputing these points 64 val pixelPosition = 65 isPartOfSensorArea( 66 x, 67 y, 68 nativeSensorBounds.centerX(), 69 nativeSensorBounds.centerY(), 70 nativeSensorBounds.width() / 2 71 ) 72 if (pixelPosition != SensorPixelPosition.OUTSIDE) { 73 sensorPixels++ 74 75 // Check if this pixel falls within ellipse touch 76 if (checkPoint(Point(x, y), touchData)) { 77 coveredPixels++ 78 79 // Check that at least one covered pixel is within sensor target 80 isTargetTouched = 81 isTargetTouched or (pixelPosition == SensorPixelPosition.TARGET) 82 } 83 } 84 } 85 } 86 87 val percentage: Float = coveredPixels.toFloat() / sensorPixels 88 if (isDebug) { 89 Log.d( 90 TAG, 91 "covered: $coveredPixels, sensor: $sensorPixels, " + 92 "percentage: $percentage, isCenterTouched: $isTargetTouched" 93 ) 94 } 95 96 return percentage >= params.minOverlap && isTargetTouched 97 } 98 99 /** Checks if point is in the sensor center target circle, outer circle, or outside of sensor */ isPartOfSensorAreanull100 private fun isPartOfSensorArea(x: Int, y: Int, cX: Int, cY: Int, r: Int): SensorPixelPosition { 101 val dx = cX - x 102 val dy = cY - y 103 104 val disSquared = dx * dx + dy * dy 105 106 return if (disSquared <= (r * params.targetSize) * (r * params.targetSize)) { 107 SensorPixelPosition.TARGET 108 } else if (disSquared <= r * r) { 109 SensorPixelPosition.SENSOR 110 } else { 111 SensorPixelPosition.OUTSIDE 112 } 113 } 114 checkPointnull115 private fun checkPoint(point: Point, touchData: NormalizedTouchData): Boolean { 116 // Calculate if sensor point is within ellipse 117 // Formula: ((cos(o)(xE - xS) + sin(o)(yE - yS))^2 / a^2) + ((sin(o)(xE - xS) + cos(o)(yE - 118 // yS))^2 / b^2) <= 1 119 val a: Float = cos(touchData.orientation) * (point.x - touchData.x) 120 val b: Float = sin(touchData.orientation) * (point.y - touchData.y) 121 val c: Float = sin(touchData.orientation) * (point.x - touchData.x) 122 val d: Float = cos(touchData.orientation) * (point.y - touchData.y) 123 val result = 124 (a + b) * (a + b) / ((touchData.minor / 2) * (touchData.minor / 2)) + 125 (c - d) * (c - d) / ((touchData.major / 2) * (touchData.major / 2)) 126 127 return result <= 1 128 } 129 } 130