diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 749d6214a9c63..e0796e6324af4 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -217,6 +217,7 @@ public class Editor { boolean mInBatchEditControllers; boolean mShowSoftInputOnFocus = true; private boolean mPreserveSelection; + private boolean mRestartActionModeOnNextRefresh; boolean mTemporaryDetach; boolean mIsBeingLongClicked; @@ -381,9 +382,8 @@ public class Editor { updateSpellCheckSpans(0, mTextView.getText().length(), true /* create the spell checker if needed */); - if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) { - // We had an active selection from before, start the selection mode. - startSelectionActionMode(); + if (mTextView.hasSelection()) { + refreshTextActionMode(); } getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true); @@ -1284,7 +1284,7 @@ public class Editor { } final InputMethodManager imm = InputMethodManager.peekInstance(); if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) { - startSelectionActionMode(); + refreshTextActionMode(); } } else { if (mBlink != null) { @@ -1847,6 +1847,7 @@ public class Editor { void refreshTextActionMode() { if (extractedTextModeWillBeStarted()) { + mRestartActionModeOnNextRefresh = false; return; } final boolean hasSelection = mTextView.hasSelection(); @@ -1855,12 +1856,19 @@ public class Editor { if ((selectionController != null && selectionController.isCursorBeingModified()) || (insertionController != null && insertionController.isCursorBeingModified())) { // ActionMode should be managed by the currently active cursor controller. + mRestartActionModeOnNextRefresh = false; return; } if (hasSelection) { - if (mTextActionMode == null || selectionController == null - || !selectionController.isActive()) { - // Avoid dismissing the selection if it exists. + hideInsertionPointCursorController(); + if (mTextActionMode == null) { + 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(); startSelectionActionMode(); } else { @@ -1875,6 +1883,7 @@ public class Editor { mTextActionMode.invalidateContentRect(); } } + mRestartActionModeOnNextRefresh = false; } /** @@ -1905,11 +1914,12 @@ public class Editor { * * @return true if the selection mode was actually started. */ - private boolean startSelectionActionMode() { + boolean startSelectionActionMode() { boolean selectionStarted = startSelectionActionModeInternal(); if (selectionStarted) { getSelectionController().show(); } + mRestartActionModeOnNextRefresh = false; return selectionStarted; } @@ -2113,6 +2123,9 @@ public class Editor { } private void stopTextActionModeWithPreservingSelection() { + if (mTextActionMode != null) { + mRestartActionModeOnNextRefresh = true; + } mPreserveSelection = true; stopTextActionMode(); mPreserveSelection = false; @@ -3707,6 +3720,8 @@ public class Editor { @Override public void onDestroyActionMode(ActionMode mode) { + // Clear mTextActionMode not to recursively destroy action mode by clearing selection. + mTextActionMode = null; Callback customCallback = getCustomCallback(); if (customCallback != null) { customCallback.onDestroyActionMode(mode); @@ -3725,8 +3740,6 @@ public class Editor { if (mSelectionModifierCursorController != null) { mSelectionModifierCursorController.hide(); } - - mTextActionMode = null; } @Override @@ -5129,27 +5142,12 @@ public class Editor { // No longer dragging to select text, let the parent intercept events. mTextView.getParent().requestDisallowInterceptTouchEvent(false); - int startOffset = mTextView.getSelectionStart(); - int endOffset = mTextView.getSelectionEnd(); - - // 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. - Selection.setSelection((Spannable) mTextView.getText(), - startOffset, endOffset); - } - if (startOffset != endOffset) { - startSelectionActionMode(); - } - // No longer the first dragging motion, reset. resetDragAcceleratorState(); + + if (mTextView.hasSelection()) { + startSelectionActionMode(); + } break; } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index e971f865bf455..fbedbdac9065a 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -9212,6 +9212,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (start >= 0 && start <= end && end <= text.length()) { Selection.setSelection((Spannable) text, start, end); + // Make sure selection mode is engaged. + if (mEditor != null) { + mEditor.startSelectionActionMode(); + } return true; } } diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java index 844eadbae692b..4a4727fd891f3 100644 --- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java +++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java @@ -47,6 +47,8 @@ import com.android.frameworks.coretests.R; import android.support.test.espresso.action.EspressoKey; import android.test.ActivityInstrumentationTestCase2; import android.test.suitebuilder.annotation.SmallTest; +import android.text.Selection; +import android.text.Spannable; import android.view.KeyEvent; import static org.hamcrest.Matchers.anyOf; @@ -534,4 +536,68 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2 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)); + } }