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