Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4119ff45 authored by Hao Dong's avatar Hao Dong Committed by Android (Google) Code Review
Browse files

Merge "Add udfps/ and support a11y for the udfps enroll view."

parents 0217352c a8872ef3
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -1633,4 +1633,12 @@
    <string name="back_navigation_animation_summary">Enable system animations for predictive back.</string>
    <!-- Developer setting: enable animations when a back gesture is executed, full explanation[CHAR LIMIT=NONE] -->
    <string name="back_navigation_animation_dialog">This setting enables system animations for predictive gesture animation. It requires setting per-app "enableOnBackInvokedCallback" to true in the manifest file.</string>

    <!-- [CHAR LIMIT=NONE] Messages shown when users press outside of udfps region during -->
    <string-array name="udfps_accessibility_touch_hints">
        <item>Move left</item>
        <item>Move down</item>
        <item>Move right</item>
        <item>Move up</item>
    </string-array>
</resources>
+1 −1
Original line number Diff line number Diff line
package com.android.systemui.biometrics
package com.android.settingslib.udfps

import android.graphics.Rect
import android.view.Surface
+159 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.udfps;

import android.content.Context;
import android.graphics.Point;
import android.util.DisplayUtils;
import android.util.Log;
import android.util.RotationUtils;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.MotionEvent;
import android.view.Surface;

import com.android.settingslib.R;

/** Utility class for working with udfps. */
public class UdfpsUtils {
    private static final String TAG = "UdfpsUtils";

    /**
     * Gets the scale factor representing the user's current resolution / the stable (default)
     * resolution.
     *
     * @param displayInfo The display information.
     */
    public float getScaleFactor(DisplayInfo displayInfo) {
        Display.Mode maxDisplayMode =
                DisplayUtils.getMaximumResolutionDisplayMode(displayInfo.supportedModes);
        float scaleFactor =
                DisplayUtils.getPhysicalPixelDisplaySizeRatio(
                        maxDisplayMode.getPhysicalWidth(),
                        maxDisplayMode.getPhysicalHeight(),
                        displayInfo.getNaturalWidth(),
                        displayInfo.getNaturalHeight()
                );
        return (scaleFactor == Float.POSITIVE_INFINITY) ? 1f : scaleFactor;
    }

    /**
     * Gets the touch in native coordinates. Map the touch to portrait mode if the device is in
     * landscape mode.
     *
     * @param idx                The pointer identifier.
     * @param event              The MotionEvent object containing full information about the event.
     * @param udfpsOverlayParams The [UdfpsOverlayParams] used.
     * @return The mapped touch event.
     */
    public Point getTouchInNativeCoordinates(int idx, MotionEvent event,
            UdfpsOverlayParams udfpsOverlayParams) {
        Point portraitTouch = new Point((int) event.getRawX(idx), (int) event.getRawY(idx));
        int rot = udfpsOverlayParams.getRotation();
        if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
            RotationUtils.rotatePoint(
                    portraitTouch,
                    RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
                    udfpsOverlayParams.getLogicalDisplayWidth(),
                    udfpsOverlayParams.getLogicalDisplayHeight()
            );
        }

