Loading core/java/android/widget/Editor.java +26 −28 Original line number Original line Diff line number Diff line Loading @@ -217,6 +217,7 @@ public class Editor { boolean mInBatchEditControllers; boolean mInBatchEditControllers; boolean mShowSoftInputOnFocus = true; boolean mShowSoftInputOnFocus = true; private boolean mPreserveSelection; private boolean mPreserveSelection; private boolean mRestartActionModeOnNextRefresh; boolean mTemporaryDetach; boolean mTemporaryDetach; boolean mIsBeingLongClicked; boolean mIsBeingLongClicked; Loading Loading @@ -381,9 +382,8 @@ public class Editor { updateSpellCheckSpans(0, mTextView.getText().length(), updateSpellCheckSpans(0, mTextView.getText().length(), true /* create the spell checker if needed */); true /* create the spell checker if needed */); if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) { if (mTextView.hasSelection()) { // We had an active selection from before, start the selection mode. refreshTextActionMode(); startSelectionActionMode(); } } getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true); getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true); Loading Loading @@ -1284,7 +1284,7 @@ public class Editor { } } final InputMethodManager imm = InputMethodManager.peekInstance(); final InputMethodManager imm = InputMethodManager.peekInstance(); if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) { if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) { startSelectionActionMode(); refreshTextActionMode(); } } } else { } else { if (mBlink != null) { if (mBlink != null) { Loading Loading @@ -1847,6 +1847,7 @@ public class Editor { void refreshTextActionMode() { void refreshTextActionMode() { if (extractedTextModeWillBeStarted()) { if (extractedTextModeWillBeStarted()) { mRestartActionModeOnNextRefresh = false; return; return; } } final boolean hasSelection = mTextView.hasSelection(); final boolean hasSelection = mTextView.hasSelection(); Loading @@ -1855,12 +1856,19 @@ public class Editor { if ((selectionController != null && selectionController.isCursorBeingModified()) if ((selectionController != null && selectionController.isCursorBeingModified()) || (insertionController != null && insertionController.isCursorBeingModified())) { || (insertionController != null && insertionController.isCursorBeingModified())) { // ActionMode should be managed by the currently active cursor controller. // ActionMode should be managed by the currently active cursor controller. mRestartActionModeOnNextRefresh = false; return; return; } } if (hasSelection) { if (hasSelection) { if (mTextActionMode == null || selectionController == null hideInsertionPointCursorController(); || !selectionController.isActive()) { if (mTextActionMode == null) { // Avoid dismissing the selection if it exists. if (mRestartActionModeOnNextRefresh || mTextView.isInExtractedMode()) { // To avoid distraction, newly start action mode only when selection action // mode is being restarted or in full screen extracted mode. startSelectionActionMode(); } } else if (selectionController == null || !selectionController.isActive()) { // Insertion action mode is active. Avoid dismissing the selection. stopTextActionModeWithPreservingSelection(); stopTextActionModeWithPreservingSelection(); startSelectionActionMode(); startSelectionActionMode(); } else { } else { Loading @@ -1875,6 +1883,7 @@ public class Editor { mTextActionMode.invalidateContentRect(); mTextActionMode.invalidateContentRect(); } } } } mRestartActionModeOnNextRefresh = false; } } /** /** Loading Loading @@ -1905,11 +1914,12 @@ public class Editor { * * * @return true if the selection mode was actually started. * @return true if the selection mode was actually started. */ */ private boolean startSelectionActionMode() { boolean startSelectionActionMode() { boolean selectionStarted = startSelectionActionModeInternal(); boolean selectionStarted = startSelectionActionModeInternal(); if (selectionStarted) { if (selectionStarted) { getSelectionController().show(); getSelectionController().show(); } } mRestartActionModeOnNextRefresh = false; return selectionStarted; return selectionStarted; } } Loading Loading @@ -2113,6 +2123,9 @@ public class Editor { } } private void stopTextActionModeWithPreservingSelection() { private void stopTextActionModeWithPreservingSelection() { if (mTextActionMode != null) { mRestartActionModeOnNextRefresh = true; } mPreserveSelection = true; mPreserveSelection = true; stopTextActionMode(); stopTextActionMode(); mPreserveSelection = false; mPreserveSelection = false; Loading Loading @@ -3707,6 +3720,8 @@ public class Editor { @Override @Override public void onDestroyActionMode(ActionMode mode) { public void onDestroyActionMode(ActionMode mode) { // Clear mTextActionMode not to recursively destroy action mode by clearing selection. mTextActionMode = null; Callback customCallback = getCustomCallback(); Callback customCallback = getCustomCallback(); if (customCallback != null) { if (customCallback != null) { customCallback.onDestroyActionMode(mode); customCallback.onDestroyActionMode(mode); Loading @@ -3725,8 +3740,6 @@ public class Editor { if (mSelectionModifierCursorController != null) { if (mSelectionModifierCursorController != null) { mSelectionModifierCursorController.hide(); mSelectionModifierCursorController.hide(); } } mTextActionMode = null; } } @Override @Override Loading Loading @@ -5129,27 +5142,12 @@ public class Editor { // No longer dragging to select text, let the parent intercept events. // No longer dragging to select text, let the parent intercept events. mTextView.getParent().requestDisallowInterceptTouchEvent(false); mTextView.getParent().requestDisallowInterceptTouchEvent(false); int startOffset = mTextView.getSelectionStart(); // No longer the first dragging motion, reset. int endOffset = mTextView.getSelectionEnd(); resetDragAcceleratorState(); // Since we don't let drag handles pass once they're visible, we need to // make sure the start / end locations are correct because the user *can* // switch directions during the initial drag. if (endOffset < startOffset) { int tmp = endOffset; endOffset = startOffset; startOffset = tmp; // Also update the selection with the right offsets in this case. if (mTextView.hasSelection()) { Selection.setSelection((Spannable) mTextView.getText(), startOffset, endOffset); } if (startOffset != endOffset) { startSelectionActionMode(); startSelectionActionMode(); } } // No longer the first dragging motion, reset. resetDragAcceleratorState(); break; break; } } } } Loading core/java/android/widget/TextView.java +4 −0 Original line number Original line Diff line number Diff line Loading @@ -9212,6 +9212,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } if (start >= 0 && start <= end && end <= text.length()) { if (start >= 0 && start <= end && end <= text.length()) { Selection.setSelection((Spannable) text, start, end); Selection.setSelection((Spannable) text, start, end); // Make sure selection mode is engaged. if (mEditor != null) { mEditor.startSelectionActionMode(); } return true; return true; } } } } Loading core/tests/coretests/src/android/widget/TextViewActivityTest.java +66 −0 Original line number Original line Diff line number Diff line Loading @@ -47,6 +47,8 @@ import com.android.frameworks.coretests.R; import android.support.test.espresso.action.EspressoKey; import android.support.test.espresso.action.EspressoKey; import android.test.ActivityInstrumentationTestCase2; import android.test.ActivityInstrumentationTestCase2; import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest; import android.text.Selection; import android.text.Spannable; import android.view.KeyEvent; import android.view.KeyEvent; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.anyOf; Loading Loading @@ -534,4 +536,68 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('i'))); .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('i'))); onView(withId(R.id.textview)).check(hasSelection("hijk")); onView(withId(R.id.textview)).check(hasSelection("hijk")); } } @SmallTest public void testSetSelectionAndActionMode() throws Exception { final String text = "abc def"; onView(withId(R.id.textview)).perform(click()); onView(withId(R.id.textview)).perform(replaceText(text)); final TextView textView = (TextView) getActivity().findViewById(R.id.textview); assertFloatingToolbarIsNotDisplayed(); textView.post(() -> Selection.setSelection((Spannable) textView.getText(), 0, 3)); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); // Don't automatically start action mode. assertFloatingToolbarIsNotDisplayed(); // Make sure that "Select All" is included in the selection action mode when the entire text // is not selected. onView(withId(R.id.textview)).perform(doubleClickOnTextAtIndex(text.indexOf('e'))); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); // Changing the selection range by API should not interrupt the selection action mode. textView.post(() -> Selection.setSelection((Spannable) textView.getText(), 0, 3)); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarContainsItem( getActivity().getString(com.android.internal.R.string.selectAll)); // Make sure that "Select All" is no longer included when the entire text is selected by // API. textView.post( () -> Selection.setSelection((Spannable) textView.getText(), 0, text.length())); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarDoesNotContainItem( getActivity().getString(com.android.internal.R.string.selectAll)); // Make sure that shrinking the selection range to cursor (an empty range) by API // terminates selection action mode and does not trigger the insertion action mode. textView.post(() -> Selection.setSelection((Spannable) textView.getText(), 0)); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsNotDisplayed(); // Make sure that user click can trigger the insertion action mode. onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length())); onHandleView(com.android.internal.R.id.insertion_handle).perform(click()); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); // Make sure that an existing insertion action mode keeps alive after the insertion point is // moved by API. textView.post(() -> Selection.setSelection((Spannable) textView.getText(), 0)); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarDoesNotContainItem( getActivity().getString(com.android.internal.R.string.copy)); // Make sure that selection action mode is started after selection is created by API when // insertion action mode is active. textView.post( () -> Selection.setSelection((Spannable) textView.getText(), 1, text.length())); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarContainsItem( getActivity().getString(com.android.internal.R.string.copy)); } } } Loading
core/java/android/widget/Editor.java +26 −28 Original line number Original line Diff line number Diff line Loading @@ -217,6 +217,7 @@ public class Editor { boolean mInBatchEditControllers; boolean mInBatchEditControllers; boolean mShowSoftInputOnFocus = true; boolean mShowSoftInputOnFocus = true; private boolean mPreserveSelection; private boolean mPreserveSelection; private boolean mRestartActionModeOnNextRefresh; boolean mTemporaryDetach; boolean mTemporaryDetach; boolean mIsBeingLongClicked; boolean mIsBeingLongClicked; Loading Loading @@ -381,9 +382,8 @@ public class Editor { updateSpellCheckSpans(0, mTextView.getText().length(), updateSpellCheckSpans(0, mTextView.getText().length(), true /* create the spell checker if needed */); true /* create the spell checker if needed */); if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) { if (mTextView.hasSelection()) { // We had an active selection from before, start the selection mode. refreshTextActionMode(); startSelectionActionMode(); } } getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true); getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true); Loading Loading @@ -1284,7 +1284,7 @@ public class Editor { } } final InputMethodManager imm = InputMethodManager.peekInstance(); final InputMethodManager imm = InputMethodManager.peekInstance(); if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) { if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) { startSelectionActionMode(); refreshTextActionMode(); } } } else { } else { if (mBlink != null) { if (mBlink != null) { Loading Loading @@ -1847,6 +1847,7 @@ public class Editor { void refreshTextActionMode() { void refreshTextActionMode() { if (extractedTextModeWillBeStarted()) { if (extractedTextModeWillBeStarted()) { mRestartActionModeOnNextRefresh = false; return; return; } } final boolean hasSelection = mTextView.hasSelection(); final boolean hasSelection = mTextView.hasSelection(); Loading @@ -1855,12 +1856,19 @@ public class Editor { if ((selectionController != null && selectionController.isCursorBeingModified()) if ((selectionController != null && selectionController.isCursorBeingModified()) || (insertionController != null && insertionController.isCursorBeingModified())) { || (insertionController != null && insertionController.isCursorBeingModified())) { // ActionMode should be managed by the currently active cursor controller. // ActionMode should be managed by the currently active cursor controller. mRestartActionModeOnNextRefresh = false; return; return; } } if (hasSelection) { if (hasSelection) { if (mTextActionMode == null || selectionController == null hideInsertionPointCursorController(); || !selectionController.isActive()) { if (mTextActionMode == null) { // Avoid dismissing the selection if it exists. if (mRestartActionModeOnNextRefresh || mTextView.isInExtractedMode()) { // To avoid distraction, newly start action mode only when selection action // mode is being restarted or in full screen extracted mode. startSelectionActionMode(); } } else if (selectionController == null || !selectionController.isActive()) { // Insertion action mode is active. Avoid dismissing the selection. stopTextActionModeWithPreservingSelection(); stopTextActionModeWithPreservingSelection(); startSelectionActionMode(); startSelectionActionMode(); } else { } else { Loading @@ -1875,6 +1883,7 @@ public class Editor { mTextActionMode.invalidateContentRect(); mTextActionMode.invalidateContentRect(); } } } } mRestartActionModeOnNextRefresh = false; } } /** /** Loading Loading @@ -1905,11 +1914,12 @@ public class Editor { * * * @return true if the selection mode was actually started. * @return true if the selection mode was actually started. */ */ private boolean startSelectionActionMode() { boolean startSelectionActionMode() { boolean selectionStarted = startSelectionActionModeInternal(); boolean selectionStarted = startSelectionActionModeInternal(); if (selectionStarted) { if (selectionStarted) { getSelectionController().show(); getSelectionController().show(); } } mRestartActionModeOnNextRefresh = false; return selectionStarted; return selectionStarted; } } Loading Loading @@ -2113,6 +2123,9 @@ public class Editor { } } private void stopTextActionModeWithPreservingSelection() { private void stopTextActionModeWithPreservingSelection() { if (mTextActionMode != null) { mRestartActionModeOnNextRefresh = true; } mPreserveSelection = true; mPreserveSelection = true; stopTextActionMode(); stopTextActionMode(); mPreserveSelection = false; mPreserveSelection = false; Loading Loading @@ -3707,6 +3720,8 @@ public class Editor { @Override @Override public void onDestroyActionMode(ActionMode mode) { public void onDestroyActionMode(ActionMode mode) { // Clear mTextActionMode not to recursively destroy action mode by clearing selection. mTextActionMode = null; Callback customCallback = getCustomCallback(); Callback customCallback = getCustomCallback(); if (customCallback != null) { if (customCallback != null) { customCallback.onDestroyActionMode(mode); customCallback.onDestroyActionMode(mode); Loading @@ -3725,8 +3740,6 @@ public class Editor { if (mSelectionModifierCursorController != null) { if (mSelectionModifierCursorController != null) { mSelectionModifierCursorController.hide(); mSelectionModifierCursorController.hide(); } } mTextActionMode = null; } } @Override @Override Loading Loading @@ -5129,27 +5142,12 @@ public class Editor { // No longer dragging to select text, let the parent intercept events. // No longer dragging to select text, let the parent intercept events. mTextView.getParent().requestDisallowInterceptTouchEvent(false); mTextView.getParent().requestDisallowInterceptTouchEvent(false); int startOffset = mTextView.getSelectionStart(); // No longer the first dragging motion, reset. int endOffset = mTextView.getSelectionEnd(); resetDragAcceleratorState(); // Since we don't let drag handles pass once they're visible, we need to // make sure the start / end locations are correct because the user *can* // switch directions during the initial drag. if (endOffset < startOffset) { int tmp = endOffset; endOffset = startOffset; startOffset = tmp; // Also update the selection with the right offsets in this case. if (mTextView.hasSelection()) { Selection.setSelection((Spannable) mTextView.getText(), startOffset, endOffset); } if (startOffset != endOffset) { startSelectionActionMode(); startSelectionActionMode(); } } // No longer the first dragging motion, reset. resetDragAcceleratorState(); break; break; } } } } Loading
core/java/android/widget/TextView.java +4 −0 Original line number Original line Diff line number Diff line Loading @@ -9212,6 +9212,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } if (start >= 0 && start <= end && end <= text.length()) { if (start >= 0 && start <= end && end <= text.length()) { Selection.setSelection((Spannable) text, start, end); Selection.setSelection((Spannable) text, start, end); // Make sure selection mode is engaged. if (mEditor != null) { mEditor.startSelectionActionMode(); } return true; return true; } } } } Loading
core/tests/coretests/src/android/widget/TextViewActivityTest.java +66 −0 Original line number Original line Diff line number Diff line Loading @@ -47,6 +47,8 @@ import com.android.frameworks.coretests.R; import android.support.test.espresso.action.EspressoKey; import android.support.test.espresso.action.EspressoKey; import android.test.ActivityInstrumentationTestCase2; import android.test.ActivityInstrumentationTestCase2; import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest; import android.text.Selection; import android.text.Spannable; import android.view.KeyEvent; import android.view.KeyEvent; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.anyOf; Loading Loading @@ -534,4 +536,68 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('i'))); .perform(dragHandle(textView, Handle.SELECTION_END, text.indexOf('i'))); onView(withId(R.id.textview)).check(hasSelection("hijk")); onView(withId(R.id.textview)).check(hasSelection("hijk")); } } @SmallTest public void testSetSelectionAndActionMode() throws Exception { final String text = "abc def"; onView(withId(R.id.textview)).perform(click()); onView(withId(R.id.textview)).perform(replaceText(text)); final TextView textView = (TextView) getActivity().findViewById(R.id.textview); assertFloatingToolbarIsNotDisplayed(); textView.post(() -> Selection.setSelection((Spannable) textView.getText(), 0, 3)); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); // Don't automatically start action mode. assertFloatingToolbarIsNotDisplayed(); // Make sure that "Select All" is included in the selection action mode when the entire text // is not selected. onView(withId(R.id.textview)).perform(doubleClickOnTextAtIndex(text.indexOf('e'))); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); // Changing the selection range by API should not interrupt the selection action mode. textView.post(() -> Selection.setSelection((Spannable) textView.getText(), 0, 3)); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarContainsItem( getActivity().getString(com.android.internal.R.string.selectAll)); // Make sure that "Select All" is no longer included when the entire text is selected by // API. textView.post( () -> Selection.setSelection((Spannable) textView.getText(), 0, text.length())); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarDoesNotContainItem( getActivity().getString(com.android.internal.R.string.selectAll)); // Make sure that shrinking the selection range to cursor (an empty range) by API // terminates selection action mode and does not trigger the insertion action mode. textView.post(() -> Selection.setSelection((Spannable) textView.getText(), 0)); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsNotDisplayed(); // Make sure that user click can trigger the insertion action mode. onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length())); onHandleView(com.android.internal.R.id.insertion_handle).perform(click()); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); // Make sure that an existing insertion action mode keeps alive after the insertion point is // moved by API. textView.post(() -> Selection.setSelection((Spannable) textView.getText(), 0)); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarDoesNotContainItem( getActivity().getString(com.android.internal.R.string.copy)); // Make sure that selection action mode is started after selection is created by API when // insertion action mode is active. textView.post( () -> Selection.setSelection((Spannable) textView.getText(), 1, text.length())); getInstrumentation().waitForIdleSync(); sleepForFloatingToolbarPopup(); assertFloatingToolbarIsDisplayed(); assertFloatingToolbarContainsItem( getActivity().getString(com.android.internal.R.string.copy)); } } }