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