blob: c37a34a685498f01c512cc8e570452bcba0b2445 [file] [log] [blame]
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08001/*
2 * Copyright (C) 2019 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
17package android.widget;
18
Shu Chenafbcf852020-03-10 08:19:07 +080019import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -080020import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080021import static android.widget.espresso.TextViewActions.dragOnText;
22import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -080023import static android.widget.espresso.TextViewAssertions.hasSelection;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080024
25import static androidx.test.espresso.Espresso.onView;
26import static androidx.test.espresso.action.ViewActions.replaceText;
27import static androidx.test.espresso.matcher.ViewMatchers.withId;
28
Nikita Dubrovsky7c583592020-02-16 15:54:23 -080029import static com.google.common.truth.Truth.assertThat;
30import static com.google.common.truth.Truth.assertWithMessage;
31
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -080032import static org.hamcrest.Matchers.emptyString;
33import static org.hamcrest.Matchers.not;
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -080034import static org.junit.Assert.assertFalse;
35import static org.junit.Assert.assertTrue;
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -080036
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080037import android.app.Activity;
38import android.app.Instrumentation;
Shu Chen77003422020-03-05 13:38:05 +080039import android.graphics.Rect;
Nikita Dubrovsky7c583592020-02-16 15:54:23 -080040import android.text.Layout;
Shu Chenafbcf852020-03-10 08:19:07 +080041import android.text.Spannable;
42import android.text.SpannableString;
43import android.text.style.AbsoluteSizeSpan;
Shu Chen77003422020-03-05 13:38:05 +080044import android.util.ArraySet;
Nikita Dubrovsky7c583592020-02-16 15:54:23 -080045import android.util.Log;
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -080046import android.view.InputDevice;
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -080047import android.view.MotionEvent;
Shu Chen77003422020-03-05 13:38:05 +080048import android.view.View;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080049
50import androidx.test.InstrumentationRegistry;
Nikita Dubrovsky256ee072020-03-20 09:51:27 -070051import androidx.test.filters.MediumTest;
Nikita Dubrovsky7c583592020-02-16 15:54:23 -080052import androidx.test.filters.Suppress;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080053import androidx.test.rule.ActivityTestRule;
54import androidx.test.runner.AndroidJUnit4;
55
56import com.android.frameworks.coretests.R;
57
58import com.google.common.base.Strings;
59
Shu Chen77003422020-03-05 13:38:05 +080060import org.junit.After;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080061import org.junit.Before;
62import org.junit.Rule;
63import org.junit.Test;
64import org.junit.runner.RunWith;
65
Shu Chen77003422020-03-05 13:38:05 +080066import java.util.Set;
Nikita Dubrovsky7c583592020-02-16 15:54:23 -080067import java.util.concurrent.atomic.AtomicLong;
68
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080069@RunWith(AndroidJUnit4.class)
Nikita Dubrovsky256ee072020-03-20 09:51:27 -070070@MediumTest
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080071public class EditorCursorDragTest {
Nikita Dubrovsky7c583592020-02-16 15:54:23 -080072 private static final String LOG_TAG = EditorCursorDragTest.class.getSimpleName();
73
74 private static final AtomicLong sTicker = new AtomicLong(1);
75
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080076 @Rule
77 public ActivityTestRule<TextViewActivity> mActivityRule = new ActivityTestRule<>(
78 TextViewActivity.class);
79
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080080 private Instrumentation mInstrumentation;
81 private Activity mActivity;
Shu Chen77003422020-03-05 13:38:05 +080082 private Set<MotionEvent> mMotionEvents = new ArraySet<>();
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080083
84 @Before
85 public void before() throws Throwable {
86 mInstrumentation = InstrumentationRegistry.getInstrumentation();
87 mActivity = mActivityRule.getActivity();
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080088 }
89
Shu Chen77003422020-03-05 13:38:05 +080090 @After
91 public void after() throws Throwable {
92 for (MotionEvent event : mMotionEvents) {
93 event.recycle();
94 }
95 mMotionEvents.clear();
96 }
97
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080098 @Test
99 public void testCursorDrag_horizontal_whenTextViewContentsFitOnScreen() throws Throwable {
100 String text = "Hello world!";
101 onView(withId(R.id.textview)).perform(replaceText(text));
102 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
103
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800104 // Swipe left to right to drag the cursor. The cursor should end up at the position where
105 // the finger is lifted.
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800106 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("llo"), text.indexOf("!")));
107 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(11));
108
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800109 // Swipe right to left to drag the cursor. The cursor should end up at the position where
110 // the finger is lifted.
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800111 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("!"), text.indexOf("llo")));
112 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
113 }
114
115 @Test
116 public void testCursorDrag_horizontal_whenTextViewContentsLargerThanScreen() throws Throwable {
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800117 String text = "Hello world!\n\n"
118 + Strings.repeat("Bla\n\n", 200) + "Bye";
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800119 onView(withId(R.id.textview)).perform(replaceText(text));
120 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
121
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800122 // Swipe left to right to drag the cursor. The cursor should end up at the position where
123 // the finger is lifted.
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800124 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("llo"), text.indexOf("!")));
125 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(11));
126
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800127 // Swipe right to left to drag the cursor. The cursor should end up at the position where
128 // the finger is lifted.
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800129 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("!"), text.indexOf("llo")));
130 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
131 }
132
133 @Test
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800134 public void testCursorDrag_diagonal_whenTextViewContentsFitOnScreen() throws Throwable {
135 StringBuilder sb = new StringBuilder();
136 for (int i = 1; i <= 9; i++) {
137 sb.append("line").append(i).append("\n");
138 }
139 String text = sb.toString();
140 onView(withId(R.id.textview)).perform(replaceText(text));
141 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
142
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800143 // Swipe along a diagonal path. This should drag the cursor. Because we snap the finger to
144 // the handle as the touch moves downwards (and because we have some slop to avoid jumping
145 // across lines), the cursor position will end up higher than the finger position.
Nikita Dubrovskyb1ad3b62020-07-01 09:51:48 -0700146 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("line1"), text.indexOf("2")));
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800147 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("1")));
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800148
149 // Swipe right-down along a very steep diagonal path. This should not drag the cursor.
150 // Normally this would trigger a scroll, but since the full view fits on the screen there
151 // is nothing to scroll and the gesture will trigger a selection drag.
152 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("line1"), text.indexOf("7")));
153 onView(withId(R.id.textview)).check(hasSelection(not(emptyString())));
154
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800155 // Tap to clear the selection.
156 int index = text.indexOf("line9");
157 onView(withId(R.id.textview)).perform(clickOnTextAtIndex(index));
158 onView(withId(R.id.textview)).check(hasSelection(emptyString()));
159 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index));
160
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800161 // Swipe right-up along a very steep diagonal path. This should not drag the cursor.
162 // Normally this would trigger a scroll, but since the full view fits on the screen there
163 // is nothing to scroll and the gesture will trigger a selection drag.
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800164 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("line7"), text.indexOf("1")));
165 onView(withId(R.id.textview)).check(hasSelection(not(emptyString())));
166 }
167
168 @Test
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800169 public void testCursorDrag_diagonal_whenTextViewContentsLargerThanScreen() throws Throwable {
170 StringBuilder sb = new StringBuilder();
171 for (int i = 1; i <= 9; i++) {
172 sb.append("line").append(i).append("\n");
173 }
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800174 sb.append(Strings.repeat("0123456789\n", 400)).append("Last");
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800175 String text = sb.toString();
176 onView(withId(R.id.textview)).perform(replaceText(text));
177 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
178
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800179 // Swipe along a diagonal path. This should drag the cursor. Because we snap the finger to
180 // the handle as the touch moves downwards (and because we have some slop to avoid jumping
181 // across lines), the cursor position will end up higher than the finger position.
Nikita Dubrovskyb1ad3b62020-07-01 09:51:48 -0700182 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("line1"), text.indexOf("2")));
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800183 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("1")));
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800184
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800185 // Swipe right-down along a very steep diagonal path. This should not drag the cursor.
186 // Normally this would trigger a scroll up, but since the view is already at the top there
187 // is nothing to scroll and the gesture will trigger a selection drag.
188 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("line1"), text.indexOf("7")));
189 onView(withId(R.id.textview)).check(hasSelection(not(emptyString())));
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800190
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800191 // Tap to clear the selection.
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800192 int index = text.indexOf("line9");
193 onView(withId(R.id.textview)).perform(clickOnTextAtIndex(index));
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800194 onView(withId(R.id.textview)).check(hasSelection(emptyString()));
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800195 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index));
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800196
197 // Swipe right-up along a very steep diagonal path. This should not drag the cursor. This
198 // will trigger a downward scroll and the cursor position will not change.
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800199 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("line7"), text.indexOf("1")));
200 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index));
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800201 }
202
203 @Test
Nikita Dubrovsky81fa5e42020-06-23 15:33:17 -0700204 public void testCursorDrag_diagonal_thresholdConfig() throws Throwable {
205 TextView tv = mActivity.findViewById(R.id.textview);
206 Editor editor = tv.getEditorForTesting();
207
208 StringBuilder sb = new StringBuilder();
209 for (int i = 1; i <= 9; i++) {
210 sb.append("here is some text").append(i).append("\n");
211 }
212 sb.append(Strings.repeat("abcdefghij\n", 400)).append("Last");
213 String text = sb.toString();
214 onView(withId(R.id.textview)).perform(replaceText(text));
215
216 int index = text.indexOf("text9");
217 onView(withId(R.id.textview)).perform(clickOnTextAtIndex(index));
218 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index));
219
220 // Configure the drag direction threshold to require the drag to be exactly horizontal. With
221 // this set, a swipe that is slightly off horizontal should not trigger cursor drag.
222 editor.setCursorDragMinAngleFromVertical(90);
223 int startIdx = text.indexOf("5");
224 int endIdx = text.indexOf("here is some text3");
225 onView(withId(R.id.textview)).perform(dragOnText(startIdx, endIdx));
226 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(index));
227
228 // Configure the drag direction threshold to require the drag to be 45 degrees or more from
229 // vertical. With this set, the same swipe gesture as above should now trigger cursor drag.
230 editor.setCursorDragMinAngleFromVertical(45);
231 onView(withId(R.id.textview)).perform(dragOnText(startIdx, endIdx));
232 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(endIdx));
233 }
234
235 @Test
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800236 public void testCursorDrag_vertical_whenTextViewContentsFitOnScreen() throws Throwable {
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800237 String text = "012345_aaa\n"
238 + "0123456789\n"
239 + "012345_bbb\n"
240 + "0123456789\n"
241 + "012345_ccc\n"
242 + "0123456789\n"
243 + "012345_ddd";
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800244 onView(withId(R.id.textview)).perform(replaceText(text));
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800245
246 // Swipe up vertically. This should not drag the cursor. Since there's also nothing to
247 // scroll, the gesture will trigger a selection drag.
248 onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800249 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800250 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("bbb"), text.indexOf("aaa")));
251 onView(withId(R.id.textview)).check(hasSelection(not(emptyString())));
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800252
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800253 // Swipe down vertically. This should not drag the cursor. Since there's also nothing to
254 // scroll, the gesture will trigger a selection drag.
255 onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
256 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
257 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("ccc"), text.indexOf("ddd")));
258 onView(withId(R.id.textview)).check(hasSelection(not(emptyString())));
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800259 }
260
261 @Test
262 public void testCursorDrag_vertical_whenTextViewContentsLargerThanScreen() throws Throwable {
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800263 String text = "012345_aaa\n"
264 + "0123456789\n"
265 + "012345_bbb\n"
266 + "0123456789\n"
267 + "012345_ccc\n"
268 + "0123456789\n"
269 + "012345_ddd\n"
270 + Strings.repeat("0123456789\n", 400) + "012345_zzz";
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800271 onView(withId(R.id.textview)).perform(replaceText(text));
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800272 onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.indexOf("ddd")));
273 int initialCursorPosition = text.indexOf("ddd");
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800274 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(initialCursorPosition));
275
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800276 // Swipe up vertically. This should trigger a downward scroll.
277 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("bbb"), text.indexOf("aaa")));
278 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(initialCursorPosition));
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800279
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -0800280 // Swipe down vertically. This should trigger an upward scroll.
281 onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("ccc"), text.indexOf("ddd")));
282 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(initialCursorPosition));
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800283 }
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800284
285 @Test
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800286 public void testEditor_onTouchEvent_quickTapAfterDrag() throws Throwable {
287 String text = "Hi world!";
288 onView(withId(R.id.textview)).perform(replaceText(text));
289 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
290
291 TextView tv = mActivity.findViewById(R.id.textview);
292 Editor editor = tv.getEditorForTesting();
293
294 // Simulate a tap-and-drag gesture.
295 long event1Time = 1001;
Shu Chen77003422020-03-05 13:38:05 +0800296 MotionEvent event1 = downEvent(tv, event1Time, event1Time, 5f, 10f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800297 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event1));
298 assertFalse(editor.getInsertionController().isCursorBeingModified());
299 assertFalse(editor.getSelectionController().isCursorBeingModified());
300
301 long event2Time = 1002;
Shu Chen77003422020-03-05 13:38:05 +0800302 MotionEvent event2 = moveEvent(tv, event1Time, event2Time, 50f, 10f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800303 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event2));
304 assertTrue(editor.getInsertionController().isCursorBeingModified());
305 assertFalse(editor.getSelectionController().isCursorBeingModified());
306
307 long event3Time = 1003;
Shu Chen77003422020-03-05 13:38:05 +0800308 MotionEvent event3 = moveEvent(tv, event1Time, event3Time, 100f, 10f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800309 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event3));
310 assertTrue(editor.getInsertionController().isCursorBeingModified());
311 assertFalse(editor.getSelectionController().isCursorBeingModified());
312
313 long event4Time = 2004;
Shu Chen77003422020-03-05 13:38:05 +0800314 MotionEvent event4 = upEvent(tv, event1Time, event4Time, 100f, 10f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800315 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event4));
316 assertFalse(editor.getInsertionController().isCursorBeingModified());
317 assertFalse(editor.getSelectionController().isCursorBeingModified());
318
319 // Simulate a quick tap after the drag, near the location where the drag ended.
320 long event5Time = 2005;
Shu Chen77003422020-03-05 13:38:05 +0800321 MotionEvent event5 = downEvent(tv, event5Time, event5Time, 90f, 10f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800322 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event5));
323 assertFalse(editor.getInsertionController().isCursorBeingModified());
324 assertFalse(editor.getSelectionController().isCursorBeingModified());
325
326 long event6Time = 2006;
Shu Chen77003422020-03-05 13:38:05 +0800327 MotionEvent event6 = upEvent(tv, event5Time, event6Time, 90f, 10f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800328 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event6));
329 assertFalse(editor.getInsertionController().isCursorBeingModified());
330 assertFalse(editor.getSelectionController().isCursorBeingModified());
331
332 // Simulate another quick tap in the same location; now selection should be triggered.
333 long event7Time = 2007;
Shu Chen77003422020-03-05 13:38:05 +0800334 MotionEvent event7 = downEvent(tv, event7Time, event7Time, 90f, 10f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800335 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event7));
336 assertFalse(editor.getInsertionController().isCursorBeingModified());
337 assertTrue(editor.getSelectionController().isCursorBeingModified());
338 }
339
340 @Test
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -0800341 public void testEditor_onTouchEvent_mouseDrag() throws Throwable {
342 String text = "testEditor_onTouchEvent_mouseDrag";
343 onView(withId(R.id.textview)).perform(replaceText(text));
344 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
345
346 TextView tv = mActivity.findViewById(R.id.textview);
347 Editor editor = tv.getEditorForTesting();
348
349 // Simulate a mouse click and drag. This should NOT trigger a cursor drag.
350 long event1Time = 1001;
Shu Chen77003422020-03-05 13:38:05 +0800351 MotionEvent event1 = mouseDownEvent(tv, event1Time, event1Time, 20f, 30f);
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -0800352 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event1));
353 assertFalse(editor.getInsertionController().isCursorBeingModified());
354 assertFalse(editor.getSelectionController().isCursorBeingModified());
355
356 long event2Time = 1002;
Shu Chen77003422020-03-05 13:38:05 +0800357 MotionEvent event2 = mouseMoveEvent(tv, event1Time, event2Time, 120f, 30f);
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -0800358 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event2));
359 assertFalse(editor.getInsertionController().isCursorBeingModified());
360 assertTrue(editor.getSelectionController().isCursorBeingModified());
361
362 long event3Time = 1003;
Shu Chen77003422020-03-05 13:38:05 +0800363 MotionEvent event3 = mouseUpEvent(tv, event1Time, event3Time, 120f, 30f);
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -0800364 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event3));
365 assertFalse(editor.getInsertionController().isCursorBeingModified());
366 assertFalse(editor.getSelectionController().isCursorBeingModified());
367 }
368
369 @Test
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800370 public void testEditor_onTouchEvent_cursorDrag() throws Throwable {
371 String text = "testEditor_onTouchEvent_cursorDrag";
372 onView(withId(R.id.textview)).perform(replaceText(text));
373 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
374
375 TextView tv = mActivity.findViewById(R.id.textview);
376 Editor editor = tv.getEditorForTesting();
377
378 // Simulate a tap-and-drag gesture. This should trigger a cursor drag.
379 long event1Time = 1001;
Shu Chen77003422020-03-05 13:38:05 +0800380 MotionEvent event1 = downEvent(tv, event1Time, event1Time, 20f, 30f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800381 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event1));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800382 assertFalse(editor.getInsertionController().isCursorBeingModified());
383 assertFalse(editor.getSelectionController().isCursorBeingModified());
384
385 long event2Time = 1002;
Shu Chen77003422020-03-05 13:38:05 +0800386 MotionEvent event2 = moveEvent(tv, event1Time, event2Time, 21f, 30f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800387 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event2));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800388 assertFalse(editor.getInsertionController().isCursorBeingModified());
389 assertFalse(editor.getSelectionController().isCursorBeingModified());
390
391 long event3Time = 1003;
Shu Chen77003422020-03-05 13:38:05 +0800392 MotionEvent event3 = moveEvent(tv, event1Time, event3Time, 120f, 30f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800393 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event3));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800394 assertTrue(editor.getInsertionController().isCursorBeingModified());
395 assertFalse(editor.getSelectionController().isCursorBeingModified());
396
397 long event4Time = 1004;
Shu Chen77003422020-03-05 13:38:05 +0800398 MotionEvent event4 = upEvent(tv, event1Time, event4Time, 120f, 30f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800399 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event4));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800400 assertFalse(editor.getInsertionController().isCursorBeingModified());
401 assertFalse(editor.getSelectionController().isCursorBeingModified());
402 }
403
404 @Test
405 public void testEditor_onTouchEvent_selectionDrag() throws Throwable {
406 String text = "testEditor_onTouchEvent_selectionDrag";
407 onView(withId(R.id.textview)).perform(replaceText(text));
408 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
409
410 TextView tv = mActivity.findViewById(R.id.textview);
411 Editor editor = tv.getEditorForTesting();
412
413 // Simulate a double-tap followed by a drag. This should trigger a selection drag.
414 long event1Time = 1001;
Shu Chen77003422020-03-05 13:38:05 +0800415 MotionEvent event1 = downEvent(tv, event1Time, event1Time, 20f, 30f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800416 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event1));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800417 assertFalse(editor.getInsertionController().isCursorBeingModified());
418 assertFalse(editor.getSelectionController().isCursorBeingModified());
419
420 long event2Time = 1002;
Shu Chen77003422020-03-05 13:38:05 +0800421 MotionEvent event2 = upEvent(tv, event1Time, event2Time, 20f, 30f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800422 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event2));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800423 assertFalse(editor.getInsertionController().isCursorBeingModified());
424 assertFalse(editor.getSelectionController().isCursorBeingModified());
425
426 long event3Time = 1003;
Shu Chen77003422020-03-05 13:38:05 +0800427 MotionEvent event3 = downEvent(tv, event3Time, event3Time, 20f, 30f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800428 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event3));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800429 assertFalse(editor.getInsertionController().isCursorBeingModified());
430 assertTrue(editor.getSelectionController().isCursorBeingModified());
431
432 long event4Time = 1004;
Shu Chen77003422020-03-05 13:38:05 +0800433 MotionEvent event4 = moveEvent(tv, event3Time, event4Time, 120f, 30f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800434 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event4));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800435 assertFalse(editor.getInsertionController().isCursorBeingModified());
436 assertTrue(editor.getSelectionController().isCursorBeingModified());
437
438 long event5Time = 1005;
Shu Chen77003422020-03-05 13:38:05 +0800439 MotionEvent event5 = upEvent(tv, event3Time, event5Time, 120f, 30f);
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -0800440 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event5));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800441 assertFalse(editor.getInsertionController().isCursorBeingModified());
442 assertFalse(editor.getSelectionController().isCursorBeingModified());
443 }
444
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800445 @Suppress // b/149712851
Nikita Dubrovsky99b55fa2020-01-12 20:57:51 -0800446 @Test // Reproduces b/147366705
447 public void testCursorDrag_nonSelectableTextView() throws Throwable {
448 String text = "Hello world!";
449 TextView tv = mActivity.findViewById(R.id.nonselectable_textview);
450 tv.setText(text);
451 Editor editor = tv.getEditorForTesting();
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800452 assertThat(editor).isNotNull();
Nikita Dubrovsky99b55fa2020-01-12 20:57:51 -0800453
454 // Simulate a tap. No error should be thrown.
455 long event1Time = 1001;
Shu Chen77003422020-03-05 13:38:05 +0800456 MotionEvent event1 = downEvent(tv, event1Time, event1Time, 20f, 30f);
Nikita Dubrovsky99b55fa2020-01-12 20:57:51 -0800457 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event1));
458
459 // Swipe left to right. No error should be thrown.
460 onView(withId(R.id.nonselectable_textview)).perform(
461 dragOnText(text.indexOf("llo"), text.indexOf("!")));
462 }
463
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800464 @Test
465 public void testCursorDrag_slop() throws Throwable {
466 String text = "line1: This is the 1st line: A\n"
467 + "line2: This is the 2nd line: B\n"
468 + "line3: This is the 3rd line: C\n";
469 onView(withId(R.id.textview)).perform(replaceText(text));
470 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
471 TextView tv = mActivity.findViewById(R.id.textview);
472
473 // Simulate a drag where the finger moves slightly up and down (above and below the original
474 // line where the drag started). The cursor should just move along the original line without
475 // jumping up or down across lines.
476 MotionEventInfo[] events = new MotionEventInfo[]{
477 // Start dragging along the second line
478 motionEventInfo(text.indexOf("line2"), 1.0f),
479 motionEventInfo(text.indexOf("This is the 2nd"), 1.0f),
480 // Move to the bottom of the first line; cursor should remain on second line
481 motionEventInfo(text.indexOf("he 1st"), 0.0f, text.indexOf("he 2nd")),
482 // Move to the top of the third line; cursor should remain on second line
483 motionEventInfo(text.indexOf("e: C"), 1.0f, text.indexOf("e: B")),
484 motionEventInfo(text.indexOf("B"), 0.0f)
485 };
486 simulateDrag(tv, events, true);
487 }
488
489 @Test
490 public void testCursorDrag_snapToHandle() throws Throwable {
491 String text = "line1: This is the 1st line: A\n"
492 + "line2: This is the 2nd line: B\n"
Shu Chen77003422020-03-05 13:38:05 +0800493 + "line3: This is the 3rd line: C\n"
494 + "line4: This is the 4th line: D\n";
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800495 onView(withId(R.id.textview)).perform(replaceText(text));
496 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
497 TextView tv = mActivity.findViewById(R.id.textview);
498
499 // When the drag motion moves downward, we delay jumping to the lower line to allow the
500 // user's touch to snap to the cursor's handle. Once the finger is over the handle, we
501 // position the cursor above the user's actual touch (offset such that the finger remains
502 // over the handle rather than on top of the cursor vertical bar). This improves the
503 // visibility of the cursor and the text underneath.
504 MotionEventInfo[] events = new MotionEventInfo[]{
505 // Start dragging along the first line
506 motionEventInfo(text.indexOf("line1"), 1.0f),
507 motionEventInfo(text.indexOf("This is the 1st"), 1.0f),
Shu Chen77003422020-03-05 13:38:05 +0800508 // Move to the middle of the fourth line; cursor should end up on second line
509 motionEventInfo(text.indexOf("he 4th"), 0.5f, text.indexOf("he 2nd")),
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800510 // Move to the middle of the second line; cursor should end up on the first line
511 motionEventInfo(text.indexOf("he 2nd"), 0.5f, text.indexOf("he 1st"))
512 };
513 simulateDrag(tv, events, true);
514
515 // If the drag motion hasn't moved downward (ie, we haven't had a chance to snap to the
516 // handle), we position the cursor directly at the touch position.
517 events = new MotionEventInfo[]{
518 // Start dragging along the third line
519 motionEventInfo(text.indexOf("line3"), 1.0f),
520 motionEventInfo(text.indexOf("This is the 3rd"), 1.0f),
521 // Move to the middle of the second line; cursor should end up on the second line
522 motionEventInfo(text.indexOf("he 2nd"), 0.5f, text.indexOf("he 2nd")),
523 };
524 simulateDrag(tv, events, true);
525 }
526
Nikita Dubrovskye8b3b312020-03-26 21:10:39 -0700527 @Suppress // b/152574363
Shu Chen77003422020-03-05 13:38:05 +0800528 @Test
Shu Chenafbcf852020-03-10 08:19:07 +0800529 public void testLineChangeSlop() throws Throwable {
530 TextView tv = mActivity.findViewById(R.id.textview);
531 Spannable s = new SpannableString("a\nb\nc");
532 s.setSpan(new AbsoluteSizeSpan(10), 2, 4, SPAN_INCLUSIVE_EXCLUSIVE);
533 s.setSpan(new AbsoluteSizeSpan(32), 4, 5, SPAN_INCLUSIVE_EXCLUSIVE);
534 mInstrumentation.runOnMainSync(() -> tv.setText(s));
535
536 Layout layout = tv.getLayout();
537 Editor editor = tv.getEditorForTesting();
538 final float verticalOffset = tv.getExtendedPaddingTop();
539 editor.setLineChangeSlopMinMaxForTesting(30, 65);
540 // Hit top part of upper line, jump to upper line.
541 assertThat(editor.getCurrentLineAdjustedForSlop(layout, 1, 5 + verticalOffset))
542 .isEqualTo(0);
543 // Hit bottom part of upper line, stay at current line.
544 assertThat(editor.getCurrentLineAdjustedForSlop(layout, 1, 40 + verticalOffset))
545 .isEqualTo(1);
546 // Hit current line, stay at current line.
547 assertThat(editor.getCurrentLineAdjustedForSlop(layout, 1, 70 + verticalOffset))
548 .isEqualTo(1);
549 // Hit top part of lower line, stay at current line.
550 assertThat(editor.getCurrentLineAdjustedForSlop(layout, 1, 85 + verticalOffset))
551 .isEqualTo(1);
552 // Hit bottom part of lower line, jump to lower line.
553 assertThat(editor.getCurrentLineAdjustedForSlop(layout, 1, 110 + verticalOffset))
554 .isEqualTo(2);
555 // Hit lower line of lower line, jump to target line.
556 assertThat(editor.getCurrentLineAdjustedForSlop(layout, 0, 110 + verticalOffset))
557 .isEqualTo(2);
558 }
559
560 @Test
Shu Cheneb8b1ba2020-04-04 14:46:50 +0800561 public void testCursorDrag_multiTouch() throws Throwable {
562 String text = "line1: This is the 1st line: A";
563 onView(withId(R.id.textview)).perform(replaceText(text));
564 TextView tv = mActivity.findViewById(R.id.textview);
565 Editor editor = tv.getEditorForTesting();
566 final int startIndex = text.indexOf("1st line");
567 Layout layout = tv.getLayout();
568 final float cursorStartX =
569 layout.getPrimaryHorizontal(startIndex) + tv.getTotalPaddingLeft();
570 final float cursorStartY = layout.getLineTop(1) + tv.getTotalPaddingTop();
571
572 // Taps to show the insertion handle.
573 tapAtPoint(tv, cursorStartX, cursorStartY);
574 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(startIndex));
575 View handleView = editor.getInsertionController().getHandle();
576
577 // Taps & holds the insertion handle.
578 long handleDownTime = sTicker.addAndGet(10_000);
579 long eventTime = handleDownTime;
580 dispatchTouchEvent(handleView, downEvent(handleView, handleDownTime, eventTime++, 0, 0));
581
582 // Tries to Drag the cursor, with the pointer id > 0 (meaning the 2nd finger).
583 long cursorDownTime = eventTime++;
584 dispatchTouchEvent(tv, obtainTouchEventWithPointerId(
585 tv, MotionEvent.ACTION_DOWN, cursorDownTime, eventTime++, 1,
586 cursorStartX - 50, cursorStartY));
587 dispatchTouchEvent(tv, obtainTouchEventWithPointerId(
588 tv, MotionEvent.ACTION_MOVE, cursorDownTime, eventTime++, 1,
589 cursorStartX - 100, cursorStartY));
590 dispatchTouchEvent(tv, obtainTouchEventWithPointerId(
591 tv, MotionEvent.ACTION_UP, cursorDownTime, eventTime++, 1,
592 cursorStartX - 100, cursorStartY));
593
594 // Checks the cursor drag doesn't work while the handle is being hold.
595 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(startIndex));
596
597 // Finger up on the insertion handle.
598 dispatchTouchEvent(handleView, upEvent(handleView, handleDownTime, eventTime, 0, 0));
599 }
600
601 @Test
Shu Chen77003422020-03-05 13:38:05 +0800602 public void testCursorDrag_snapDistance() throws Throwable {
603 String text = "line1: This is the 1st line: A\n"
604 + "line2: This is the 2nd line: B\n"
605 + "line3: This is the 3rd line: C\n";
606 onView(withId(R.id.textview)).perform(replaceText(text));
607 TextView tv = mActivity.findViewById(R.id.textview);
608 Editor editor = tv.getEditorForTesting();
609 final int startIndex = text.indexOf("he 2nd");
610 Layout layout = tv.getLayout();
611 final float cursorStartX = layout.getPrimaryHorizontal(startIndex) + tv.getTotalPaddingLeft();
612 final float cursorStartY = layout.getLineTop(1) + tv.getTotalPaddingTop();
613 final float dragHandleStartX = 20;
614 final float dragHandleStartY = 20;
615
616 // Drag the handle from the 2nd line to the 3rd line.
617 tapAtPoint(tv, cursorStartX, cursorStartY);
618 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(startIndex));
619 View handleView = editor.getInsertionController().getHandle();
620 final int rawYOfHandleDrag = dragDownUntilLineChange(
621 handleView, dragHandleStartX, dragHandleStartY, tv.getSelectionStart());
622
623 // Drag the cursor from the 2nd line to the 3rd line.
624 tapAtPoint(tv, cursorStartX, cursorStartY);
625 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(startIndex));
626 final int rawYOfCursorDrag =
627 dragDownUntilLineChange(tv, cursorStartX, cursorStartY, tv.getSelectionStart());
628
629 // Drag the handle with touch through from the 2nd line to the 3rd line.
630 tv.getEditorForTesting().setFlagInsertionHandleGesturesEnabled(true);
631 tapAtPoint(tv, cursorStartX, cursorStartY);
632 onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(startIndex));
633 handleView = editor.getInsertionController().getHandle();
634 int rawYOfHandleDragWithTouchThrough =
635 dragDownUntilLineChange(handleView, dragHandleStartX, dragHandleStartY, tv.getSelectionStart());
636
637 String msg = String.format(
638 "rawYOfHandleDrag: %d, rawYOfCursorDrag: %d, rawYOfHandleDragWithTouchThrough: %d",
639 rawYOfHandleDrag, rawYOfCursorDrag, rawYOfHandleDragWithTouchThrough);
640 final int max = Math.max(
641 rawYOfCursorDrag, Math.max(rawYOfHandleDrag, rawYOfHandleDragWithTouchThrough));
642 final int min = Math.min(
643 rawYOfCursorDrag, Math.min(rawYOfHandleDrag, rawYOfHandleDragWithTouchThrough));
644 // The drag step is 5 pixels in dragDownUntilLineChange().
645 // The difference among the 3 raw Y values should be no bigger than the drag step.
646 assertWithMessage(msg).that(max - min).isLessThan(6);
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800647 }
648
Shu Chen77003422020-03-05 13:38:05 +0800649 private void dispatchTouchEvent(View view, MotionEvent event) {
650 mInstrumentation.runOnMainSync(() -> view.dispatchTouchEvent(event));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800651 }
652
Shu Chen77003422020-03-05 13:38:05 +0800653 private void tapAtPoint(TextView tv, final float x, final float y) {
654 long downTime = sTicker.addAndGet(10_000);
655 dispatchTouchEvent(tv, downEvent(tv, downTime, downTime, x, y));
656 dispatchTouchEvent(tv, upEvent(tv, downTime, downTime + 1, x, y));
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -0800657 }
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -0800658
Shu Chen77003422020-03-05 13:38:05 +0800659 private int dragDownUntilLineChange(View view, final float startX, final float startY,
660 final int startOffset) {
661 TextView tv = mActivity.findViewById(R.id.textview);
662 final int startLine = tv.getLayout().getLineForOffset(startOffset);
663
664 int rawY = 0;
665 long downTime = sTicker.addAndGet(10_000);
666 long eventTime = downTime;
667 // Move horizontally first to initiate the cursor drag.
668 dispatchTouchEvent(view, downEvent(view, downTime, eventTime++, startX, startY));
669 dispatchTouchEvent(view, moveEvent(view, downTime, eventTime++, startX + 50, startY));
670 dispatchTouchEvent(view, moveEvent(view, downTime, eventTime++, startX, startY));
671 // Move downwards 5 pixels at a time until a line change occurs.
672 for (int i = 0; i < 200; i++) {
673 MotionEvent ev = moveEvent(view, downTime, eventTime++, startX, startY + i * 5);
674 rawY = (int) ev.getRawY();
675 dispatchTouchEvent(view, ev);
676 if (tv.getLayout().getLineForOffset(tv.getSelectionStart()) > startLine) {
677 break;
678 }
679 }
680 String msg = String.format("The cursor didn't jump from %d!", startOffset);
681 assertWithMessage(msg).that(
682 tv.getLayout().getLineForOffset(tv.getSelectionStart())).isGreaterThan(startLine);
683 dispatchTouchEvent(view, upEvent(view, downTime, eventTime, startX, startY));
684 return rawY;
685 }
686
687 private MotionEvent obtainTouchEvent(
688 View view, int action, long downTime, long eventTime, float x, float y) {
689 Rect r = new Rect();
690 view.getBoundsOnScreen(r);
691 float rawX = x + r.left;
692 float rawY = y + r.top;
693 MotionEvent event =
694 MotionEvent.obtain(downTime, eventTime, action, rawX, rawY, 0);
695 view.toLocalMotionEvent(event);
696 mMotionEvents.add(event);
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -0800697 return event;
698 }
699
Shu Cheneb8b1ba2020-04-04 14:46:50 +0800700 private MotionEvent obtainTouchEventWithPointerId(
701 View view, int action, long downTime, long eventTime, int pointerId, float x, float y) {
702 Rect r = new Rect();
703 view.getBoundsOnScreen(r);
704 float rawX = x + r.left;
705 float rawY = y + r.top;
706 MotionEvent.PointerCoords coordinates = new MotionEvent.PointerCoords();
707 coordinates.x = rawX;
708 coordinates.y = rawY;
709 MotionEvent event = MotionEvent.obtain(
710 downTime, eventTime, action, 1, new int[] {pointerId},
711 new MotionEvent.PointerCoords[] {coordinates},
712 0, 1f, 1f, 0, 0, 0, 0);
713 view.toLocalMotionEvent(event);
714 mMotionEvents.add(event);
715 return event;
716 }
717
Shu Chen77003422020-03-05 13:38:05 +0800718 private MotionEvent obtainMouseEvent(
719 View view, int action, long downTime, long eventTime, float x, float y) {
720 MotionEvent event = obtainTouchEvent(view, action, downTime, eventTime, x, y);
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -0800721 event.setSource(InputDevice.SOURCE_MOUSE);
Shu Chen77003422020-03-05 13:38:05 +0800722 if (action != MotionEvent.ACTION_UP) {
723 event.setButtonState(MotionEvent.BUTTON_PRIMARY);
724 }
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -0800725 return event;
726 }
727
Shu Chen77003422020-03-05 13:38:05 +0800728 private MotionEvent downEvent(View view, long downTime, long eventTime, float x, float y) {
729 return obtainTouchEvent(view, MotionEvent.ACTION_DOWN, downTime, eventTime, x, y);
730 }
731
732 private MotionEvent moveEvent(View view, long downTime, long eventTime, float x, float y) {
733 return obtainTouchEvent(view, MotionEvent.ACTION_MOVE, downTime, eventTime, x, y);
734 }
735
736 private MotionEvent upEvent(View view, long downTime, long eventTime, float x, float y) {
737 return obtainTouchEvent(view, MotionEvent.ACTION_UP, downTime, eventTime, x, y);
738 }
739
740 private MotionEvent mouseDownEvent(View view, long downTime, long eventTime, float x, float y) {
741 return obtainMouseEvent(view, MotionEvent.ACTION_DOWN, downTime, eventTime, x, y);
742 }
743
744 private MotionEvent mouseMoveEvent(View view, long downTime, long eventTime, float x, float y) {
745 return obtainMouseEvent(view, MotionEvent.ACTION_MOVE, downTime, eventTime, x, y);
746 }
747
748 private MotionEvent mouseUpEvent(View view, long downTime, long eventTime, float x, float y) {
749 return obtainMouseEvent(view, MotionEvent.ACTION_UP, downTime, eventTime, x, y);
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -0800750 }
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800751
752 public static MotionEventInfo motionEventInfo(int index, float ratioToLineTop) {
753 return new MotionEventInfo(index, ratioToLineTop, index);
754 }
755
756 public static MotionEventInfo motionEventInfo(int index, float ratioToLineTop,
757 int expectedCursorIndex) {
758 return new MotionEventInfo(index, ratioToLineTop, expectedCursorIndex);
759 }
760
761 private static class MotionEventInfo {
762 public final int index;
763 public final float ratioToLineTop; // 0.0 = bottom of line, 0.5 = middle of line, etc
764 public final int expectedCursorIndex;
765
766 private MotionEventInfo(int index, float ratioToLineTop, int expectedCursorIndex) {
767 this.index = index;
768 this.ratioToLineTop = ratioToLineTop;
769 this.expectedCursorIndex = expectedCursorIndex;
770 }
771
772 public float[] getCoordinates(TextView textView) {
773 Layout layout = textView.getLayout();
774 int line = layout.getLineForOffset(index);
775 float x = layout.getPrimaryHorizontal(index) + textView.getTotalPaddingLeft();
776 int bottom = layout.getLineBottom(line);
777 int top = layout.getLineTop(line);
778 float y = bottom - ((bottom - top) * ratioToLineTop) + textView.getTotalPaddingTop();
779 return new float[]{x, y};
780 }
781 }
782
783 private void simulateDrag(TextView tv, MotionEventInfo[] events, boolean runAssertions)
784 throws Exception {
785 Editor editor = tv.getEditorForTesting();
786
787 float[] downCoords = events[0].getCoordinates(tv);
788 long downEventTime = sTicker.addAndGet(10_000);
Shu Chen77003422020-03-05 13:38:05 +0800789 MotionEvent downEvent = downEvent(tv, downEventTime, downEventTime,
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800790 downCoords[0], downCoords[1]);
791 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(downEvent));
792
793 for (int i = 1; i < events.length; i++) {
794 float[] moveCoords = events[i].getCoordinates(tv);
795 long eventTime = downEventTime + i;
Shu Chen77003422020-03-05 13:38:05 +0800796 MotionEvent event = moveEvent(tv, downEventTime, eventTime, moveCoords[0],
797 moveCoords[1]);
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800798 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event));
799 assertCursorPosition(tv, events[i].expectedCursorIndex, runAssertions);
800 }
801
802 MotionEventInfo lastEvent = events[events.length - 1];
803 float[] upCoords = lastEvent.getCoordinates(tv);
804 long upEventTime = downEventTime + events.length;
Shu Chen77003422020-03-05 13:38:05 +0800805 MotionEvent upEvent = upEvent(tv, downEventTime, upEventTime, upCoords[0], upCoords[1]);
Nikita Dubrovsky7c583592020-02-16 15:54:23 -0800806 mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(upEvent));
807 }
808
809 private static void assertCursorPosition(TextView tv, int expectedPosition,
810 boolean runAssertions) {
811 String textAfterExpectedPos = getTextAfterIndex(tv, expectedPosition, 15);
812 String textAfterActualPos = getTextAfterIndex(tv, tv.getSelectionStart(), 15);
813 String msg = "Expected cursor at " + expectedPosition + ", just before \""
814 + textAfterExpectedPos + "\". Cursor is at " + tv.getSelectionStart()
815 + ", just before \"" + textAfterActualPos + "\".";
816 Log.d(LOG_TAG, msg);
817 if (runAssertions) {
818 assertWithMessage(msg).that(tv.getSelectionStart()).isEqualTo(expectedPosition);
819 assertThat(tv.getSelectionEnd()).isEqualTo(expectedPosition);
820 }
821 }
822
823 private static String getTextAfterIndex(TextView tv, int position, int maxLength) {
824 int end = Math.min(position + maxLength, tv.getText().length());
825 try {
826 String afterPosition = tv.getText().subSequence(position, end).toString();
827 if (afterPosition.indexOf('\n') > 0) {
828 afterPosition = afterPosition.substring(0, afterPosition.indexOf('\n'));
829 }
830 return afterPosition;
831 } catch (StringIndexOutOfBoundsException e) {
832 Log.d(LOG_TAG, "Invalid target position: position=" + position + ", length="
833 + tv.getText().length() + ", end=" + end);
834 return "";
835 }
836 }
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800837}