1 /*
2  * Copyright (C) 2023 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;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Point;
22 import android.util.DisplayUtils;
23 import android.util.Log;
24 import android.util.RotationUtils;
25 import android.view.Display;
26 import android.view.DisplayInfo;
27 import android.view.MotionEvent;
28 import android.view.Surface;
29 
30 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
31 import com.android.systemui.shared.biometrics.R;
32 
33 /** Utility class for working with udfps. */
34 public class UdfpsUtils {
35     private static final String TAG = "UdfpsUtils";
36 
37     /**
38      * Gets the scale factor representing the user's current resolution / the stable (default)
39      * resolution.
40      *
41      * @param displayInfo The display information.
42      */
getScaleFactor(DisplayInfo displayInfo)43     public float getScaleFactor(DisplayInfo displayInfo) {
44         Display.Mode maxDisplayMode =
45                 DisplayUtils.getMaximumResolutionDisplayMode(displayInfo.supportedModes);
46         float scaleFactor =
47                 DisplayUtils.getPhysicalPixelDisplaySizeRatio(
48                         maxDisplayMode.getPhysicalWidth(),
49                         maxDisplayMode.getPhysicalHeight(),
50                         displayInfo.getNaturalWidth(),
51                         displayInfo.getNaturalHeight()
52                 );
53         return (scaleFactor == Float.POSITIVE_INFINITY) ? 1f : scaleFactor;
54     }
55 
56     /**
57      * Gets the touch in native coordinates.
58      *
59      * Maps the touch to portrait mode if the device is in landscape mode.
60      *
61      * @param idx                The pointer identifier.
62      * @param event              The MotionEvent object containing full information about the event.
63      * @param udfpsOverlayParams The [UdfpsOverlayParams] used.
64      * @return The mapped touch event.
65      */
getTouchInNativeCoordinates(int idx, MotionEvent event, UdfpsOverlayParams udfpsOverlayParams)66     public Point getTouchInNativeCoordinates(int idx, MotionEvent event,
67             UdfpsOverlayParams udfpsOverlayParams) {
68         return getTouchInNativeCoordinates(idx, event, udfpsOverlayParams, true);
69     }
70 
71     /**
72      * Gets the touch in native coordinates.
73      *
74      * Optionally map the touch to portrait mode if the device is in landscape mode.
75      *
76      * @param idx                The pointer identifier.
77      * @param event              The MotionEvent object containing full information about the event.
78      * @param udfpsOverlayParams The [UdfpsOverlayParams] used.
79      * @param rotateToPortrait   Whether to rotate the touch to portrait orientation.
80      * @return The mapped touch event.
81      */
getTouchInNativeCoordinates(int idx, MotionEvent event, UdfpsOverlayParams udfpsOverlayParams, boolean rotateToPortrait)82     public Point getTouchInNativeCoordinates(int idx, MotionEvent event,
83             UdfpsOverlayParams udfpsOverlayParams, boolean rotateToPortrait) {
84         Point touch;
85         if (rotateToPortrait) {
86             touch = getPortraitTouch(idx, event, udfpsOverlayParams);
87         } else {
88             touch = new Point((int) event.getRawX(idx), (int) event.getRawY(idx));
89         }
90 
91         // Scale the coordinates to native resolution.
92         float scale = udfpsOverlayParams.getScaleFactor();
93         touch.x = (int) (touch.x / scale);
94         touch.y = (int) (touch.y / scale);
95         return touch;
96     }
97 
98     /**
99      * @param idx                The pointer identifier.
100      * @param event              The MotionEvent object containing full information about the event.
101      * @param udfpsOverlayParams The [UdfpsOverlayParams] used.
102      * @return Whether the touch event (that needs to be rotated to portrait) is within sensor area.
103      */
isWithinSensorArea(int idx, MotionEvent event, UdfpsOverlayParams udfpsOverlayParams)104     public boolean isWithinSensorArea(int idx, MotionEvent event,
105             UdfpsOverlayParams udfpsOverlayParams) {
106         return isWithinSensorArea(idx, event, udfpsOverlayParams, true);
107     }
108 
109     /**
110      * @param idx                The pointer identifier.
111      * @param event              The MotionEvent object containing full information about the event.
112      * @param udfpsOverlayParams The [UdfpsOverlayParams] used.
113      * @param rotateTouchToPortrait Whether to rotate the touch coordinates to portrait.
114      * @return Whether the touch event is within sensor area.
115      */
isWithinSensorArea(int idx, MotionEvent event, UdfpsOverlayParams udfpsOverlayParams, boolean rotateTouchToPortrait)116     public boolean isWithinSensorArea(int idx, MotionEvent event,
117             UdfpsOverlayParams udfpsOverlayParams, boolean rotateTouchToPortrait) {
118         Point touch;
119         if (rotateTouchToPortrait) {
120             touch = getPortraitTouch(idx, event, udfpsOverlayParams);
121         } else {
122             touch = new Point((int) event.getRawX(idx), (int) event.getRawY(idx));
123         }
124         return udfpsOverlayParams.getSensorBounds().contains(touch.x, touch.y);
125     }
126 
127     /**
128      * This function computes the angle of touch relative to the sensor, rotated to portrait,
129      * and maps the angle to a list of help messages which are announced if accessibility is
130      * enabled.
131      *
132      * @return announcement string
133      */
onTouchOutsideOfSensorArea(boolean touchExplorationEnabled, Context context, int scaledTouchX, int scaledTouchY, UdfpsOverlayParams udfpsOverlayParams)134     public String onTouchOutsideOfSensorArea(boolean touchExplorationEnabled, Context context,
135             int scaledTouchX, int scaledTouchY, UdfpsOverlayParams udfpsOverlayParams) {
136         return onTouchOutsideOfSensorArea(touchExplorationEnabled, context, scaledTouchX,
137                 scaledTouchY, udfpsOverlayParams, true);
138     }
139 
140     /**
141      * This function computes the angle of touch relative to the sensor and maps the angle to a list
142      * of help messages which are announced if accessibility is enabled.
143      *
144      * @return announcement string
145      */
onTouchOutsideOfSensorArea(boolean touchExplorationEnabled, Context context, int scaledTouchX, int scaledTouchY, UdfpsOverlayParams udfpsOverlayParams, boolean touchRotatedToPortrait)146     public String onTouchOutsideOfSensorArea(boolean touchExplorationEnabled, Context context,
147             int scaledTouchX, int scaledTouchY, UdfpsOverlayParams udfpsOverlayParams,
148             boolean touchRotatedToPortrait) {
149         if (!touchExplorationEnabled) {
150             return null;
151         }
152 
153         Resources resources = context.getResources();
154         String[] touchHints = new String[] {
155                 resources.getString(R.string.udfps_accessibility_touch_hints_left),
156                 resources.getString(R.string.udfps_accessibility_touch_hints_down),
157                 resources.getString(R.string.udfps_accessibility_touch_hints_right),
158                 resources.getString(R.string.udfps_accessibility_touch_hints_up),
159         };
160 
161         // Scale the coordinates to native resolution.
162         float scale = udfpsOverlayParams.getScaleFactor();
163         float scaledSensorX = udfpsOverlayParams.getSensorBounds().centerX() / scale;
164         float scaledSensorY = udfpsOverlayParams.getSensorBounds().centerY() / scale;
165         String theStr =
166                 onTouchOutsideOfSensorAreaImpl(
167                         touchHints,
168                         scaledTouchX,
169                         scaledTouchY,
170                         scaledSensorX,
171                         scaledSensorY,
172                         udfpsOverlayParams.getRotation(),
173                         touchRotatedToPortrait
174                 );
175         Log.v(TAG, "Announcing touch outside : $theStr");
176         return theStr;
177     }
178 
179     /**
180      * This function computes the angle of touch relative to the sensor and maps the angle to a list
181      * of help messages which are announced if accessibility is enabled.
182      *
183      * There are 4 quadrants of the circle (90 degree arcs)
184      *
185      * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left" [45, 135) ->
186      * touchHints[1] = "Move Fingerprint down" And so on.
187      */
onTouchOutsideOfSensorAreaImpl(String[] touchHints, float touchX, float touchY, float sensorX, float sensorY, int rotation, boolean rotatedToPortrait)188     private String onTouchOutsideOfSensorAreaImpl(String[] touchHints, float touchX,
189             float touchY, float sensorX, float sensorY, int rotation, boolean rotatedToPortrait) {
190         float xRelativeToSensor = touchX - sensorX;
191         // Touch coordinates are with respect to the upper left corner, so reverse
192         // this calculation
193         float yRelativeToSensor = sensorY - touchY;
194         double angleInRad = Math.atan2(yRelativeToSensor, xRelativeToSensor);
195         // If the radians are negative, that means we are counting clockwise.
196         // So we need to add 360 degrees
197         if (angleInRad < 0.0) {
198             angleInRad += 2.0 * Math.PI;
199         }
200         // rad to deg conversion
201         double degrees = Math.toDegrees(angleInRad);
202         double degreesPerBucket = 360.0 / touchHints.length;
203         double halfBucketDegrees = degreesPerBucket / 2.0;
204         // The mapping should be as follows
205         // [315, 360] && [0, 45] -> 0
206         // [45, 135]             -> 1
207         int index = (int) ((degrees + halfBucketDegrees) % 360 / degreesPerBucket);
208         index %= touchHints.length;
209 
210         if (rotatedToPortrait) {
211             // A rotation of 90 degrees corresponds to increasing the index by 1.
212             if (rotation == Surface.ROTATION_90) {
213                 index = (index + 1) % touchHints.length;
214             }
215             if (rotation == Surface.ROTATION_270) {
216                 index = (index + 3) % touchHints.length;
217             }
218         }
219 
220         return touchHints[index];
221     }
222 
223     /**
224      * Map the touch to portrait mode if the device is in landscape mode.
225      */
getPortraitTouch(int idx, MotionEvent event, UdfpsOverlayParams udfpsOverlayParams)226     private Point getPortraitTouch(int idx, MotionEvent event,
227             UdfpsOverlayParams udfpsOverlayParams) {
228         Point portraitTouch = new Point((int) event.getRawX(idx), (int) event.getRawY(idx));
229         int rot = udfpsOverlayParams.getRotation();
230         if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
231             RotationUtils.rotatePoint(
232                     portraitTouch,
233                     RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
234                     udfpsOverlayParams.getLogicalDisplayWidth(),
235                     udfpsOverlayParams.getLogicalDisplayHeight()
236             );
237         }
238         return portraitTouch;
239     }
240 }
241