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