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

Commit 25aa5570 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Make LockPatternView dot hit area circular." into tm-dev am: 68c8926a

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/16978906

Change-Id: If603d0c8c011d84468615190bfe86adf72b503d1
parents 4371bb50 68c8926a
Loading
Loading
Loading
Loading
+37 −72
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import android.util.AttributeSet;
import android.util.IntArray;
import android.util.Log;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.RenderNodeAnimator;
@@ -82,10 +83,12 @@ public class LockPatternView extends View {
    private static final int DOT_ACTIVATION_DURATION_MILLIS = 50;
    private static final int DOT_RADIUS_INCREASE_DURATION_MILLIS = 96;
    private static final int DOT_RADIUS_DECREASE_DURATION_MILLIS = 192;
    private static final float MIN_DOT_HIT_FACTOR = 0.2f;
    private final CellState[][] mCellStates;

    private final int mDotSize;
    private final int mDotSizeActivated;
    private final float mDotHitFactor;
    private final int mPathWidth;

    private boolean mDrawingProfilingStarted = false;
@@ -143,12 +146,11 @@ public class LockPatternView extends View {
    private boolean mPatternInProgress = false;
    private boolean mFadePattern = true;

    private float mHitFactor = 0.6f;

    @UnsupportedAppUsage
    private float mSquareWidth;
    @UnsupportedAppUsage
    private float mSquareHeight;
    private float mDotHitRadius;
    private final LinearGradient mFadeOutGradientShader;

    private final Path mCurrentPath = new Path();
@@ -164,8 +166,7 @@ public class LockPatternView extends View {

    private final Interpolator mFastOutSlowInInterpolator;
    private final Interpolator mLinearOutSlowInInterpolator;
    private PatternExploreByTouchHelper mExploreByTouchHelper;
    private AudioManager mAudioManager;
    private final PatternExploreByTouchHelper mExploreByTouchHelper;

    private Drawable mSelectedDrawable;
    private Drawable mNotSelectedDrawable;
@@ -349,6 +350,9 @@ public class LockPatternView extends View {
        mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);
        mDotSizeActivated = getResources().getDimensionPixelSize(
                R.dimen.lock_pattern_dot_size_activated);
        TypedValue outValue = new TypedValue();
        getResources().getValue(R.dimen.lock_pattern_dot_hit_factor, outValue, true);
        mDotHitFactor = Math.max(Math.min(outValue.getFloat(), 1f), MIN_DOT_HIT_FACTOR);

        mUseLockPatternDrawable = getResources().getBoolean(R.bool.use_lock_pattern_drawable);
        if (mUseLockPatternDrawable) {
@@ -375,7 +379,6 @@ public class LockPatternView extends View {
                AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
        mExploreByTouchHelper = new PatternExploreByTouchHelper(this);
        setAccessibilityDelegate(mExploreByTouchHelper);
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);

        int fadeAwayGradientWidth = getResources().getDimensionPixelSize(
                R.dimen.lock_pattern_fade_away_gradient_width);
@@ -679,6 +682,7 @@ public class LockPatternView extends View {
        final int height = h - mPaddingTop - mPaddingBottom;
        mSquareHeight = height / 3.0f;
        mExploreByTouchHelper.invalidateRoot();
        mDotHitRadius = Math.min(mSquareHeight / 2, mSquareWidth / 2) * mDotHitFactor;

        if (mUseLockPatternDrawable) {
            mNotSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
@@ -890,63 +894,30 @@ public class LockPatternView extends View {
        return set;
    }

    // helper method to find which cell a point maps to
    @Nullable
    private Cell checkForNewHit(float x, float y) {

        final int rowHit = getRowHit(y);
        if (rowHit < 0) {
            return null;
        Cell cellHit = detectCellHit(x, y);
        if (cellHit != null && !mPatternDrawLookup[cellHit.row][cellHit.column]) {
            return cellHit;
        }
        final int columnHit = getColumnHit(x);
        if (columnHit < 0) {
            return null;
        }

        if (mPatternDrawLookup[rowHit][columnHit]) {
        return null;
    }
        return Cell.of(rowHit, columnHit);
    }

    /**
     * Helper method to find the row that y falls into.
     * @param y The y coordinate
     * @return The row that y falls in, or -1 if it falls in no row.
     */
    private int getRowHit(float y) {

        final float squareHeight = mSquareHeight;
        float hitSize = squareHeight * mHitFactor;

        float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
        for (int i = 0; i < 3; i++) {

            final float hitTop = offset + squareHeight * i;
            if (y >= hitTop && y <= hitTop + hitSize) {
                return i;
            }
        }
        return -1;
    /** Helper method to find which cell a point maps to. */
    @Nullable
    private Cell detectCellHit(float x, float y) {
        final float hitRadiusSquared = mDotHitRadius * mDotHitRadius;
        for (int row = 0; row < 3; row++) {
            for (int column = 0; column < 3; column++) {
                float centerY = getCenterYForRow(row);
                float centerX = getCenterXForColumn(column);
                if ((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)
                        < hitRadiusSquared) {
                    return Cell.of(row, column);
                }

    /**
     * Helper method to find the column x fallis into.
     * @param x The x coordinate.
     * @return The column that x falls in, or -1 if it falls in no column.
     */
    private int getColumnHit(float x) {
        final float squareWidth = mSquareWidth;
        float hitSize = squareWidth * mHitFactor;

        float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
        for (int i = 0; i < 3; i++) {

            final float hitLeft = offset + squareWidth * i;
            if (x >= hitLeft && x <= hitLeft + hitSize) {
                return i;
            }
        }
        return -1;
        return null;
    }

    @Override
@@ -1553,8 +1524,7 @@ public class LockPatternView extends View {
        protected int getVirtualViewAt(float x, float y) {
            // This must use the same hit logic for the screen to ensure consistency whether
            // accessibility is on or off.
            int id = getVirtualViewIdForHit(x, y);
            return id;
            return getVirtualViewIdForHit(x, y);
        }

        @Override
@@ -1670,12 +1640,11 @@ public class LockPatternView extends View {
            final int col = ordinal % 3;
            float centerX = getCenterXForColumn(col);
            float centerY = getCenterYForRow(row);
            float cellheight = mSquareHeight * mHitFactor * 0.5f;
            float cellwidth = mSquareWidth * mHitFactor * 0.5f;
            bounds.left = (int) (centerX - cellwidth);
            bounds.right = (int) (centerX + cellwidth);
            bounds.top = (int) (centerY - cellheight);
            bounds.bottom = (int) (centerY + cellheight);
            float cellHitRadius = mDotHitRadius;
            bounds.left = (int) (centerX - cellHitRadius);
            bounds.right = (int) (centerX + cellHitRadius);
            bounds.top = (int) (centerY - cellHitRadius);
            bounds.bottom = (int) (centerY + cellHitRadius);
            return bounds;
        }

@@ -1694,16 +1663,12 @@ public class LockPatternView extends View {
         * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit
         */
        private int getVirtualViewIdForHit(float x, float y) {
            final int rowHit = getRowHit(y);
            if (rowHit < 0) {
                return ExploreByTouchHelper.INVALID_ID;
            }
            final int columnHit = getColumnHit(x);
            if (columnHit < 0) {
            Cell cellHit = detectCellHit(x, y);
            if (cellHit == null) {
                return ExploreByTouchHelper.INVALID_ID;
            }
            boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
            int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
            boolean dotAvailable = mPatternDrawLookup[cellHit.row][cellHit.column];
            int dotId = (cellHit.row * 3 + cellHit.column) + VIRTUAL_BASE_VIEW_ID;
            int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
            if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => "
                    + view + "avail =" + dotAvailable);
+3 −0
Original line number Diff line number Diff line
@@ -668,6 +668,9 @@
    <dimen name="lock_pattern_dot_line_width">22dp</dimen>
    <dimen name="lock_pattern_dot_size">14dp</dimen>
    <dimen name="lock_pattern_dot_size_activated">30dp</dimen>
    <!-- How much of the cell space is classified as hit areas [0..1] where 1 means that hit area is
         a circle with diameter equals to cell minimum side min(width, height). -->
    <item type="dimen" format="float" name="lock_pattern_dot_hit_factor">0.6</item>
    <!-- Width of a gradient applied to a lock pattern line while its disappearing animation. -->
    <dimen name="lock_pattern_fade_away_gradient_width">8dp</dimen>

+1 −0
Original line number Diff line number Diff line
@@ -1325,6 +1325,7 @@
  <java-symbol type="dimen" name="lock_pattern_dot_line_width" />
  <java-symbol type="dimen" name="lock_pattern_dot_size" />
  <java-symbol type="dimen" name="lock_pattern_dot_size_activated" />
  <java-symbol type="dimen" name="lock_pattern_dot_hit_factor" />
  <java-symbol type="dimen" name="lock_pattern_fade_away_gradient_width" />
  <java-symbol type="drawable" name="clock_dial" />
  <java-symbol type="drawable" name="clock_hand_hour" />
+247 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.internal.widget;

import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;

import android.content.Context;

import androidx.test.annotation.UiThreadTest;

import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toolbar;


import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.rule.UiThreadTestRule;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import com.android.internal.R;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

@RunWith(Parameterized.class)
@SmallTest
public class LockPatternViewTest {

    @Rule
    public UiThreadTestRule uiThreadTestRule = new UiThreadTestRule();

    private final int mViewSize;
    private final float mDefaultError;
    private final float mDot1x;
    private final float mDot1y;
    private final float mDot2x;
    private final float mDot2y;
    private final float mDot3x;
    private final float mDot3y;
    private final float mDot5x;
    private final float mDot5y;
    private final float mDot7x;
    private final float mDot7y;
    private final float mDot9x;
    private final float mDot9y;

    private Context mContext;
    private LockPatternView mLockPatternView;
    @Mock
    private LockPatternView.OnPatternListener mPatternListener;
    @Captor
    private ArgumentCaptor<List<LockPatternView.Cell>> mCellsArgumentCaptor;

    public LockPatternViewTest(int viewSize) {
        mViewSize = viewSize;
        float cellSize = viewSize / 3f;
        mDefaultError = cellSize * 0.2f;
        mDot1x = cellSize / 2f;
        mDot1y = cellSize / 2f;
        mDot2x = cellSize + mDot1x;
        mDot2y = mDot1y;
        mDot3x = cellSize + mDot2x;
        mDot3y = mDot1y;
        // dot4 is skipped as redundant
        mDot5x = cellSize + mDot1x;
        mDot5y = cellSize + mDot1y;
        // dot6 is skipped as redundant
        mDot7x = mDot1x;
        mDot7y = cellSize * 2 + mDot1y;
        // dot8 is skipped as redundant
        mDot9x = cellSize * 2 + mDot7x;
        mDot9y = mDot7y;
    }

    @Parameterized.Parameters
    public static Collection primeNumbers() {
        return Arrays.asList(192, 512, 768, 1024);
    }

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mContext = InstrumentationRegistry.getContext();
        mLockPatternView = new LockPatternView(mContext, null);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(mViewSize,
                View.MeasureSpec.EXACTLY);
        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mViewSize,
                View.MeasureSpec.EXACTLY);
        mLockPatternView.measure(widthMeasureSpec, heightMeasureSpec);
        mLockPatternView.layout(0, 0, mLockPatternView.getMeasuredWidth(),
                mLockPatternView.getMeasuredHeight());
    }

    @UiThreadTest
    @Test
    public void downStartsPattern() {
        mLockPatternView.setOnPatternListener(mPatternListener);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, mDot1x, mDot1y, 1));
        verify(mPatternListener).onPatternStart();
    }

    @UiThreadTest
    @Test
    public void up_completesPattern() {
        mLockPatternView.setOnPatternListener(mPatternListener);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, mDot1x, mDot1y, 1));
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, mDot1x, mDot1y, 1));
        verify(mPatternListener).onPatternDetected(any());
    }

    @UiThreadTest
    @Test
    public void moveToDot_hitsDot() {
        mLockPatternView.setOnPatternListener(mPatternListener);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, mDot1x, mDot1y, 1));
        verify(mPatternListener).onPatternStart();
    }

    @UiThreadTest
    @Test
    public void moveOutside_doesNotHitsDot() {
        mLockPatternView.setOnPatternListener(mPatternListener);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 2f, 2f, 1));
        verify(mPatternListener, never()).onPatternStart();
    }

    @UiThreadTest
    @Test
    public void moveAlongTwoDots_hitsTwo() {
        mLockPatternView.setOnPatternListener(mPatternListener);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
        makeMove(mDot1x, mDot1y, mDot2x, mDot2y, 6);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot2x, mDot2y, 1));

        verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture());
        List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue();
        assertThat(patternCells, hasSize(2));
        assertThat(patternCells,
                contains(LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(0, 1)));
    }

    @UiThreadTest
    @Test
    public void moveAlongTwoDotsDiagonally_hitsTwo() {
        mLockPatternView.setOnPatternListener(mPatternListener);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
        makeMove(mDot1x, mDot1y, mDot5x, mDot5y, 6);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot5x, mDot5y, 1));

        verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture());
        List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue();
        assertThat(patternCells, hasSize(2));
        assertThat(patternCells,
                contains(LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(1, 1)));
    }

    @UiThreadTest
    @Test
    public void moveAlongZPattern_hitsDots() {
        mLockPatternView.setOnPatternListener(mPatternListener);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
        makeMove(mDot1x, mDot1y, mDot3x + mDefaultError, mDot3y, 10);
        makeMove(mDot3x - mDefaultError, mDot3y, mDot7x, mDot7y, 10);
        makeMove(mDot7x, mDot7y - mDefaultError, mDot9x, mDot9y - mDefaultError, 10);
        mLockPatternView.onTouchEvent(
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, mViewSize - mDefaultError,
                        mViewSize - mDefaultError, 1));

        verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture());
        List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue();
        assertThat(patternCells, hasSize(7));
        assertThat(patternCells,
                contains(LockPatternView.Cell.of(0, 0),
                        LockPatternView.Cell.of(0, 1),
                        LockPatternView.Cell.of(0, 2),
                        LockPatternView.Cell.of(1, 1),
                        LockPatternView.Cell.of(2, 0),
                        LockPatternView.Cell.of(2, 1),
                        LockPatternView.Cell.of(2, 2)));
    }

    private void makeMove(float xFrom, float yFrom, float xTo, float yTo, int numberOfSteps) {
        for (int i = 0; i < numberOfSteps; i++) {
            float progress = i / (numberOfSteps - 1f);
            float rest = 1f - progress;
            mLockPatternView.onTouchEvent(
                    MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
                            /* x= */ xFrom * rest + xTo * progress,
                            /* y= */ yFrom * rest + yTo * progress,
                            1));
        }
    }
}