OSDN Git Service

Show Floating Toolbar when tapping a selectable TextLink in TextView.
authorRichard Ledley <rledley@google.com>
Thu, 30 Nov 2017 10:54:08 +0000 (10:54 +0000)
committerRichard Ledley <rledley@google.com>
Mon, 18 Dec 2017 10:12:59 +0000 (10:12 +0000)
Test: bit FrameworksCoreTests:android.widget.TextViewActivityTest\#testToolbarAppearsAfterLinkClicked
Bug: b/67629726
Change-Id: Ied7a1903a308db37d0eb288c8e611da8229f381a

core/java/android/view/textclassifier/TextLinks.java
core/java/android/widget/Editor.java
core/java/android/widget/SelectionActionModeHelper.java
core/java/android/widget/TextView.java
core/tests/coretests/src/android/widget/TextViewActivityTest.java

index 0e039e3..4fe5662 100644 (file)
@@ -22,6 +22,8 @@ import android.annotation.Nullable;
 import android.os.LocaleList;
 import android.text.SpannableString;
 import android.text.style.ClickableSpan;
+import android.view.View;
+import android.widget.TextView;
 
 import com.android.internal.util.Preconditions;
 
@@ -189,9 +191,14 @@ public final class TextLinks {
      * @hide
      */
     public static final Function<TextLink, ClickableSpan> DEFAULT_SPAN_FACTORY =
-            textLink -> {
-                // TODO: Implement.
-                throw new UnsupportedOperationException("Not yet implemented");
+            textLink -> new ClickableSpan() {
+                @Override
+                public void onClick(View widget) {
+                    if (widget instanceof TextView) {
+                        final TextView textView = (TextView) widget;
+                        textView.requestActionMode(textLink);
+                    }
+                }
             };
 
     /**
index a440398..05d18d1 100644 (file)
@@ -107,6 +107,7 @@ import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextLinks;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.TextView.Drawables;
 import android.widget.TextView.OnEditorActionListener;
@@ -174,6 +175,13 @@ public class Editor {
         int SELECTION_END = 2;
     }
 
+    @IntDef({TextActionMode.SELECTION, TextActionMode.INSERTION, TextActionMode.TEXT_LINK})
+    @interface TextActionMode {
+        int SELECTION = 0;
+        int INSERTION = 1;
+        int TEXT_LINK = 2;
+    }
+
     // Each Editor manages its own undo stack.
     private final UndoManager mUndoManager = new UndoManager();
     private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
@@ -2053,7 +2061,7 @@ public class Editor {
         stopTextActionMode();
 
         ActionMode.Callback actionModeCallback =
-                new TextActionModeCallback(false /* hasSelection */);
+                new TextActionModeCallback(TextActionMode.INSERTION);
         mTextActionMode = mTextView.startActionMode(
                 actionModeCallback, ActionMode.TYPE_FLOATING);
         if (mTextActionMode != null && getInsertionController() != null) {
@@ -2079,7 +2087,23 @@ public class Editor {
      * Asynchronously starts a selection action mode using the TextClassifier.
      */
     void startSelectionActionModeAsync(boolean adjustSelection) {
-        getSelectionActionModeHelper().startActionModeAsync(adjustSelection);
+        getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
+    }
+
+    void startLinkActionModeAsync(TextLinks.TextLink link) {
+        Preconditions.checkNotNull(link);
+        if (!(mTextView.getText() instanceof Spannable)) {
+            return;
+        }
+        Spannable text = (Spannable) mTextView.getText();
+        stopTextActionMode();
+        if (mTextView.isTextSelectable()) {
+            Selection.setSelection((Spannable) text, link.getStart(), link.getEnd());
+        } else {
+            //TODO: Nonselectable text
+        }
+
+        getSelectionActionModeHelper().startLinkActionModeAsync(link);
     }
 
     /**
@@ -2145,7 +2169,7 @@ public class Editor {
         return true;
     }
 
-    boolean startSelectionActionModeInternal() {
+    boolean startActionModeInternal(@TextActionMode int actionMode) {
         if (extractedTextModeWillBeStarted()) {
             return false;
         }
@@ -2159,8 +2183,7 @@ public class Editor {
             return false;
         }
 
-        ActionMode.Callback actionModeCallback =
-                new TextActionModeCallback(true /* hasSelection */);
+        ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);
         mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
 
         final boolean selectionStarted = mTextActionMode != null;
@@ -3828,8 +3851,9 @@ public class Editor {
         private final int mHandleHeight;
         private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>();
 
-        public TextActionModeCallback(boolean hasSelection) {
-            mHasSelection = hasSelection;
+        TextActionModeCallback(@TextActionMode int mode) {
+            mHasSelection = mode == TextActionMode.SELECTION
+                    || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK);
             if (mHasSelection) {
                 SelectionModifierCursorController selectionController = getSelectionController();
                 if (selectionController.mStartHandle == null) {
index d0ad27a..2c6466c 100644 (file)
@@ -35,6 +35,7 @@ import android.util.Log;
 import android.view.ActionMode;
 import android.view.textclassifier.TextClassification;
 import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
 import android.view.textclassifier.TextSelection;
 import android.view.textclassifier.logging.SmartSelectionEventTracker;
 import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent;
@@ -97,7 +98,10 @@ public final class SelectionActionModeHelper {
         }
     }
 
-    public void startActionModeAsync(boolean adjustSelection) {
+    /**
+     * Starts Selection ActionMode.
+     */
+    public void startSelectionActionModeAsync(boolean adjustSelection) {
         // Check if the smart selection should run for editable text.
         adjustSelection &= !mTextView.isTextEditable()
                 || mTextView.getTextClassifier().getSettings()
@@ -109,7 +113,7 @@ public final class SelectionActionModeHelper {
                 mTextView.getSelectionEnd());
         cancelAsyncTask();
         if (skipTextClassification()) {
-            startActionMode(null);
+            startSelectionActionMode(null);
         } else {
             resetTextClassificationHelper();
             mTextClassificationAsyncTask = new TextClassificationAsyncTask(
@@ -119,8 +123,27 @@ public final class SelectionActionModeHelper {
                             ? mTextClassificationHelper::suggestSelection
                             : mTextClassificationHelper::classifyText,
                     mSmartSelectSprite != null
-                            ? this::startActionModeWithSmartSelectAnimation
-                            : this::startActionMode)
+                            ? this::startSelectionActionModeWithSmartSelectAnimation
+                            : this::startSelectionActionMode)
+                    .execute();
+        }
+    }
+
+    /**
+     * Starts Link ActionMode.
+     */
+    public void startLinkActionModeAsync(TextLinks.TextLink textLink) {
+        //TODO: tracking/logging
+        cancelAsyncTask();
+        if (skipTextClassification()) {
+            startLinkActionMode(null);
+        } else {
+            resetTextClassificationHelper(textLink.getStart(), textLink.getEnd());
+            mTextClassificationAsyncTask = new TextClassificationAsyncTask(
+                    mTextView,
+                    mTextClassificationHelper.getTimeoutDuration(),
+                    mTextClassificationHelper::classifyText,
+                    this::startLinkActionMode)
                     .execute();
         }
     }
@@ -200,9 +223,19 @@ public final class SelectionActionModeHelper {
         return noOpTextClassifier || noSelection || password;
     }
 
-    private void startActionMode(@Nullable SelectionResult result) {
+    private void startLinkActionMode(@Nullable SelectionResult result) {
+        startActionMode(Editor.TextActionMode.TEXT_LINK, result);
+    }
+
+    private void startSelectionActionMode(@Nullable SelectionResult result) {
+        startActionMode(Editor.TextActionMode.SELECTION, result);
+    }
+
+    private void startActionMode(
+            @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
         final CharSequence text = getText(mTextView);
-        if (result != null && text instanceof Spannable) {
+        if (result != null && text instanceof Spannable
+                && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
             // Do not change the selection if TextClassifier should be dark launched.
             if (!mTextView.getTextClassifier().getSettings().isDarkLaunch()) {
                 Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
@@ -211,12 +244,13 @@ public final class SelectionActionModeHelper {
         } else {
             mTextClassification = null;
         }
-        if (mEditor.startSelectionActionModeInternal()) {
+        if (mEditor.startActionModeInternal(actionMode)) {
             final SelectionModifierCursorController controller = mEditor.getSelectionController();
-            if (controller != null) {
+            if (controller != null
+                    && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
                 controller.show();
             }
-            if (result != null) {
+            if (result != null && actionMode == Editor.TextActionMode.SELECTION) {
                 mSelectionTracker.onSmartSelection(result);
             }
         }
@@ -224,10 +258,11 @@ public final class SelectionActionModeHelper {
         mTextClassificationAsyncTask = null;
     }
 
-    private void startActionModeWithSmartSelectAnimation(@Nullable SelectionResult result) {
+    private void startSelectionActionModeWithSmartSelectAnimation(
+            @Nullable SelectionResult result) {
         final Layout layout = mTextView.getLayout();
 
-        final Runnable onAnimationEndCallback = () -> startActionMode(result);
+        final Runnable onAnimationEndCallback = () -> startSelectionActionMode(result);
         // TODO do not trigger the animation if the change included only non-printable characters
         final boolean didSelectionChange =
                 result != null && (mTextView.getSelectionStart() != result.mStart
@@ -386,15 +421,24 @@ public final class SelectionActionModeHelper {
         mTextClassificationAsyncTask = null;
     }
 
-    private void resetTextClassificationHelper() {
+    private void resetTextClassificationHelper(int selectionStart, int selectionEnd) {
+        if (selectionStart < 0 || selectionEnd < 0) {
+            // Use selection indices
+            selectionStart = mTextView.getSelectionStart();
+            selectionEnd = mTextView.getSelectionEnd();
+        }
         mTextClassificationHelper.init(
                 mTextView.getContext(),
                 mTextView.getTextClassifier(),
                 getText(mTextView),
-                mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
+                selectionStart, selectionEnd,
                 mTextView.getTextLocales());
     }
 
+    private void resetTextClassificationHelper() {
+        resetTextClassificationHelper(-1, -1);
+    }
+
     private void cancelSmartSelectAnimation() {
         if (mSmartSelectSprite != null) {
             mSmartSelectSprite.cancelAnimation();
index 903d3ca..9ac443b 100644 (file)
@@ -160,6 +160,7 @@ import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 import android.view.textclassifier.TextClassificationManager;
 import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
 import android.view.textservice.SpellCheckerSubtype;
 import android.view.textservice.TextServicesManager;
 import android.widget.RemoteViews.RemoteView;
@@ -168,6 +169,7 @@ import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.FastMath;
+import com.android.internal.util.Preconditions;
 import com.android.internal.widget.EditableInputConnection;
 
 import libcore.util.EmptyArray;
@@ -11151,6 +11153,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
     }
 
     /**
+     * Starts an ActionMode for the specified TextLink.
+     *
+     * @return Whether or not we're attempting to start the action mode.
+     * @hide
+     */
+    public boolean requestActionMode(@NonNull TextLinks.TextLink link) {
+        Preconditions.checkNotNull(link);
+        if (mEditor != null) {
+            mEditor.startLinkActionModeAsync(link);
+            return true;
+        }
+        return false;
+    }
+    /**
      * @hide
      */
     protected void stopTextActionMode() {
index 0e460b9..1a654f4 100644 (file)
@@ -27,8 +27,10 @@ import static android.support.test.espresso.matcher.ViewMatchers.withId;
 import static android.support.test.espresso.matcher.ViewMatchers.withText;
 import static android.widget.espresso.CustomViewActions.longPressAtRelativeCoordinates;
 import static android.widget.espresso.DragHandleUtils.onHandleView;
-import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarContainsItem;
-import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarDoesNotContainItem;
+import static android.widget.espresso.FloatingToolbarEspressoUtils
+        .assertFloatingToolbarContainsItem;
+import static android.widget.espresso.FloatingToolbarEspressoUtils
+        .assertFloatingToolbarDoesNotContainItem;
 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed;
 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarItemIndex;
 import static android.widget.espresso.FloatingToolbarEspressoUtils.clickFloatingToolbarItem;
@@ -68,12 +70,15 @@ import android.test.suitebuilder.annotation.Suppress;
 import android.text.InputType;
 import android.text.Selection;
 import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
 import android.view.ActionMode;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.textclassifier.TextClassificationManager;
 import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
 import android.widget.espresso.CustomViewActions.RelativeCoordinatesProvider;
 
 import com.android.frameworks.coretests.R;
@@ -305,6 +310,33 @@ public class TextViewActivityTest {
     }
 
     @Test
+    public void testToolbarAppearsAfterLinkClicked() throws Throwable {
+        useSystemDefaultTextClassifier();
+        TextClassificationManager textClassificationManager =
+                mActivity.getSystemService(TextClassificationManager.class);
+        TextClassifier textClassifier = textClassificationManager.getTextClassifier();
+        final TextView textView = mActivity.findViewById(R.id.textview);
+        SpannableString content = new SpannableString("Call me at +19148277737");
+        TextLinks links = textClassifier.generateLinks(content);
+        links.apply(content, null);
+
+        mActivityRule.runOnUiThread(() -> {
+            textView.setText(content);
+            textView.setMovementMethod(LinkMovementMethod.getInstance());
+        });
+        mInstrumentation.waitForIdleSync();
+
+        // Wait for the UI thread to refresh
+        Thread.sleep(1000);
+
+        TextLinks.TextLink textLink = links.getLinks().iterator().next();
+        int position = (textLink.getStart() + textLink.getEnd()) / 2;
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(position));
+        sleepForFloatingToolbarPopup();
+        assertFloatingToolbarIsDisplayed();
+    }
+
+    @Test
     public void testToolbarAndInsertionHandle() {
         final String text = "text";
         onView(withId(R.id.textview)).perform(replaceText(text));