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

Commit 2314564f authored by Simon Ziegltrum's avatar Simon Ziegltrum Committed by Android (Google) Code Review
Browse files

Merge "Add click support for LockPatternView" into main

parents f93cb5b9 13c7727e
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);
    }
}