Loading core/java/com/android/internal/widget/LockPatternView.java +203 −39 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; import android.view.HapticFeedbackConstants; import android.view.InputDevice; import android.view.MotionEvent; import android.view.RenderNodeAnimator; import android.view.View; Loading @@ -56,6 +57,8 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import androidx.annotation.VisibleForTesting; import com.android.internal.R; import com.android.internal.graphics.ColorUtils; Loading Loading @@ -148,6 +151,8 @@ public class LockPatternView extends View { private boolean mInputEnabled = true; @UnsupportedAppUsage private boolean mInStealthMode = false; private InputMode mInputMode = InputMode.Swipe; private boolean mClickInputSupported = false; @UnsupportedAppUsage private boolean mPatternInProgress = false; private boolean mFadePattern = true; Loading Loading @@ -290,6 +295,23 @@ public class LockPatternView extends View { Wrong } /** * Input behavior types of the UI */ public enum InputMode { /** * A user is entering the pattern using swiping, e.g. with a finger, stylus or with * touch exploration support */ Swipe, /** * A user is entering the pattern using click, e.g. with a finger, mouse or track pad */ Click } /** * The call back interface for detecting patterns entered by the user. */ Loading @@ -297,25 +319,60 @@ public class LockPatternView extends View { /** * A new pattern has begun. * * @deprecated use {@link #onPatternStart(InputMode)} */ void onPatternStart(); @Deprecated default void onPatternStart() {} /** * A new pattern has begun. * @param inputMode The input mode that was used to enter the pattern. */ default void onPatternStart(InputMode inputMode) { onPatternStart(); } /** * The pattern was cleared. */ void onPatternCleared(); default void onPatternCleared() {} /** * The user extended the pattern currently being drawn by one cell. * @param pattern The pattern with newly added cell. * * @deprecated use {@link #onPatternCellAdded(List<Cell>, InputMode)} */ @Deprecated default void onPatternCellAdded(List<Cell> pattern) {} /** * The user extended the pattern currently being drawn by one cell. * @param pattern The pattern with newly added cell. * @param inputMode The input mode that was used to enter the pattern. */ default void onPatternCellAdded(List<Cell> pattern, InputMode inputMode) { onPatternCellAdded(pattern); } /** * A pattern was detected from the user. * @param pattern The pattern. * * @deprecated use {@link #onPatternDetected(List<Cell>, InputMode)} */ void onPatternCellAdded(List<Cell> pattern); @Deprecated default void onPatternDetected(List<Cell> pattern) {} /** * A pattern was detected from the user. * @param pattern The pattern. * @param inputMode The input mode that was used to enter the pattern. */ void onPatternDetected(List<Cell> pattern); default void onPatternDetected(List<Cell> pattern, InputMode inputMode) { onPatternDetected(pattern); } } /** An external haptics player for pattern updates. */ Loading Loading @@ -450,6 +507,25 @@ public class LockPatternView extends View { mInStealthMode = inStealthMode; } /** * Get the current input mode * @return Current input mode of the view. */ @VisibleForTesting public InputMode getInputMode() { return mInputMode; } /** * Set whether the view supports click input mode. If true, a pattern * can be entered using sequential clicks. * * @param clickInputSupported Whether tap input is supported. */ public void setClickInputSupported(boolean clickInputSupported) { mClickInputSupported = clickInputSupported; } /** * Set whether the pattern should fade as it's being drawn. If * true, each segment of the pattern fades over time. Loading Loading @@ -626,7 +702,7 @@ public class LockPatternView extends View { private void notifyCellAdded() { // sendAccessEvent(R.string.lockscreen_access_pattern_cell_added); if (mOnPatternListener != null) { mOnPatternListener.onPatternCellAdded(mPattern); mOnPatternListener.onPatternCellAdded(new ArrayList(mPattern), mInputMode); } // Disable used cells for accessibility as they get added if (DEBUG_A11Y) Log.v(TAG, "ivnalidating root because cell was added."); Loading @@ -635,14 +711,14 @@ public class LockPatternView extends View { private void notifyPatternStarted() { if (mOnPatternListener != null) { mOnPatternListener.onPatternStart(); mOnPatternListener.onPatternStart(mInputMode); } } @UnsupportedAppUsage private void notifyPatternDetected() { if (mOnPatternListener != null) { mOnPatternListener.onPatternDetected(mPattern); mOnPatternListener.onPatternDetected(new ArrayList(mPattern), mInputMode); } } Loading Loading @@ -1141,31 +1217,68 @@ public class LockPatternView extends View { return false; } switch(event.getAction()) { case MotionEvent.ACTION_DOWN: final int source = event.getSource(); final boolean sourceIsMouse = (source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE; final boolean sourceIsTouch = !sourceIsMouse; if ((AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled() || sourceIsTouch) && mInputMode == InputMode.Click) { // Switch to swipe mode // Only if current pattern is not already valid if (mPattern.size() >= LockPatternUtils.MIN_LOCK_PATTERN_SIZE && mPatternDisplayMode != DisplayMode.Wrong) { return false; } switchInputMode(InputMode.Swipe); } else if (mClickInputSupported && sourceIsMouse && mInputMode == InputMode.Swipe) { // Switch to click mode // Valid pattern is already preserved by enablement check above switchInputMode(InputMode.Click); } if (mInputMode == InputMode.Click) { return switch (event.getAction()) { // Handle ACTION_DOWN event. Otherwise the ACTION_UP event wouldn't be received case MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> true; case MotionEvent.ACTION_UP -> { handleActionMouseUp(event); yield true; } case MotionEvent.ACTION_CANCEL -> { handleActionCancel(); yield true; } default -> false; }; } else { return switch (event.getAction()) { case MotionEvent.ACTION_DOWN -> { handleActionDown(event); return true; case MotionEvent.ACTION_UP: yield true; } case MotionEvent.ACTION_UP -> { handleActionUp(); return true; case MotionEvent.ACTION_MOVE: yield true; } case MotionEvent.ACTION_MOVE -> { handleActionMove(event); return true; case MotionEvent.ACTION_CANCEL: if (mPatternInProgress) { setPatternInProgress(false); resetPattern(); notifyPatternCleared(); yield true; } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; case MotionEvent.ACTION_CANCEL -> { handleActionCancel(); yield true; } default -> false; }; } return true; } return false; private void switchInputMode(InputMode inputMode) { setPatternInProgress(false); resetPattern(); mInputMode = inputMode; notifyPatternCleared(); } private void setPatternInProgress(boolean progress) { Loading Loading @@ -1267,6 +1380,33 @@ public class LockPatternView extends View { } } private void handleActionMouseUp(MotionEvent event) { if (mPatternDisplayMode == DisplayMode.Wrong) { resetPattern(); } final float x = event.getX(); final float y = event.getY(); final int previousPatternSize = mPattern.size(); final Cell hitCell = detectAndAddHit(x, y); if (hitCell != null) { if (previousPatternSize == 0) { setPatternInProgress(true); mPatternDisplayMode = DisplayMode.Correct; notifyPatternStarted(); } notifyPatternDetected(); invalidate(); mInProgressX = getCenterXForColumn(hitCell.column); mInProgressY = getCenterYForRow(hitCell.row); } if (PROFILE_DRAWING) { if (!mDrawingProfilingStarted) { Debug.startMethodTracing("LockPatternDrawing"); mDrawingProfilingStarted = true; } } } private void deactivateLastCell() { Cell lastCell = mPattern.get(mPattern.size() - 1); startCellDeactivatedAnimation(lastCell, /* fillInGap= */ false); Loading @@ -1287,6 +1427,7 @@ public class LockPatternView extends View { } } } private void handleActionDown(MotionEvent event) { resetPattern(); final float x = event.getX(); Loading Loading @@ -1320,6 +1461,20 @@ public class LockPatternView extends View { } } private void handleActionCancel() { if (mPatternInProgress) { setPatternInProgress(false); resetPattern(); notifyPatternCleared(); } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } } /** * Change theme colors * @param regularColor The dot color Loading Loading @@ -1660,7 +1815,7 @@ public class LockPatternView extends View { return new SavedState(superState, patternString, mPatternDisplayMode.ordinal(), mInputEnabled, mInStealthMode); mInputEnabled, mInputMode.ordinal(), mInStealthMode); } @Override Loading @@ -1672,6 +1827,7 @@ public class LockPatternView extends View { LockPatternUtils.byteArrayToPattern(ss.getSerializedPattern().getBytes())); mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; mInputEnabled = ss.isInputEnabled(); mInputMode = InputMode.values()[ss.getInputMode()]; mInStealthMode = ss.isInStealthMode(); } Loading @@ -1690,6 +1846,7 @@ public class LockPatternView extends View { private final String mSerializedPattern; private final int mDisplayMode; private final boolean mInputEnabled; private final int mInputMode; private final boolean mInStealthMode; /** Loading @@ -1697,11 +1854,12 @@ public class LockPatternView extends View { */ @UnsupportedAppUsage private SavedState(Parcelable superState, String serializedPattern, int displayMode, boolean inputEnabled, boolean inStealthMode) { boolean inputEnabled, int inputMode, boolean inStealthMode) { super(superState); mSerializedPattern = serializedPattern; mDisplayMode = displayMode; mInputEnabled = inputEnabled; mInputMode = inputMode; mInStealthMode = inStealthMode; } Loading @@ -1714,22 +1872,27 @@ public class LockPatternView extends View { mSerializedPattern = in.readString(); mDisplayMode = in.readInt(); mInputEnabled = (Boolean) in.readValue(null); mInputMode = in.readInt(); mInStealthMode = (Boolean) in.readValue(null); } public String getSerializedPattern() { String getSerializedPattern() { return mSerializedPattern; } public int getDisplayMode() { int getDisplayMode() { return mDisplayMode; } public boolean isInputEnabled() { boolean isInputEnabled() { return mInputEnabled; } public boolean isInStealthMode() { int getInputMode() { return mInputMode; } boolean isInStealthMode() { return mInStealthMode; } Loading @@ -1739,6 +1902,7 @@ public class LockPatternView extends View { dest.writeString(mSerializedPattern); dest.writeInt(mDisplayMode); dest.writeValue(mInputEnabled); dest.writeInt(mInputMode); dest.writeValue(mInStealthMode); } Loading core/tests/coretests/src/com/android/internal/widget/LockPatternViewTest.java +95 −27 Original line number Diff line number Diff line Loading @@ -16,50 +16,40 @@ 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.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import android.content.Context; import android.view.InputDevice; import android.view.MotionEvent; import android.view.View; import androidx.test.InstrumentationRegistry; import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; import androidx.test.rule.UiThreadTestRule; import com.google.android.collect.Lists; 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.Collections; import java.util.List; @RunWith(Parameterized.class) Loading Loading @@ -137,7 +127,7 @@ public class LockPatternViewTest { mLockPatternView.setOnPatternListener(mPatternListener); mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, mDot1x, mDot1y, 1)); verify(mPatternListener).onPatternStart(); verify(mPatternListener).onPatternStart(any()); } @UiThreadTest Loading @@ -148,7 +138,7 @@ public class LockPatternViewTest { 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()); verify(mPatternListener).onPatternDetected(any(), any()); } @UiThreadTest Loading @@ -159,7 +149,7 @@ public class LockPatternViewTest { 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(); verify(mPatternListener).onPatternStart(any()); } @UiThreadTest Loading @@ -170,7 +160,7 @@ public class LockPatternViewTest { 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(); verify(mPatternListener, never()).onPatternStart(any()); } @UiThreadTest Loading @@ -183,7 +173,7 @@ public class LockPatternViewTest { mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot2x, mDot2y, 1)); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture()); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture(), any()); List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue(); assertThat(patternCells, hasSize(2)); assertThat(patternCells, Loading @@ -200,7 +190,7 @@ public class LockPatternViewTest { mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot5x, mDot5y, 1)); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture()); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture(), any()); List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue(); assertThat(patternCells, hasSize(2)); assertThat(patternCells, Loading @@ -220,7 +210,7 @@ public class LockPatternViewTest { MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, mViewSize - mDefaultError, mViewSize - mDefaultError, 1)); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture()); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture(), any()); List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue(); assertThat(patternCells, hasSize(7)); assertThat(patternCells, Loading @@ -244,4 +234,82 @@ public class LockPatternViewTest { 1)); } } @UiThreadTest @Test public void switchToClickModeAndClear() { mLockPatternView.setClickInputSupported(true); mLockPatternView.setOnPatternListener(mPatternListener); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Swipe)); mouseClick(mLockPatternView, mDot1x, mDot1y); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Click)); verify(mPatternListener).onPatternCleared(); } @UiThreadTest @Test public void switchToSwipeModeAndClear() { mLockPatternView.setClickInputSupported(true); mouseClick(mLockPatternView, mDot1x, mDot1y); mLockPatternView.setOnPatternListener(mPatternListener); mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1)); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Swipe)); verify(mPatternListener).onPatternCleared(); } @UiThreadTest @Test public void dontSwitchToSwipeModeForValidPattern() { mLockPatternView.setClickInputSupported(true); mouseClick(mLockPatternView, mDot1x, mDot1y); mLockPatternView.setOnPatternListener(mPatternListener); mLockPatternView.setPattern(LockPatternView.DisplayMode.Correct, Collections.unmodifiableList(Lists.newArrayList( LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(0, 1), LockPatternView.Cell.of(1, 1), LockPatternView.Cell.of(2, 1) ))); mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1)); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Click)); verify(mPatternListener, never()).onPatternCleared(); } @UiThreadTest @Test public void clickFromCornerToCornerIncludesCenterOfEdgeAndNotifies() { mLockPatternView.setClickInputSupported(true); mLockPatternView.setOnPatternListener(mPatternListener); mouseClick(mLockPatternView, mDot1x, mDot1y); mouseClick(mLockPatternView, mDot3x, mDot3y); verify(mPatternListener, times(2)).onPatternDetected(mCellsArgumentCaptor.capture(), any()); List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue(); assertThat(patternCells, hasSize(3)); verify(mPatternListener, times(3)).onPatternCellAdded(any(), any()); } @UiThreadTest @Test public void swipeOverInvalidClickPattern() { mLockPatternView.setClickInputSupported(true); mouseClick(mLockPatternView, mDot1x, mDot1y); mouseClick(mLockPatternView, mDot3x, mDot3y); mouseClick(mLockPatternView, mDot7x, mDot7y); mouseClick(mLockPatternView, mDot9x, mDot9y); mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1)); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Swipe)); } private void mouseClick(View view, float x, float y) { MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, y, 1); event.setSource(InputDevice.SOURCE_MOUSE); view.onTouchEvent(event); event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x, y, 1); event.setSource(InputDevice.SOURCE_MOUSE); view.onTouchEvent(event); } } Loading
core/java/com/android/internal/widget/LockPatternView.java +203 −39 Original line number Diff line number Diff line Loading @@ -46,6 +46,7 @@ import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; import android.view.HapticFeedbackConstants; import android.view.InputDevice; import android.view.MotionEvent; import android.view.RenderNodeAnimator; import android.view.View; Loading @@ -56,6 +57,8 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import androidx.annotation.VisibleForTesting; import com.android.internal.R; import com.android.internal.graphics.ColorUtils; Loading Loading @@ -148,6 +151,8 @@ public class LockPatternView extends View { private boolean mInputEnabled = true; @UnsupportedAppUsage private boolean mInStealthMode = false; private InputMode mInputMode = InputMode.Swipe; private boolean mClickInputSupported = false; @UnsupportedAppUsage private boolean mPatternInProgress = false; private boolean mFadePattern = true; Loading Loading @@ -290,6 +295,23 @@ public class LockPatternView extends View { Wrong } /** * Input behavior types of the UI */ public enum InputMode { /** * A user is entering the pattern using swiping, e.g. with a finger, stylus or with * touch exploration support */ Swipe, /** * A user is entering the pattern using click, e.g. with a finger, mouse or track pad */ Click } /** * The call back interface for detecting patterns entered by the user. */ Loading @@ -297,25 +319,60 @@ public class LockPatternView extends View { /** * A new pattern has begun. * * @deprecated use {@link #onPatternStart(InputMode)} */ void onPatternStart(); @Deprecated default void onPatternStart() {} /** * A new pattern has begun. * @param inputMode The input mode that was used to enter the pattern. */ default void onPatternStart(InputMode inputMode) { onPatternStart(); } /** * The pattern was cleared. */ void onPatternCleared(); default void onPatternCleared() {} /** * The user extended the pattern currently being drawn by one cell. * @param pattern The pattern with newly added cell. * * @deprecated use {@link #onPatternCellAdded(List<Cell>, InputMode)} */ @Deprecated default void onPatternCellAdded(List<Cell> pattern) {} /** * The user extended the pattern currently being drawn by one cell. * @param pattern The pattern with newly added cell. * @param inputMode The input mode that was used to enter the pattern. */ default void onPatternCellAdded(List<Cell> pattern, InputMode inputMode) { onPatternCellAdded(pattern); } /** * A pattern was detected from the user. * @param pattern The pattern. * * @deprecated use {@link #onPatternDetected(List<Cell>, InputMode)} */ void onPatternCellAdded(List<Cell> pattern); @Deprecated default void onPatternDetected(List<Cell> pattern) {} /** * A pattern was detected from the user. * @param pattern The pattern. * @param inputMode The input mode that was used to enter the pattern. */ void onPatternDetected(List<Cell> pattern); default void onPatternDetected(List<Cell> pattern, InputMode inputMode) { onPatternDetected(pattern); } } /** An external haptics player for pattern updates. */ Loading Loading @@ -450,6 +507,25 @@ public class LockPatternView extends View { mInStealthMode = inStealthMode; } /** * Get the current input mode * @return Current input mode of the view. */ @VisibleForTesting public InputMode getInputMode() { return mInputMode; } /** * Set whether the view supports click input mode. If true, a pattern * can be entered using sequential clicks. * * @param clickInputSupported Whether tap input is supported. */ public void setClickInputSupported(boolean clickInputSupported) { mClickInputSupported = clickInputSupported; } /** * Set whether the pattern should fade as it's being drawn. If * true, each segment of the pattern fades over time. Loading Loading @@ -626,7 +702,7 @@ public class LockPatternView extends View { private void notifyCellAdded() { // sendAccessEvent(R.string.lockscreen_access_pattern_cell_added); if (mOnPatternListener != null) { mOnPatternListener.onPatternCellAdded(mPattern); mOnPatternListener.onPatternCellAdded(new ArrayList(mPattern), mInputMode); } // Disable used cells for accessibility as they get added if (DEBUG_A11Y) Log.v(TAG, "ivnalidating root because cell was added."); Loading @@ -635,14 +711,14 @@ public class LockPatternView extends View { private void notifyPatternStarted() { if (mOnPatternListener != null) { mOnPatternListener.onPatternStart(); mOnPatternListener.onPatternStart(mInputMode); } } @UnsupportedAppUsage private void notifyPatternDetected() { if (mOnPatternListener != null) { mOnPatternListener.onPatternDetected(mPattern); mOnPatternListener.onPatternDetected(new ArrayList(mPattern), mInputMode); } } Loading Loading @@ -1141,31 +1217,68 @@ public class LockPatternView extends View { return false; } switch(event.getAction()) { case MotionEvent.ACTION_DOWN: final int source = event.getSource(); final boolean sourceIsMouse = (source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE; final boolean sourceIsTouch = !sourceIsMouse; if ((AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled() || sourceIsTouch) && mInputMode == InputMode.Click) { // Switch to swipe mode // Only if current pattern is not already valid if (mPattern.size() >= LockPatternUtils.MIN_LOCK_PATTERN_SIZE && mPatternDisplayMode != DisplayMode.Wrong) { return false; } switchInputMode(InputMode.Swipe); } else if (mClickInputSupported && sourceIsMouse && mInputMode == InputMode.Swipe) { // Switch to click mode // Valid pattern is already preserved by enablement check above switchInputMode(InputMode.Click); } if (mInputMode == InputMode.Click) { return switch (event.getAction()) { // Handle ACTION_DOWN event. Otherwise the ACTION_UP event wouldn't be received case MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> true; case MotionEvent.ACTION_UP -> { handleActionMouseUp(event); yield true; } case MotionEvent.ACTION_CANCEL -> { handleActionCancel(); yield true; } default -> false; }; } else { return switch (event.getAction()) { case MotionEvent.ACTION_DOWN -> { handleActionDown(event); return true; case MotionEvent.ACTION_UP: yield true; } case MotionEvent.ACTION_UP -> { handleActionUp(); return true; case MotionEvent.ACTION_MOVE: yield true; } case MotionEvent.ACTION_MOVE -> { handleActionMove(event); return true; case MotionEvent.ACTION_CANCEL: if (mPatternInProgress) { setPatternInProgress(false); resetPattern(); notifyPatternCleared(); yield true; } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; case MotionEvent.ACTION_CANCEL -> { handleActionCancel(); yield true; } default -> false; }; } return true; } return false; private void switchInputMode(InputMode inputMode) { setPatternInProgress(false); resetPattern(); mInputMode = inputMode; notifyPatternCleared(); } private void setPatternInProgress(boolean progress) { Loading Loading @@ -1267,6 +1380,33 @@ public class LockPatternView extends View { } } private void handleActionMouseUp(MotionEvent event) { if (mPatternDisplayMode == DisplayMode.Wrong) { resetPattern(); } final float x = event.getX(); final float y = event.getY(); final int previousPatternSize = mPattern.size(); final Cell hitCell = detectAndAddHit(x, y); if (hitCell != null) { if (previousPatternSize == 0) { setPatternInProgress(true); mPatternDisplayMode = DisplayMode.Correct; notifyPatternStarted(); } notifyPatternDetected(); invalidate(); mInProgressX = getCenterXForColumn(hitCell.column); mInProgressY = getCenterYForRow(hitCell.row); } if (PROFILE_DRAWING) { if (!mDrawingProfilingStarted) { Debug.startMethodTracing("LockPatternDrawing"); mDrawingProfilingStarted = true; } } } private void deactivateLastCell() { Cell lastCell = mPattern.get(mPattern.size() - 1); startCellDeactivatedAnimation(lastCell, /* fillInGap= */ false); Loading @@ -1287,6 +1427,7 @@ public class LockPatternView extends View { } } } private void handleActionDown(MotionEvent event) { resetPattern(); final float x = event.getX(); Loading Loading @@ -1320,6 +1461,20 @@ public class LockPatternView extends View { } } private void handleActionCancel() { if (mPatternInProgress) { setPatternInProgress(false); resetPattern(); notifyPatternCleared(); } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } } /** * Change theme colors * @param regularColor The dot color Loading Loading @@ -1660,7 +1815,7 @@ public class LockPatternView extends View { return new SavedState(superState, patternString, mPatternDisplayMode.ordinal(), mInputEnabled, mInStealthMode); mInputEnabled, mInputMode.ordinal(), mInStealthMode); } @Override Loading @@ -1672,6 +1827,7 @@ public class LockPatternView extends View { LockPatternUtils.byteArrayToPattern(ss.getSerializedPattern().getBytes())); mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; mInputEnabled = ss.isInputEnabled(); mInputMode = InputMode.values()[ss.getInputMode()]; mInStealthMode = ss.isInStealthMode(); } Loading @@ -1690,6 +1846,7 @@ public class LockPatternView extends View { private final String mSerializedPattern; private final int mDisplayMode; private final boolean mInputEnabled; private final int mInputMode; private final boolean mInStealthMode; /** Loading @@ -1697,11 +1854,12 @@ public class LockPatternView extends View { */ @UnsupportedAppUsage private SavedState(Parcelable superState, String serializedPattern, int displayMode, boolean inputEnabled, boolean inStealthMode) { boolean inputEnabled, int inputMode, boolean inStealthMode) { super(superState); mSerializedPattern = serializedPattern; mDisplayMode = displayMode; mInputEnabled = inputEnabled; mInputMode = inputMode; mInStealthMode = inStealthMode; } Loading @@ -1714,22 +1872,27 @@ public class LockPatternView extends View { mSerializedPattern = in.readString(); mDisplayMode = in.readInt(); mInputEnabled = (Boolean) in.readValue(null); mInputMode = in.readInt(); mInStealthMode = (Boolean) in.readValue(null); } public String getSerializedPattern() { String getSerializedPattern() { return mSerializedPattern; } public int getDisplayMode() { int getDisplayMode() { return mDisplayMode; } public boolean isInputEnabled() { boolean isInputEnabled() { return mInputEnabled; } public boolean isInStealthMode() { int getInputMode() { return mInputMode; } boolean isInStealthMode() { return mInStealthMode; } Loading @@ -1739,6 +1902,7 @@ public class LockPatternView extends View { dest.writeString(mSerializedPattern); dest.writeInt(mDisplayMode); dest.writeValue(mInputEnabled); dest.writeInt(mInputMode); dest.writeValue(mInStealthMode); } Loading
core/tests/coretests/src/com/android/internal/widget/LockPatternViewTest.java +95 −27 Original line number Diff line number Diff line Loading @@ -16,50 +16,40 @@ 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.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import android.content.Context; import android.view.InputDevice; import android.view.MotionEvent; import android.view.View; import androidx.test.InstrumentationRegistry; import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; import androidx.test.rule.UiThreadTestRule; import com.google.android.collect.Lists; 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.Collections; import java.util.List; @RunWith(Parameterized.class) Loading Loading @@ -137,7 +127,7 @@ public class LockPatternViewTest { mLockPatternView.setOnPatternListener(mPatternListener); mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, mDot1x, mDot1y, 1)); verify(mPatternListener).onPatternStart(); verify(mPatternListener).onPatternStart(any()); } @UiThreadTest Loading @@ -148,7 +138,7 @@ public class LockPatternViewTest { 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()); verify(mPatternListener).onPatternDetected(any(), any()); } @UiThreadTest Loading @@ -159,7 +149,7 @@ public class LockPatternViewTest { 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(); verify(mPatternListener).onPatternStart(any()); } @UiThreadTest Loading @@ -170,7 +160,7 @@ public class LockPatternViewTest { 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(); verify(mPatternListener, never()).onPatternStart(any()); } @UiThreadTest Loading @@ -183,7 +173,7 @@ public class LockPatternViewTest { mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot2x, mDot2y, 1)); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture()); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture(), any()); List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue(); assertThat(patternCells, hasSize(2)); assertThat(patternCells, Loading @@ -200,7 +190,7 @@ public class LockPatternViewTest { mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot5x, mDot5y, 1)); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture()); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture(), any()); List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue(); assertThat(patternCells, hasSize(2)); assertThat(patternCells, Loading @@ -220,7 +210,7 @@ public class LockPatternViewTest { MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, mViewSize - mDefaultError, mViewSize - mDefaultError, 1)); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture()); verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture(), any()); List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue(); assertThat(patternCells, hasSize(7)); assertThat(patternCells, Loading @@ -244,4 +234,82 @@ public class LockPatternViewTest { 1)); } } @UiThreadTest @Test public void switchToClickModeAndClear() { mLockPatternView.setClickInputSupported(true); mLockPatternView.setOnPatternListener(mPatternListener); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Swipe)); mouseClick(mLockPatternView, mDot1x, mDot1y); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Click)); verify(mPatternListener).onPatternCleared(); } @UiThreadTest @Test public void switchToSwipeModeAndClear() { mLockPatternView.setClickInputSupported(true); mouseClick(mLockPatternView, mDot1x, mDot1y); mLockPatternView.setOnPatternListener(mPatternListener); mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1)); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Swipe)); verify(mPatternListener).onPatternCleared(); } @UiThreadTest @Test public void dontSwitchToSwipeModeForValidPattern() { mLockPatternView.setClickInputSupported(true); mouseClick(mLockPatternView, mDot1x, mDot1y); mLockPatternView.setOnPatternListener(mPatternListener); mLockPatternView.setPattern(LockPatternView.DisplayMode.Correct, Collections.unmodifiableList(Lists.newArrayList( LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(0, 1), LockPatternView.Cell.of(1, 1), LockPatternView.Cell.of(2, 1) ))); mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1)); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Click)); verify(mPatternListener, never()).onPatternCleared(); } @UiThreadTest @Test public void clickFromCornerToCornerIncludesCenterOfEdgeAndNotifies() { mLockPatternView.setClickInputSupported(true); mLockPatternView.setOnPatternListener(mPatternListener); mouseClick(mLockPatternView, mDot1x, mDot1y); mouseClick(mLockPatternView, mDot3x, mDot3y); verify(mPatternListener, times(2)).onPatternDetected(mCellsArgumentCaptor.capture(), any()); List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue(); assertThat(patternCells, hasSize(3)); verify(mPatternListener, times(3)).onPatternCellAdded(any(), any()); } @UiThreadTest @Test public void swipeOverInvalidClickPattern() { mLockPatternView.setClickInputSupported(true); mouseClick(mLockPatternView, mDot1x, mDot1y); mouseClick(mLockPatternView, mDot3x, mDot3y); mouseClick(mLockPatternView, mDot7x, mDot7y); mouseClick(mLockPatternView, mDot9x, mDot9y); mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); mLockPatternView.onTouchEvent( MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1)); assertThat(mLockPatternView.getInputMode(), is(LockPatternView.InputMode.Swipe)); } private void mouseClick(View view, float x, float y) { MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, y, 1); event.setSource(InputDevice.SOURCE_MOUSE); view.onTouchEvent(event); event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x, y, 1); event.setSource(InputDevice.SOURCE_MOUSE); view.onTouchEvent(event); } }