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

Commit 68c8926a authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Make LockPatternView dot hit area circular." into tm-dev

parents 1ce39475 15aafa78
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));
        }
    }
}