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