1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.widget;
18 
19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
20 
21 import android.content.Context;
22 
23 import androidx.test.annotation.UiThreadTest;
24 
25 import android.util.AttributeSet;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.Toolbar;
30 
31 
32 import static org.hamcrest.Matchers.contains;
33 import static org.hamcrest.Matchers.hasSize;
34 import static org.hamcrest.Matchers.is;
35 import static org.junit.Assert.assertThat;
36 import static org.mockito.ArgumentMatchers.any;
37 import static org.mockito.ArgumentMatchers.eq;
38 import static org.mockito.Mockito.atLeastOnce;
39 import static org.mockito.Mockito.mock;
40 import static org.mockito.Mockito.never;
41 import static org.mockito.Mockito.verify;
42 import static org.mockito.Mockito.verifyNoMoreInteractions;
43 
44 import androidx.test.InstrumentationRegistry;
45 import androidx.test.filters.SmallTest;
46 import androidx.test.rule.UiThreadTestRule;
47 
48 import org.junit.Before;
49 import org.junit.Rule;
50 import org.junit.Test;
51 import org.junit.runner.RunWith;
52 import org.junit.runners.Parameterized;
53 import org.mockito.ArgumentCaptor;
54 import org.mockito.ArgumentMatchers;
55 import org.mockito.Captor;
56 import org.mockito.Mock;
57 import org.mockito.MockitoAnnotations;
58 
59 import com.android.internal.R;
60 
61 import java.util.Arrays;
62 import java.util.Collection;
63 import java.util.List;
64 
65 @RunWith(Parameterized.class)
66 @SmallTest
67 public class LockPatternViewTest {
68 
69     @Rule
70     public UiThreadTestRule uiThreadTestRule = new UiThreadTestRule();
71 
72     private final int mViewSize;
73     private final float mDefaultError;
74     private final float mDot1x;
75     private final float mDot1y;
76     private final float mDot2x;
77     private final float mDot2y;
78     private final float mDot3x;
79     private final float mDot3y;
80     private final float mDot5x;
81     private final float mDot5y;
82     private final float mDot7x;
83     private final float mDot7y;
84     private final float mDot9x;
85     private final float mDot9y;
86 
87     private Context mContext;
88     private LockPatternView mLockPatternView;
89     @Mock
90     private LockPatternView.OnPatternListener mPatternListener;
91     @Captor
92     private ArgumentCaptor<List<LockPatternView.Cell>> mCellsArgumentCaptor;
93 
LockPatternViewTest(int viewSize)94     public LockPatternViewTest(int viewSize) {
95         mViewSize = viewSize;
96         float cellSize = viewSize / 3f;
97         mDefaultError = cellSize * 0.2f;
98         mDot1x = cellSize / 2f;
99         mDot1y = cellSize / 2f;
100         mDot2x = cellSize + mDot1x;
101         mDot2y = mDot1y;
102         mDot3x = cellSize + mDot2x;
103         mDot3y = mDot1y;
104         // dot4 is skipped as redundant
105         mDot5x = cellSize + mDot1x;
106         mDot5y = cellSize + mDot1y;
107         // dot6 is skipped as redundant
108         mDot7x = mDot1x;
109         mDot7y = cellSize * 2 + mDot1y;
110         // dot8 is skipped as redundant
111         mDot9x = cellSize * 2 + mDot7x;
112         mDot9y = mDot7y;
113     }
114 
115     @Parameterized.Parameters
primeNumbers()116     public static Collection primeNumbers() {
117         return Arrays.asList(192, 512, 768, 1024);
118     }
119 
120     @Before
setUp()121     public void setUp() throws Exception {
122         MockitoAnnotations.initMocks(this);
123         mContext = InstrumentationRegistry.getContext();
124         mLockPatternView = new LockPatternView(mContext, null);
125         int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(mViewSize,
126                 View.MeasureSpec.EXACTLY);
127         int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mViewSize,
128                 View.MeasureSpec.EXACTLY);
129         mLockPatternView.measure(widthMeasureSpec, heightMeasureSpec);
130         mLockPatternView.layout(0, 0, mLockPatternView.getMeasuredWidth(),
131                 mLockPatternView.getMeasuredHeight());
132     }
133 
134     @UiThreadTest
135     @Test
downStartsPattern()136     public void downStartsPattern() {
137         mLockPatternView.setOnPatternListener(mPatternListener);
138         mLockPatternView.onTouchEvent(
139                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, mDot1x, mDot1y, 1));
140         verify(mPatternListener).onPatternStart();
141     }
142 
143     @UiThreadTest
144     @Test
up_completesPattern()145     public void up_completesPattern() {
146         mLockPatternView.setOnPatternListener(mPatternListener);
147         mLockPatternView.onTouchEvent(
148                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, mDot1x, mDot1y, 1));
149         mLockPatternView.onTouchEvent(
150                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, mDot1x, mDot1y, 1));
151         verify(mPatternListener).onPatternDetected(any());
152     }
153 
154     @UiThreadTest
155     @Test
moveToDot_hitsDot()156     public void moveToDot_hitsDot() {
157         mLockPatternView.setOnPatternListener(mPatternListener);
158         mLockPatternView.onTouchEvent(
159                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
160         mLockPatternView.onTouchEvent(
161                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, mDot1x, mDot1y, 1));
162         verify(mPatternListener).onPatternStart();
163     }
164 
165     @UiThreadTest
166     @Test
moveOutside_doesNotHitsDot()167     public void moveOutside_doesNotHitsDot() {
168         mLockPatternView.setOnPatternListener(mPatternListener);
169         mLockPatternView.onTouchEvent(
170                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
171         mLockPatternView.onTouchEvent(
172                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 2f, 2f, 1));
173         verify(mPatternListener, never()).onPatternStart();
174     }
175 
176     @UiThreadTest
177     @Test
moveAlongTwoDots_hitsTwo()178     public void moveAlongTwoDots_hitsTwo() {
179         mLockPatternView.setOnPatternListener(mPatternListener);
180         mLockPatternView.onTouchEvent(
181                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
182         makeMove(mDot1x, mDot1y, mDot2x, mDot2y, 6);
183         mLockPatternView.onTouchEvent(
184                 MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot2x, mDot2y, 1));
185 
186         verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture());
187         List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue();
188         assertThat(patternCells, hasSize(2));
189         assertThat(patternCells,
190                 contains(LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(0, 1)));
191     }
192 
193     @UiThreadTest
194     @Test
moveAlongTwoDotsDiagonally_hitsTwo()195     public void moveAlongTwoDotsDiagonally_hitsTwo() {
196         mLockPatternView.setOnPatternListener(mPatternListener);
197         mLockPatternView.onTouchEvent(
198                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
199         makeMove(mDot1x, mDot1y, mDot5x, mDot5y, 6);
200         mLockPatternView.onTouchEvent(
201                 MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot5x, mDot5y, 1));
202 
203         verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture());
204         List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue();
205         assertThat(patternCells, hasSize(2));
206         assertThat(patternCells,
207                 contains(LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(1, 1)));
208     }
209 
210     @UiThreadTest
211     @Test
moveAlongZPattern_hitsDots()212     public void moveAlongZPattern_hitsDots() {
213         mLockPatternView.setOnPatternListener(mPatternListener);
214         mLockPatternView.onTouchEvent(
215                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
216         makeMove(mDot1x, mDot1y, mDot3x + mDefaultError, mDot3y, 10);
217         makeMove(mDot3x - mDefaultError, mDot3y, mDot7x, mDot7y, 10);
218         makeMove(mDot7x, mDot7y - mDefaultError, mDot9x, mDot9y - mDefaultError, 10);
219         mLockPatternView.onTouchEvent(
220                 MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, mViewSize - mDefaultError,
221                         mViewSize - mDefaultError, 1));
222 
223         verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture());
224         List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue();
225         assertThat(patternCells, hasSize(7));
226         assertThat(patternCells,
227                 contains(LockPatternView.Cell.of(0, 0),
228                         LockPatternView.Cell.of(0, 1),
229                         LockPatternView.Cell.of(0, 2),
230                         LockPatternView.Cell.of(1, 1),
231                         LockPatternView.Cell.of(2, 0),
232                         LockPatternView.Cell.of(2, 1),
233                         LockPatternView.Cell.of(2, 2)));
234     }
235 
makeMove(float xFrom, float yFrom, float xTo, float yTo, int numberOfSteps)236     private void makeMove(float xFrom, float yFrom, float xTo, float yTo, int numberOfSteps) {
237         for (int i = 0; i < numberOfSteps; i++) {
238             float progress = i / (numberOfSteps - 1f);
239             float rest = 1f - progress;
240             mLockPatternView.onTouchEvent(
241                     MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
242                             /* x= */ xFrom * rest + xTo * progress,
243                             /* y= */ yFrom * rest + yTo * progress,
244                             1));
245         }
246     }
247 }
248