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

Commit 13c7727e authored by Simon Ziegltrum's avatar Simon Ziegltrum
Browse files

Add click support for LockPatternView

Bug: 403345215
Test: manual on brya redrix
Test: atest packages/apps/Settings/tests/robotests/src/com/android/settings/password/ChooseLockPatternTest.java
Test: atest packages/apps/Settings/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java
Test: atest packages/apps/Settings/tests/robotests/src/com/android/settings/password/SetupChooseLockPatternTest.java
Test: atest frameworks/base/core/tests/coretests/src/com/android/internal/widget/LockPatternViewTest.java
Flag: com.android.settings.flags.enable_pattern_input_click_support
Change-Id: I42199efe8780a7ca5368e6411c94d326fa077552
parent ecc3fe1d
Loading
Loading
Loading
Loading
+203 −39
Original line number Diff line number Diff line
@@ -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;
@@ -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;

@@ -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;
@@ -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.
     */
@@ -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. */
@@ -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.
@@ -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.");
@@ -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);
        }
    }

@@ -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) {
@@ -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);
@@ -1287,6 +1427,7 @@ public class LockPatternView extends View {
            }
        }
    }

    private void handleActionDown(MotionEvent event) {
        resetPattern();
        final float x = event.getX();
@@ -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
@@ -1660,7 +1815,7 @@ public class LockPatternView extends View {
        return new SavedState(superState,
                patternString,
                mPatternDisplayMode.ordinal(),
                mInputEnabled, mInStealthMode);
                mInputEnabled, mInputMode.ordinal(), mInStealthMode);
    }

    @Override
@@ -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();
    }

@@ -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;

        /**
@@ -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;
        }

@@ -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;
        }

@@ -1739,6 +1902,7 @@ public class LockPatternView extends View {
            dest.writeString(mSerializedPattern);
            dest.writeInt(mDisplayMode);
            dest.writeValue(mInputEnabled);
            dest.writeInt(mInputMode);
            dest.writeValue(mInStealthMode);
        }

+95 −27
Original line number Diff line number Diff line
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
@@ -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,
@@ -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,
@@ -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);
    }
}