diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java
index 2b6b933c6886d..01cec7727c4ee 100644
--- a/core/java/com/android/internal/widget/LockPatternView.java
+++ b/core/java/com/android/internal/widget/LockPatternView.java
@@ -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);
+ return null;
}
- /**
- * 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;
+ /** 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);
+ }
}
}
- return -1;
- }
-
- /**
- * 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) {
+ Cell cellHit = detectCellHit(x, y);
+ if (cellHit == null) {
return ExploreByTouchHelper.INVALID_ID;
}
- final int columnHit = getColumnHit(x);
- if (columnHit < 0) {
- 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);
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 1b9f7feec0ef0..44c5512593750 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -668,6 +668,9 @@
22dp
14dp
30dp
+
+ - 0.6
8dp
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8f3abd60dd3ca..6f34b3f899f12 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1325,6 +1325,7 @@
+
diff --git a/core/tests/coretests/src/com/android/internal/widget/LockPatternViewTest.java b/core/tests/coretests/src/com/android/internal/widget/LockPatternViewTest.java
new file mode 100644
index 0000000000000..8ba49663ad099
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/LockPatternViewTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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> 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 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 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 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));
+ }
+ }
+}