TextViewActivityTest.java revision 080c8542b68cf17a0441862c404cb49ce0e86cfe
1/*
2 * Copyright (C) 2015 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
19import static android.support.test.espresso.Espresso.onView;
20import static android.support.test.espresso.action.ViewActions.click;
21import static android.support.test.espresso.action.ViewActions.longClick;
22import static android.support.test.espresso.action.ViewActions.pressKey;
23import static android.support.test.espresso.action.ViewActions.replaceText;
24import static android.support.test.espresso.assertion.ViewAssertions.matches;
25import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
26import static android.support.test.espresso.matcher.ViewMatchers.withId;
27import static android.support.test.espresso.matcher.ViewMatchers.withText;
28import static android.widget.espresso.CustomViewActions.longPressAtRelativeCoordinates;
29import static android.widget.espresso.DragHandleUtils.onHandleView;
30import static android.widget.espresso.FloatingToolbarEspressoUtils
31        .assertFloatingToolbarContainsItem;
32import static android.widget.espresso.FloatingToolbarEspressoUtils
33        .assertFloatingToolbarDoesNotContainItem;
34import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed;
35import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarItemIndex;
36import static android.widget.espresso.FloatingToolbarEspressoUtils.clickFloatingToolbarItem;
37import static android.widget.espresso.FloatingToolbarEspressoUtils.sleepForFloatingToolbarPopup;
38import static android.widget.espresso.TextViewActions.Handle;
39import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
40import static android.widget.espresso.TextViewActions.doubleClickOnTextAtIndex;
41import static android.widget.espresso.TextViewActions.doubleTapAndDragOnText;
42import static android.widget.espresso.TextViewActions.dragHandle;
43import static android.widget.espresso.TextViewActions.longPressAndDragOnText;
44import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex;
45import static android.widget.espresso.TextViewAssertions.doesNotHaveStyledText;
46import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
47import static android.widget.espresso.TextViewAssertions.hasSelection;
48
49import static junit.framework.Assert.assertFalse;
50import static junit.framework.Assert.assertTrue;
51
52import static org.hamcrest.Matchers.anyOf;
53import static org.hamcrest.Matchers.is;
54import static org.mockito.Matchers.any;
55import static org.mockito.Mockito.mock;
56import static org.mockito.Mockito.never;
57import static org.mockito.Mockito.verify;
58import static org.mockito.Mockito.when;
59
60import android.app.Activity;
61import android.app.Instrumentation;
62import android.content.ClipData;
63import android.content.ClipboardManager;
64import android.support.test.InstrumentationRegistry;
65import android.support.test.espresso.action.EspressoKey;
66import android.support.test.filters.MediumTest;
67import android.support.test.rule.ActivityTestRule;
68import android.support.test.runner.AndroidJUnit4;
69import android.support.test.uiautomator.UiDevice;
70import android.test.suitebuilder.annotation.Suppress;
71import android.text.InputType;
72import android.text.Selection;
73import android.text.Spannable;
74import android.text.SpannableString;
75import android.text.method.LinkMovementMethod;
76import android.view.ActionMode;
77import android.view.KeyEvent;
78import android.view.Menu;
79import android.view.MenuItem;
80import android.view.textclassifier.TextClassificationManager;
81import android.view.textclassifier.TextClassifier;
82import android.view.textclassifier.TextLinks;
83import android.view.textclassifier.TextLinksParams;
84import android.widget.espresso.CustomViewActions.RelativeCoordinatesProvider;
85
86import com.android.frameworks.coretests.R;
87
88import org.junit.Before;
89import org.junit.Rule;
90import org.junit.Test;
91import org.junit.runner.RunWith;
92
93/**
94 * Tests the TextView widget from an Activity
95 */
96@RunWith(AndroidJUnit4.class)
97@MediumTest
98public class TextViewActivityTest {
99
100    @Rule
101    public ActivityTestRule<TextViewActivity> mActivityRule = new ActivityTestRule<>(
102            TextViewActivity.class);
103
104    private Activity mActivity;
105    private Instrumentation mInstrumentation;
106
107    @Before
108    public void setUp() {
109        mActivity = mActivityRule.getActivity();
110        mInstrumentation = InstrumentationRegistry.getInstrumentation();
111        mActivity.getSystemService(TextClassificationManager.class)
112                .setTextClassifier(TextClassifier.NO_OP);
113    }
114
115    @Test
116    public void testTypedTextIsOnScreen() {
117        final String helloWorld = "Hello world!";
118        // We use replaceText instead of typeTextIntoFocusedView to input text to avoid
119        // unintentional interactions with software keyboard.
120        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
121
122        onView(withId(R.id.textview)).check(matches(withText(helloWorld)));
123    }
124    @Test
125    public void testPositionCursorAtTextAtIndex() {
126        final String helloWorld = "Hello world!";
127        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
128        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(helloWorld.indexOf("world")));
129
130        // Delete text at specified index and see if we got the right one.
131        onView(withId(R.id.textview)).perform(pressKey(KeyEvent.KEYCODE_FORWARD_DEL));
132        onView(withId(R.id.textview)).check(matches(withText("Hello orld!")));
133    }
134
135    @Test
136    public void testPositionCursorAtTextAtIndex_arabic() {
137        // Arabic text. The expected cursorable boundary is
138        // | \u0623 \u064F | \u067A | \u0633 \u0652 |
139        final String text = "\u0623\u064F\u067A\u0633\u0652";
140        onView(withId(R.id.textview)).perform(replaceText(text));
141
142        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
143        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
144        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(1));
145        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(0), is(2))));
146        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(2));
147        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
148        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(3));
149        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(3));
150        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(4));
151        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(3), is(5))));
152        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(5));
153        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(5));
154    }
155
156    @Test
157    public void testPositionCursorAtTextAtIndex_devanagari() {
158        // Devanagari text. The expected cursorable boundary is | \u0915 \u093E |
159        final String text = "\u0915\u093E";
160        onView(withId(R.id.textview)).perform(replaceText(text));
161
162        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(0));
163        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
164        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(1));
165        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(anyOf(is(0), is(2))));
166        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(2));
167        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
168    }
169
170    @Test
171    public void testLongPressToSelect() {
172        final String helloWorld = "Hello Kirk!";
173        onView(withId(R.id.textview)).perform(click());
174        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
175        onView(withId(R.id.textview)).perform(
176                longPressOnTextAtIndex(helloWorld.indexOf("Kirk")));
177
178        onView(withId(R.id.textview)).check(hasSelection("Kirk"));
179    }
180
181    @Test
182    public void testLongPressEmptySpace() {
183        final String helloWorld = "Hello big round sun!";
184        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
185        // Move cursor somewhere else
186        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(helloWorld.indexOf("big")));
187        // Long-press at end of line.
188        onView(withId(R.id.textview)).perform(longPressAtRelativeCoordinates(
189                RelativeCoordinatesProvider.HorizontalReference.RIGHT, -5,
190                RelativeCoordinatesProvider.VerticalReference.CENTER, 0));
191
192        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(helloWorld.length()));
193    }
194
195    @Test
196    public void testLongPressAndDragToSelect() {
197        final String helloWorld = "Hello little handsome boy!";
198        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
199        onView(withId(R.id.textview)).perform(
200                longPressAndDragOnText(helloWorld.indexOf("little"), helloWorld.indexOf(" boy!")));
201
202        onView(withId(R.id.textview)).check(hasSelection("little handsome"));
203    }
204
205    @Test
206    public void testLongPressAndDragToSelect_emoji() {
207        final String text = "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03";
208        onView(withId(R.id.textview)).perform(replaceText(text));
209
210        onView(withId(R.id.textview)).perform(longPressAndDragOnText(4, 6));
211        onView(withId(R.id.textview)).check(hasSelection("\uD83D\uDE02"));
212
213        onView(withId(R.id.textview)).perform(click());
214
215        onView(withId(R.id.textview)).perform(longPressAndDragOnText(4, 2));
216        onView(withId(R.id.textview)).check(hasSelection("\uD83D\uDE01"));
217    }
218
219    @Test
220    public void testDragAndDrop() {
221        final String text = "abc def ghi.";
222        onView(withId(R.id.textview)).perform(replaceText(text));
223        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf("e")));
224
225        onView(withId(R.id.textview)).perform(
226                longPressAndDragOnText(text.indexOf("e"), text.length()));
227
228        onView(withId(R.id.textview)).check(matches(withText("abc ghi.def")));
229        onView(withId(R.id.textview)).check(hasSelection(""));
230        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex("abc ghi.def".length()));
231
232        // Test undo returns to the original state.
233        onView(withId(R.id.textview)).perform(pressKey(
234                (new EspressoKey.Builder()).withCtrlPressed(true).withKeyCode(KeyEvent.KEYCODE_Z)
235                        .build()));
236        onView(withId(R.id.textview)).check(matches(withText(text)));
237    }
238
239    @Test
240    public void testDoubleTapToSelect() {
241        final String helloWorld = "Hello SuetYi!";
242        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
243
244        onView(withId(R.id.textview)).perform(
245                doubleClickOnTextAtIndex(helloWorld.indexOf("SuetYi")));
246
247        onView(withId(R.id.textview)).check(hasSelection("SuetYi"));
248    }
249
250    @Test
251    public void testDoubleTapAndDragToSelect() {
252        final String helloWorld = "Hello young beautiful person!";
253        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
254        onView(withId(R.id.textview)).perform(doubleTapAndDragOnText(helloWorld.indexOf("young"),
255                        helloWorld.indexOf(" person!")));
256
257        onView(withId(R.id.textview)).check(hasSelection("young beautiful"));
258    }
259
260    @Test
261    public void testDoubleTapAndDragToSelect_multiLine() {
262        final String helloWorld = "abcd\n" + "efg\n" + "hijklm\n" + "nop";
263        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
264        onView(withId(R.id.textview)).perform(
265                doubleTapAndDragOnText(helloWorld.indexOf("m"), helloWorld.indexOf("a")));
266        onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijklm"));
267    }
268
269    @Test
270    public void testSelectBackwordsByTouch() {
271        final String helloWorld = "Hello king of the Jungle!";
272        onView(withId(R.id.textview)).perform(replaceText(helloWorld));
273        onView(withId(R.id.textview)).perform(
274                doubleTapAndDragOnText(helloWorld.indexOf(" Jungle!"), helloWorld.indexOf("king")));
275
276        onView(withId(R.id.textview)).check(hasSelection("king of the"));
277    }
278
279    @Test
280    public void testToolbarAppearsAfterSelection() {
281        final String text = "Toolbar appears after selection.";
282        onView(withId(R.id.textview)).perform(replaceText(text));
283        onView(withId(R.id.textview)).perform(
284                longPressOnTextAtIndex(text.indexOf("appears")));
285
286        sleepForFloatingToolbarPopup();
287        assertFloatingToolbarIsDisplayed();
288    }
289
290    @Test
291    public void testToolbarAppearsAfterSelection_withFirstStringLtrAlgorithmAndRtlHint()
292            throws Throwable {
293        // after the hint layout change, the floating toolbar was not visible in the case below
294        // this test tests that the floating toolbar is displayed on the screen and is visible to
295        // user.
296        mActivityRule.runOnUiThread(() -> {
297            final TextView textView = mActivity.findViewById(R.id.textview);
298            textView.setTextDirection(TextView.TEXT_DIRECTION_FIRST_STRONG_LTR);
299            textView.setInputType(InputType.TYPE_CLASS_TEXT);
300            textView.setSingleLine(true);
301            textView.setHint("الروبوت");
302        });
303        mInstrumentation.waitForIdleSync();
304
305        onView(withId(R.id.textview)).perform(replaceText("test"));
306        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(1));
307        clickFloatingToolbarItem(mActivity.getString(com.android.internal.R.string.cut));
308        onView(withId(R.id.textview)).perform(longClick());
309        sleepForFloatingToolbarPopup();
310
311        assertFloatingToolbarIsDisplayed();
312    }
313
314    @Test
315    public void testToolbarAppearsAfterLinkClicked() throws Throwable {
316        TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.textview);
317        int position = (textLink.getStart() + textLink.getEnd()) / 2;
318        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(position));
319        sleepForFloatingToolbarPopup();
320        assertFloatingToolbarIsDisplayed();
321
322    }
323
324    @Test
325    public void testToolbarAppearsAfterLinkClickedNonselectable() throws Throwable {
326        TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview);
327        int position = (textLink.getStart() + textLink.getEnd()) / 2;
328        onView(withId(R.id.nonselectable_textview)).perform(clickOnTextAtIndex(position));
329        sleepForFloatingToolbarPopup();
330        assertFloatingToolbarIsDisplayed();
331    }
332
333    @Test
334    public void testSelectionRemovedWhenNonselectableTextLosesFocus() throws Throwable {
335        // Add a link to both selectable and nonselectable TextViews:
336        TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.textview);
337        int selectablePosition = (textLink.getStart() + textLink.getEnd()) / 2;
338        textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview);
339        int nonselectablePosition = (textLink.getStart() + textLink.getEnd()) / 2;
340        TextView selectableTextView = mActivity.findViewById(R.id.textview);
341        TextView nonselectableTextView = mActivity.findViewById(R.id.nonselectable_textview);
342
343        onView(withId(R.id.nonselectable_textview))
344                .perform(clickOnTextAtIndex(nonselectablePosition));
345        sleepForFloatingToolbarPopup();
346        assertFloatingToolbarIsDisplayed();
347        assertTrue(nonselectableTextView.hasSelection());
348
349        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(selectablePosition));
350        sleepForFloatingToolbarPopup();
351        assertFloatingToolbarIsDisplayed();
352
353        assertTrue(selectableTextView.hasSelection());
354        assertFalse(nonselectableTextView.hasSelection());
355    }
356
357    @Test
358    public void testSelectionRemovedFromNonselectableTextWhenWindowLosesFocus() throws Throwable {
359        TextLinks.TextLink textLink = addLinkifiedTextToTextView(R.id.nonselectable_textview);
360        int nonselectablePosition = (textLink.getStart() + textLink.getEnd()) / 2;
361        TextView nonselectableTextView = mActivity.findViewById(R.id.nonselectable_textview);
362
363        onView(withId(R.id.nonselectable_textview))
364                .perform(clickOnTextAtIndex(nonselectablePosition));
365        sleepForFloatingToolbarPopup();
366        assertFloatingToolbarIsDisplayed();
367        assertTrue(nonselectableTextView.hasSelection());
368
369        UiDevice device = UiDevice.getInstance(mInstrumentation);
370        device.openNotification();
371        Thread.sleep(2000);
372        device.pressBack();
373        Thread.sleep(2000);
374
375        assertFalse(nonselectableTextView.hasSelection());
376    }
377
378    private TextLinks.TextLink addLinkifiedTextToTextView(int id) throws Throwable {
379        TextView textView = mActivity.findViewById(id);
380        useSystemDefaultTextClassifier();
381        TextClassificationManager textClassificationManager =
382                mActivity.getSystemService(TextClassificationManager.class);
383        TextClassifier textClassifier = textClassificationManager.getTextClassifier();
384        Spannable content = new SpannableString("Call me at +19148277737");
385        TextLinks.Request request = new TextLinks.Request.Builder(content).build();
386        TextLinks links = textClassifier.generateLinks(request);
387        TextLinksParams applyParams = new TextLinksParams.Builder()
388                .setApplyStrategy(TextLinks.APPLY_STRATEGY_REPLACE)
389                .build();
390        applyParams.apply(content, links);
391
392        mActivityRule.runOnUiThread(() -> {
393            textView.setText(content);
394            textView.setMovementMethod(LinkMovementMethod.getInstance());
395        });
396        mInstrumentation.waitForIdleSync();
397
398        // Wait for the UI thread to refresh
399        Thread.sleep(1000);
400
401        return links.getLinks().iterator().next();
402    }
403
404    @Test
405    public void testToolbarAndInsertionHandle() {
406        final String text = "text";
407        onView(withId(R.id.textview)).perform(replaceText(text));
408        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
409
410        onHandleView(com.android.internal.R.id.insertion_handle).perform(click());
411        sleepForFloatingToolbarPopup();
412        assertFloatingToolbarIsDisplayed();
413
414        assertFloatingToolbarContainsItem(
415                mActivity.getString(com.android.internal.R.string.selectAll));
416        assertFloatingToolbarDoesNotContainItem(
417                mActivity.getString(com.android.internal.R.string.copy));
418        assertFloatingToolbarDoesNotContainItem(
419                mActivity.getString(com.android.internal.R.string.cut));
420    }
421
422    @Test
423    public void testToolbarAndSelectionHandle() {
424        final String text = "abcd efg hijk";
425        onView(withId(R.id.textview)).perform(replaceText(text));
426
427        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf("f")));
428        sleepForFloatingToolbarPopup();
429        assertFloatingToolbarIsDisplayed();
430
431        assertFloatingToolbarContainsItem(
432                mActivity.getString(com.android.internal.R.string.selectAll));
433        assertFloatingToolbarContainsItem(
434                mActivity.getString(com.android.internal.R.string.copy));
435        assertFloatingToolbarContainsItem(
436                mActivity.getString(com.android.internal.R.string.cut));
437
438        final TextView textView = mActivity.findViewById(R.id.textview);
439        onHandleView(com.android.internal.R.id.selection_start_handle)
440                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
441        sleepForFloatingToolbarPopup();
442        assertFloatingToolbarIsDisplayed();
443
444        onHandleView(com.android.internal.R.id.selection_end_handle)
445                .perform(dragHandle(textView, Handle.SELECTION_END, text.length()));
446        sleepForFloatingToolbarPopup();
447        assertFloatingToolbarIsDisplayed();
448
449        assertFloatingToolbarDoesNotContainItem(
450                mActivity.getString(com.android.internal.R.string.selectAll));
451        assertFloatingToolbarContainsItem(
452                mActivity.getString(com.android.internal.R.string.copy));
453        assertFloatingToolbarContainsItem(
454                mActivity.getString(com.android.internal.R.string.cut));
455    }
456
457    @Test
458    public void testInsertionHandle() {
459        final String text = "abcd efg hijk ";
460        onView(withId(R.id.textview)).perform(replaceText(text));
461
462        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
463        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
464
465        final TextView textView = mActivity.findViewById(R.id.textview);
466
467        onHandleView(com.android.internal.R.id.insertion_handle)
468                .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('a')));
469        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("a")));
470
471        onHandleView(com.android.internal.R.id.insertion_handle)
472                .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('f')));
473        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("f")));
474    }
475
476    @Test
477    public void testInsertionHandle_multiLine() {
478        final String text = "abcd\n" + "efg\n" + "hijk\n";
479        onView(withId(R.id.textview)).perform(replaceText(text));
480
481        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
482        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length()));
483
484        final TextView textView = mActivity.findViewById(R.id.textview);
485
486        onHandleView(com.android.internal.R.id.insertion_handle)
487                .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('a')));
488        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("a")));
489
490        onHandleView(com.android.internal.R.id.insertion_handle)
491                .perform(dragHandle(textView, Handle.INSERTION, text.indexOf('f')));
492        onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("f")));
493    }
494
495    @Test
496    public void testSelectionHandles() {
497        final String text = "abcd efg hijk lmn";
498        onView(withId(R.id.textview)).perform(replaceText(text));
499
500        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
501
502        onHandleView(com.android.internal.R.id.selection_start_handle)
503                .check(matches(isDisplayed()));
504        onHandleView(com.android.internal.R.id.selection_end_handle)
505                .check(matches(isDisplayed()));
506
507        final TextView textView = mActivity.findViewById(R.id.textview);
508        onHandleView(com.android.internal.R.id.selection_start_handle)
509                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
510        onView(withId(R.id.textview)).check(hasSelection("abcd efg"));
511
512        onHandleView(com.android.internal.R.id.selection_end_handle)
513                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('k') + 1));
514        onView(withId(R.id.textview)).check(hasSelection("abcd efg hijk"));
515    }
516
517    @Test
518    public void testSelectionHandles_bidi() {
519        final String text = "abc \u0621\u0622\u0623 def";
520        onView(withId(R.id.textview)).perform(replaceText(text));
521
522        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('\u0622')));
523
524        onHandleView(com.android.internal.R.id.selection_start_handle)
525                .check(matches(isDisplayed()));
526        onHandleView(com.android.internal.R.id.selection_end_handle)
527                .check(matches(isDisplayed()));
528
529        onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
530
531        final TextView textView = mActivity.findViewById(R.id.textview);
532        onHandleView(com.android.internal.R.id.selection_start_handle)
533                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('f')));
534        onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
535
536        onHandleView(com.android.internal.R.id.selection_end_handle)
537                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
538        onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
539
540        onHandleView(com.android.internal.R.id.selection_start_handle)
541                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u0623'),
542                        false));
543        onView(withId(R.id.textview)).check(hasSelection("\u0623"));
544
545        onHandleView(com.android.internal.R.id.selection_start_handle)
546                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u0621'),
547                        false));
548        onView(withId(R.id.textview)).check(hasSelection("\u0621\u0622\u0623"));
549
550        onHandleView(com.android.internal.R.id.selection_start_handle)
551                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
552        onView(withId(R.id.textview)).check(hasSelection("abc \u0621\u0622\u0623"));
553
554        onHandleView(com.android.internal.R.id.selection_end_handle)
555                .perform(dragHandle(textView, Handle.SELECTION_END, text.length()));
556        onView(withId(R.id.textview)).check(hasSelection("abc \u0621\u0622\u0623 def"));
557    }
558
559    @Test
560    public void testSelectionHandles_multiLine() {
561        final String text = "abcd\n" + "efg\n" + "hijk\n" + "lmn\n" + "opqr";
562        onView(withId(R.id.textview)).perform(replaceText(text));
563        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
564
565        final TextView textView = mActivity.findViewById(R.id.textview);
566        onHandleView(com.android.internal.R.id.selection_start_handle)
567                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('e')));
568        onView(withId(R.id.textview)).check(hasSelection("efg\nhijk"));
569
570        onHandleView(com.android.internal.R.id.selection_start_handle)
571                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('a')));
572        onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk"));
573
574        onHandleView(com.android.internal.R.id.selection_end_handle)
575                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('n') + 1));
576        onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn"));
577
578        onHandleView(com.android.internal.R.id.selection_end_handle)
579                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('r') + 1));
580        onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn\nopqr"));
581    }
582
583    @Suppress // Consistently failing.
584    @Test
585    public void testSelectionHandles_multiLine_rtl() {
586        // Arabic text.
587        final String text = "\u062A\u062B\u062C\n" + "\u062D\u062E\u062F\n"
588                + "\u0630\u0631\u0632\n" + "\u0633\u0634\u0635\n" + "\u0636\u0637\u0638\n"
589                + "\u0639\u063A\u063B";
590        onView(withId(R.id.textview)).perform(replaceText(text));
591        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('\u0634')));
592
593        final TextView textView = mActivity.findViewById(R.id.textview);
594        onHandleView(com.android.internal.R.id.selection_start_handle)
595                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u062E')));
596        onView(withId(R.id.textview)).check(hasSelection(
597                text.substring(text.indexOf('\u062D'), text.indexOf('\u0635') + 1)));
598
599        onHandleView(com.android.internal.R.id.selection_start_handle)
600                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('\u062A')));
601        onView(withId(R.id.textview)).check(hasSelection(
602                text.substring(text.indexOf('\u062A'), text.indexOf('\u0635') + 1)));
603
604        onHandleView(com.android.internal.R.id.selection_end_handle)
605                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('\u0638')));
606        onView(withId(R.id.textview)).check(hasSelection(
607                text.substring(text.indexOf('\u062A'), text.indexOf('\u0638') + 1)));
608
609        onHandleView(com.android.internal.R.id.selection_end_handle)
610                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('\u063B')));
611        onView(withId(R.id.textview)).check(hasSelection(text));
612    }
613
614    @Test
615    public void testSelectionHandles_doesNotPassAnotherHandle() {
616        final String text = "abcd efg hijk lmn";
617        onView(withId(R.id.textview)).perform(replaceText(text));
618        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
619
620        final TextView textView = mActivity.findViewById(R.id.textview);
621        onHandleView(com.android.internal.R.id.selection_start_handle)
622                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('l')));
623        onView(withId(R.id.textview)).check(hasSelection("g"));
624
625        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
626        onHandleView(com.android.internal.R.id.selection_end_handle)
627                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
628        onView(withId(R.id.textview)).check(hasSelection("e"));
629    }
630
631    @Test
632    public void testSelectionHandles_doesNotPassAnotherHandle_multiLine() {
633        final String text = "abcd\n" + "efg\n" + "hijk\n" + "lmn\n" + "opqr";
634        onView(withId(R.id.textview)).perform(replaceText(text));
635        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
636
637        final TextView textView = mActivity.findViewById(R.id.textview);
638        onHandleView(com.android.internal.R.id.selection_start_handle)
639                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('r') + 1));
640        onView(withId(R.id.textview)).check(hasSelection("k"));
641
642        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
643        onHandleView(com.android.internal.R.id.selection_end_handle)
644                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('a')));
645        onView(withId(R.id.textview)).check(hasSelection("h"));
646    }
647
648    @Test
649    public void testSelectionHandles_snapToWordBoundary() {
650        final String text = "abcd efg hijk lmn opqr";
651        onView(withId(R.id.textview)).perform(replaceText(text));
652        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
653
654        final TextView textView = mActivity.findViewById(R.id.textview);
655
656        onHandleView(com.android.internal.R.id.selection_start_handle)
657                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('f')));
658        onView(withId(R.id.textview)).check(hasSelection("efg hijk"));
659
660        onHandleView(com.android.internal.R.id.selection_start_handle)
661                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('d') + 1));
662        onView(withId(R.id.textview)).check(hasSelection("efg hijk"));
663
664
665        onHandleView(com.android.internal.R.id.selection_start_handle)
666                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('c')));
667        onView(withId(R.id.textview)).check(hasSelection("abcd efg hijk"));
668
669        onHandleView(com.android.internal.R.id.selection_start_handle)
670                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('d')));
671        onView(withId(R.id.textview)).check(hasSelection("d efg hijk"));
672
673        onHandleView(com.android.internal.R.id.selection_start_handle)
674                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('b')));
675        onView(withId(R.id.textview)).check(hasSelection("bcd efg hijk"));
676
677        onView(withId(R.id.textview)).perform(click());
678        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
679
680        onHandleView(com.android.internal.R.id.selection_end_handle)
681                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('n')));
682        onView(withId(R.id.textview)).check(hasSelection("hijk lmn"));
683
684        onHandleView(com.android.internal.R.id.selection_end_handle)
685                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('o')));
686        onView(withId(R.id.textview)).check(hasSelection("hijk lmn"));
687
688        onHandleView(com.android.internal.R.id.selection_end_handle)
689                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('q')));
690        onView(withId(R.id.textview)).check(hasSelection("hijk lmn opqr"));
691
692        onHandleView(com.android.internal.R.id.selection_end_handle)
693                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('p')));
694        onView(withId(R.id.textview)).check(hasSelection("hijk lmn o"));
695
696        onHandleView(com.android.internal.R.id.selection_end_handle)
697                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('r')));
698        onView(withId(R.id.textview)).check(hasSelection("hijk lmn opq"));
699    }
700
701    @Test
702    public void testSelectionHandles_snapToWordBoundary_multiLine() {
703        final String text = "abcd efg\n" + "hijk lmn\n" + "opqr stu";
704        onView(withId(R.id.textview)).perform(replaceText(text));
705        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('m')));
706
707        final TextView textView = mActivity.findViewById(R.id.textview);
708
709        onHandleView(com.android.internal.R.id.selection_start_handle)
710                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('c')));
711        onView(withId(R.id.textview)).check(hasSelection("abcd efg\nhijk lmn"));
712
713        onHandleView(com.android.internal.R.id.selection_start_handle)
714                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('g')));
715        onView(withId(R.id.textview)).check(hasSelection("g\nhijk lmn"));
716
717        onHandleView(com.android.internal.R.id.selection_start_handle)
718                .perform(dragHandle(textView, Handle.SELECTION_START, text.indexOf('m')));
719        onView(withId(R.id.textview)).check(hasSelection("lmn"));
720
721        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('i')));
722
723        onHandleView(com.android.internal.R.id.selection_end_handle)
724                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('u')));
725        onView(withId(R.id.textview)).check(hasSelection("hijk lmn\nopqr stu"));
726
727        onHandleView(com.android.internal.R.id.selection_end_handle)
728                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('p')));
729        onView(withId(R.id.textview)).check(hasSelection("hijk lmn\no"));
730
731        onHandleView(com.android.internal.R.id.selection_end_handle)
732                .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('i')));
733        onView(withId(R.id.textview)).check(hasSelection("hijk"));
734    }
735
736    @Test
737    public void testSelectionHandles_visibleEvenWithEmptyMenu() {
738        ((TextView) mActivity.findViewById(R.id.textview)).setCustomSelectionActionModeCallback(
739                new ActionMode.Callback() {
740                    @Override
741                    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
742                        menu.clear();
743                        return true;
744                    }
745
746                    @Override
747                    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
748                        menu.clear();
749                        return true;
750                    }
751
752                    @Override
753                    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
754                        return false;
755                    }
756
757                    @Override
758                    public void onDestroyActionMode(ActionMode mode) {}
759                });
760        final String text = "abcd efg hijk lmn";
761        onView(withId(R.id.textview)).perform(replaceText(text));
762
763        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('f')));
764
765        onHandleView(com.android.internal.R.id.selection_start_handle)
766                .check(matches(isDisplayed()));
767        onHandleView(com.android.internal.R.id.selection_end_handle)
768                .check(matches(isDisplayed()));
769    }
770
771    @Test
772    public void testSetSelectionAndActionMode() throws Throwable {
773        final TextView textView = mActivity.findViewById(R.id.textview);
774        final ActionMode.Callback amCallback = mock(ActionMode.Callback.class);
775        when(amCallback.onCreateActionMode(any(ActionMode.class), any(Menu.class)))
776                .thenReturn(true);
777        when(amCallback.onPrepareActionMode(any(ActionMode.class), any(Menu.class)))
778                .thenReturn(true);
779        textView.setCustomSelectionActionModeCallback(amCallback);
780
781        final String text = "abc def";
782        onView(withId(R.id.textview)).perform(replaceText(text));
783        mActivityRule.runOnUiThread(
784                () -> Selection.setSelection((Spannable) textView.getText(), 0, 3));
785        mInstrumentation.waitForIdleSync();
786        // Don't automatically start action mode.
787        verify(amCallback, never()).onCreateActionMode(any(ActionMode.class), any(Menu.class));
788        // Make sure that "Select All" is included in the selection action mode when the entire text
789        // is not selected.
790        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('e')));
791        sleepForFloatingToolbarPopup();
792        assertFloatingToolbarIsDisplayed();
793        // Changing the selection range by API should not interrupt the selection action mode.
794        mActivityRule.runOnUiThread(
795                () -> Selection.setSelection((Spannable) textView.getText(), 0, 3));
796        mInstrumentation.waitForIdleSync();
797        sleepForFloatingToolbarPopup();
798        assertFloatingToolbarIsDisplayed();
799        assertFloatingToolbarContainsItem(
800                mActivity.getString(com.android.internal.R.string.selectAll));
801        // Make sure that "Select All" is no longer included when the entire text is selected by
802        // API.
803        mActivityRule.runOnUiThread(
804                () -> Selection.setSelection((Spannable) textView.getText(), 0, text.length()));
805        mInstrumentation.waitForIdleSync();
806
807        sleepForFloatingToolbarPopup();
808        assertFloatingToolbarIsDisplayed();
809        assertFloatingToolbarDoesNotContainItem(
810                mActivity.getString(com.android.internal.R.string.selectAll));
811        // Make sure that shrinking the selection range to cursor (an empty range) by API
812        // terminates selection action mode and does not trigger the insertion action mode.
813        mActivityRule.runOnUiThread(
814                () -> Selection.setSelection((Spannable) textView.getText(), 0));
815        mInstrumentation.waitForIdleSync();
816
817        // Make sure that user click can trigger the insertion action mode.
818        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length()));
819        onHandleView(com.android.internal.R.id.insertion_handle).perform(click());
820        sleepForFloatingToolbarPopup();
821        assertFloatingToolbarIsDisplayed();
822        // Make sure that an existing insertion action mode keeps alive after the insertion point is
823        // moved by API.
824        mActivityRule.runOnUiThread(
825                () -> Selection.setSelection((Spannable) textView.getText(), 0));
826        mInstrumentation.waitForIdleSync();
827
828        sleepForFloatingToolbarPopup();
829        assertFloatingToolbarIsDisplayed();
830        assertFloatingToolbarDoesNotContainItem(
831                mActivity.getString(com.android.internal.R.string.copy));
832        // Make sure that selection action mode is started after selection is created by API when
833        // insertion action mode is active.
834        mActivityRule.runOnUiThread(
835                () -> Selection.setSelection((Spannable) textView.getText(), 1, text.length()));
836        mInstrumentation.waitForIdleSync();
837
838        sleepForFloatingToolbarPopup();
839        assertFloatingToolbarIsDisplayed();
840        assertFloatingToolbarContainsItem(
841                mActivity.getString(com.android.internal.R.string.copy));
842    }
843
844    @Test
845    public void testTransientState() throws Throwable {
846        final String text = "abc def";
847        onView(withId(R.id.textview)).perform(replaceText(text));
848
849        final TextView textView = mActivity.findViewById(R.id.textview);
850        assertFalse(textView.hasTransientState());
851
852        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('b')));
853        // hasTransientState should return true when user generated selection is active.
854        assertTrue(textView.hasTransientState());
855        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.indexOf('d')));
856        // hasTransientState should return false as the selection has been cleared.
857        assertFalse(textView.hasTransientState());
858        mActivityRule.runOnUiThread(
859                () -> Selection.setSelection((Spannable) textView.getText(), 0, text.length()));
860        mInstrumentation.waitForIdleSync();
861
862        // hasTransientState should return false when selection is created by API.
863        assertFalse(textView.hasTransientState());
864    }
865
866    @Test
867    public void testResetMenuItemTitle() throws Throwable {
868        mActivity.getSystemService(TextClassificationManager.class).setTextClassifier(null);
869        final TextView textView = mActivity.findViewById(R.id.textview);
870        final int itemId = 1;
871        final String title1 = " AFIGBO";
872        final int index = title1.indexOf('I');
873        final String title2 = title1.substring(index);
874        final String[] title = new String[]{title1};
875        mActivityRule.runOnUiThread(() -> textView.setCustomSelectionActionModeCallback(
876                new ActionMode.Callback() {
877                    @Override
878                    public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
879                        return true;
880                    }
881
882                    @Override
883                    public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
884                        menu.removeItem(itemId);
885                        menu.add(Menu.NONE /* group */, itemId, 0 /* order */, title[0]);
886                        return true;
887                    }
888
889                    @Override
890                    public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
891                        return false;
892                    }
893
894                    @Override
895                    public void onDestroyActionMode(ActionMode actionMode) {
896                    }
897                }));
898        mInstrumentation.waitForIdleSync();
899
900        onView(withId(R.id.textview)).perform(replaceText(title1));
901        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(index));
902        sleepForFloatingToolbarPopup();
903        assertFloatingToolbarContainsItem(title1);
904
905        // Change the menu item title.
906        title[0] = title2;
907        // Change the selection to invalidate the action mode without restarting it.
908        onHandleView(com.android.internal.R.id.selection_start_handle)
909                .perform(dragHandle(textView, Handle.SELECTION_START, index));
910        sleepForFloatingToolbarPopup();
911        assertFloatingToolbarContainsItem(title2);
912    }
913
914    @Test
915    public void testAssistItemIsAtIndexZero() throws Throwable {
916        useSystemDefaultTextClassifier();
917        final TextView textView = mActivity.findViewById(R.id.textview);
918        mActivityRule.runOnUiThread(() -> textView.setCustomSelectionActionModeCallback(
919                new ActionMode.Callback() {
920                    @Override
921                    public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
922                        // Create another item at order position 0 to confirm that it will never be
923                        // placed before the textAssist item.
924                        menu.add(Menu.NONE, 0 /* id */, 0 /* order */, "Test");
925                        return true;
926                    }
927
928                    @Override
929                    public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
930                        return true;
931                    }
932
933                    @Override
934                    public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
935                        return false;
936                    }
937
938                    @Override
939                    public void onDestroyActionMode(ActionMode actionMode) {
940                    }
941                }));
942        mInstrumentation.waitForIdleSync();
943        final String text = "droid@android.com";
944
945        onView(withId(R.id.textview)).perform(replaceText(text));
946        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(text.indexOf('@')));
947        sleepForFloatingToolbarPopup();
948        assertFloatingToolbarItemIndex(android.R.id.textAssist, 0);
949    }
950
951    @Test
952    public void testNoAssistItemForPasswordField() throws Throwable {
953        useSystemDefaultTextClassifier();
954        final TextView textView = mActivity.findViewById(R.id.textview);
955        mActivityRule.runOnUiThread(() -> {
956            textView.setInputType(
957                    InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
958        });
959        mInstrumentation.waitForIdleSync();
960        final String password = "afigbo@android.com";
961
962        onView(withId(R.id.textview)).perform(replaceText(password));
963        onView(withId(R.id.textview)).perform(longPressOnTextAtIndex(password.indexOf('@')));
964        sleepForFloatingToolbarPopup();
965        assertFloatingToolbarDoesNotContainItem(android.R.id.textAssist);
966    }
967
968    @Test
969    public void testPastePlainText_menuAction() {
970        initializeClipboardWithText(TextStyle.STYLED);
971
972        onView(withId(R.id.textview)).perform(replaceText(""));
973        onView(withId(R.id.textview)).perform(longClick());
974        sleepForFloatingToolbarPopup();
975        clickFloatingToolbarItem(
976                mActivity.getString(com.android.internal.R.string.paste_as_plain_text));
977        mInstrumentation.waitForIdleSync();
978
979        onView(withId(R.id.textview)).check(matches(withText("styledtext")));
980        onView(withId(R.id.textview)).check(doesNotHaveStyledText());
981    }
982
983    @Test
984    public void testPastePlainText_noMenuItemForPlainText() {
985        initializeClipboardWithText(TextStyle.PLAIN);
986
987        onView(withId(R.id.textview)).perform(replaceText(""));
988        onView(withId(R.id.textview)).perform(longClick());
989        sleepForFloatingToolbarPopup();
990
991        assertFloatingToolbarDoesNotContainItem(
992                mActivity.getString(com.android.internal.R.string.paste_as_plain_text));
993    }
994
995    private void useSystemDefaultTextClassifier() {
996        mActivity.getSystemService(TextClassificationManager.class).setTextClassifier(null);
997    }
998
999    private void initializeClipboardWithText(TextStyle textStyle) {
1000        final ClipData clip;
1001        switch (textStyle) {
1002            case STYLED:
1003                clip = ClipData.newHtmlText("html", "styledtext", "<b>styledtext</b>");
1004                break;
1005            case PLAIN:
1006                clip = ClipData.newPlainText("plain", "plaintext");
1007                break;
1008            default:
1009                throw new IllegalArgumentException("Invalid text style");
1010        }
1011        mActivity.getWindow().getDecorView().post(() ->
1012                mActivity.getSystemService(ClipboardManager.class).setPrimaryClip(clip));
1013        mInstrumentation.waitForIdleSync();
1014    }
1015
1016    private enum TextStyle {
1017        PLAIN, STYLED
1018    }
1019}
1020