        // Scale the coordinates to native resolution.
        float scale = udfpsOverlayParams.getScaleFactor();
        portraitTouch.x = (int) (portraitTouch.x / scale);
        portraitTouch.y = (int) (portraitTouch.y / scale);
        return portraitTouch;
    }

    /**
     * This function computes the angle of touch relative to the sensor and maps the angle to a list
     * of help messages which are announced if accessibility is enabled.
     *
     * @return Whether the announcing string is null
     */
    public String onTouchOutsideOfSensorArea(boolean touchExplorationEnabled,
            Context context, int touchX, int touchY, UdfpsOverlayParams udfpsOverlayParams) {
        if (!touchExplorationEnabled) {
            return null;
        }

        String[] touchHints = context.getResources().getStringArray(
                R.array.udfps_accessibility_touch_hints);
        if (touchHints.length != 4) {
            Log.e(TAG, "expected exactly 4 touch hints, got " + touchHints.length + "?");
            return null;
        }

        // Scale the coordinates to native resolution.
        float scale = udfpsOverlayParams.getScaleFactor();
        float scaledSensorX = udfpsOverlayParams.getSensorBounds().centerX() / scale;
        float scaledSensorY = udfpsOverlayParams.getSensorBounds().centerY() / scale;
        String theStr =
                onTouchOutsideOfSensorAreaImpl(
                        touchHints,
                        touchX,
                        touchY,
                        scaledSensorX,
                        scaledSensorY,
                        udfpsOverlayParams.getRotation()
                );
        Log.v(TAG, "Announcing touch outside : $theStr");
        return theStr;
    }

    /**
     * This function computes the angle of touch relative to the sensor and maps the angle to a list
     * of help messages which are announced if accessibility is enabled.
     *
     * There are 4 quadrants of the circle (90 degree arcs)
     *
     * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left" [45, 135) ->
     * touchHints[1] = "Move Fingerprint down" And so on.
     */
    private String onTouchOutsideOfSensorAreaImpl(String[] touchHints, float touchX,
            float touchY, float sensorX, float sensorY, int rotation) {
        float xRelativeToSensor = touchX - sensorX;
        // Touch coordinates are with respect to the upper left corner, so reverse
        // this calculation
        float yRelativeToSensor = sensorY - touchY;
        var angleInRad = Math.atan2(yRelativeToSensor, xRelativeToSensor);
        // If the radians are negative, that means we are counting clockwise.
        // So we need to add 360 degrees
        if (angleInRad < 0.0) {
            angleInRad += 2.0 * Math.PI;
        }
        // rad to deg conversion
        double degrees = Math.toDegrees(angleInRad);
        double degreesPerBucket = 360.0 / touchHints.length;
        double halfBucketDegrees = degreesPerBucket / 2.0;
        // The mapping should be as follows
        // [315, 360] && [0, 45] -> 0
        // [45, 135]             -> 1
        int index = (int) ((degrees + halfBucketDegrees) % 360 / degreesPerBucket);
        index %= touchHints.length;

        // A rotation of 90 degrees corresponds to increasing the index by 1.
        if (rotation == Surface.ROTATION_90) {
            index = (index + 1) % touchHints.length;
        }
        if (rotation == Surface.ROTATION_270) {
            index = (index + 3) % touchHints.length;
        }
        return touchHints[index];
    }
}
+154 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.udfps;

import static com.google.common.truth.Truth.assertThat;

import android.content.Context;
import android.graphics.Rect;
import android.view.Surface;

import com.android.settingslib.R;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;


@RunWith(RobolectricTestRunner.class)
public class UdfpsUtilsTest {
    @Rule
    public final MockitoRule rule = MockitoJUnit.rule();

    private Context mContext;
    private String[] mTouchHints;
    private UdfpsUtils mUdfpsUtils;

    @Before
    public void setUp() {
        mContext = RuntimeEnvironment.application;
        mTouchHints = mContext.getResources().getStringArray(
                R.array.udfps_accessibility_touch_hints);
        mUdfpsUtils = new UdfpsUtils();
    }

    @Test
    public void testTouchOutsideAreaNoRotation() {
        int rotation = Surface.ROTATION_0;
        // touch at 0 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        0 /* touchX */, 0/* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[0]);
        // touch at 90 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        0 /* touchX */, -1/* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[1]);
        // touch at 180 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        -1 /* touchX */, 0/* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[2]);
        // touch at 270 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        0 /* touchX */, 1/* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[3]);
    }


    @Test
    public void testTouchOutsideAreaNoRotation90Degrees() {
        int rotation = Surface.ROTATION_90;
        // touch at 0 degrees -> 90 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        0 /* touchX */, 0 /* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[1]);
        // touch at 90 degrees -> 180 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        0 /* touchX */, -1 /* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[2]);
        // touch at 180 degrees -> 270 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        -1 /* touchX */, 0 /* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[3]);
        // touch at 270 degrees -> 0 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        0 /* touchX */, 1/* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[0]);
    }


    @Test
    public void testTouchOutsideAreaNoRotation270Degrees() {
        int rotation = Surface.ROTATION_270;
        // touch at 0 degrees -> 270 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        0 /* touchX */, 0/* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[3]);
        // touch at 90 degrees -> 0 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        0 /* touchX */, -1/* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[0]);
        // touch at 180 degrees -> 90 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        -1 /* touchX */, 0/* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[1]);
        // touch at 270 degrees -> 180 degrees
        assertThat(
                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
                        0 /* touchX */, 1/* touchY */,
                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
                )
        ).isEqualTo(mTouchHints[2]);
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -19,4 +19,12 @@
    android:id="@+id/udfps_animation_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- The layout height/width are placeholders, which will be overwritten by
     FingerprintSensorPropertiesInternal. -->
    <View
        android:id="@+id/udfps_enroll_accessibility_view"
        android:layout_gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:contentDescription="@string/accessibility_fingerprint_label"/>
</com.android.systemui.biometrics.UdfpsFpmEmptyView>
Loading