Loading core/java/com/android/internal/widget/LockPatternView.java +37 −72 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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(); Loading @@ -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; Loading Loading @@ -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) { Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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; } Loading @@ -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); Loading core/res/res/values/dimens.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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> Loading core/res/res/values/symbols.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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" /> Loading core/tests/coretests/src/com/android/internal/widget/LockPatternViewTest.java 0 → 100644 +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)); } } } Loading
core/java/com/android/internal/widget/LockPatternView.java +37 −72 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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(); Loading @@ -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; Loading Loading @@ -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) { Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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; } Loading @@ -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); Loading
core/res/res/values/dimens.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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> Loading
core/res/res/values/symbols.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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" /> Loading
core/tests/coretests/src/com/android/internal/widget/LockPatternViewTest.java 0 → 100644 +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)); } } }