Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bf1a0e23 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "start/invalidate selection actionMode asynchronously"

parents c03fdb3e 8710ea13
Loading
Loading
Loading
Loading
+57 −60
Original line number Diff line number Diff line
@@ -106,7 +106,6 @@ import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.textclassifier.TextClassificationResult;
import android.view.textclassifier.TextSelection;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.TextView.Drawables;
import android.widget.TextView.OnEditorActionListener;
@@ -169,7 +168,7 @@ public class Editor {
    private InsertionPointCursorController mInsertionPointCursorController;
    SelectionModifierCursorController mSelectionModifierCursorController;
    // Action mode used when text is selected or when actions on an insertion cursor are triggered.
    ActionMode mTextActionMode;
    private ActionMode mTextActionMode;
    private boolean mInsertionControllerEnabled;
    private boolean mSelectionControllerEnabled;

@@ -236,7 +235,7 @@ public class Editor {
    private boolean mPreserveSelection;
    private boolean mRestartActionModeOnNextRefresh;

    private TextClassificationResult mTextClassificationResult;
    private SelectionActionModeHelper mSelectionActionModeHelper;

    boolean mIsBeingLongClicked;

@@ -292,7 +291,7 @@ public class Editor {

    private Rect mTempRect;

    private TextView mTextView;
    private final TextView mTextView;

    final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;

@@ -1850,7 +1849,7 @@ public class Editor {
            mInsertionPointCursorController.invalidateHandle();
        }
        if (mTextActionMode != null) {
            invalidateActionMode(getTextClassifierInfo(false));
            invalidateActionModeAsync();
        }
    }

@@ -1943,12 +1942,12 @@ public class Editor {
                if (mRestartActionModeOnNextRefresh) {
                    // To avoid distraction, newly start action mode only when selection action
                    // mode is being restarted.
                    startSelectionActionMode(null);
                    startSelectionActionMode();
                }
            } else if (selectionController == null || !selectionController.isActive()) {
                // Insertion action mode is active. Avoid dismissing the selection.
                stopTextActionModeWithPreservingSelection();
                startSelectionActionMode(null);
                startSelectionActionMode();
            } else {
                mTextActionMode.invalidateContentRect();
            }
@@ -1985,55 +1984,46 @@ public class Editor {
        }
    }

    /**
     * Starts a Selection Action Mode with the current selection and ensures the selection handles
     * are shown if there is a selection. This should be used when the mode is started from a
     * non-touch event.
     *
     * @return true if the selection mode was actually started.
     */
    boolean startSelectionActionMode(@Nullable TextClassificationResult textClassificationResult) {
        mTextClassificationResult = textClassificationResult;
        boolean selectionStarted = startSelectionActionModeInternal();
        if (selectionStarted) {
            getSelectionController().show();
    @NonNull
    TextView getTextView() {
        return mTextView;
    }
        mRestartActionModeOnNextRefresh = false;
        return selectionStarted;

    @Nullable
    ActionMode getTextActionMode() {
        return mTextActionMode;
    }

    private boolean startSelectionActionModeWithTextAssistant() {
        return startSelectionActionMode(getTextClassifierInfo(true));
    void setRestartActionModeOnNextRefresh(boolean value) {
        mRestartActionModeOnNextRefresh = value;
    }

    private void invalidateActionMode(TextClassificationResult textClassificationResult) {
        mTextClassificationResult = textClassificationResult;
        mTextActionMode.invalidate();
    /**
     * Asynchronously starts a selection action mode using the TextClassifier.
     */
    void startSelectionActionModeAsync() {
        getSelectionActionModeHelper().startActionModeAsync();
    }

    // TODO: Make this a non-blocking call.
    private TextClassificationResult getTextClassifierInfo(boolean updateSelection) {
        // TODO: Trim the text so that only text necessary to provide context of the selected
        // text is sent to the assistant.
        final int trimStartIndex = 0;
        final int trimEndIndex = mTextView.getText().length();
        CharSequence trimmedText =
                mTextView.getText().subSequence(trimStartIndex, trimEndIndex);
        int startIndex = mTextView.getSelectionStart() - trimStartIndex;
        int endIndex = mTextView.getSelectionEnd() - trimStartIndex;
    /**
     * Synchronously starts a selection action mode without the TextClassifier.
     */
    void startSelectionActionMode() {
        getSelectionActionModeHelper().startActionMode();
    }

        if (updateSelection) {
            TextSelection textSelection = mTextView.getTextClassifier()
                    .suggestSelection(trimmedText, startIndex, endIndex);
            startIndex = Math.max(0, textSelection.getSelectionStartIndex() + trimStartIndex);
            endIndex = Math.min(mTextView.getText().length(),
                    textSelection.getSelectionEndIndex() + trimStartIndex);
            Selection.setSelection((Spannable) mTextView.getText(), startIndex, endIndex);
            return getTextClassifierInfo(false);
    /**
     * Asynchronously invalidates an action mode using the TextClassifier.
     */
    private void invalidateActionModeAsync() {
        getSelectionActionModeHelper().invalidateActionModeAsync();
    }

        return mTextView.getTextClassifier()
                .getTextClassificationResult(trimmedText, startIndex, endIndex);
    private SelectionActionModeHelper getSelectionActionModeHelper() {
        if (mSelectionActionModeHelper == null) {
            mSelectionActionModeHelper = new SelectionActionModeHelper(this);
        }
        return mSelectionActionModeHelper;
    }

    /**
@@ -2076,13 +2066,13 @@ public class Editor {
        return true;
    }

    private boolean startSelectionActionModeInternal() {
    boolean startSelectionActionModeInternal() {
        if (extractedTextModeWillBeStarted()) {
            return false;
        }
        if (mTextActionMode != null) {
            // Text action mode is already started
            invalidateActionMode(getTextClassifierInfo(false));
            invalidateActionModeAsync();
            return false;
        }

@@ -2273,7 +2263,8 @@ public class Editor {
        return mInsertionPointCursorController;
    }

    private SelectionModifierCursorController getSelectionController() {
    @Nullable
    SelectionModifierCursorController getSelectionController() {
        if (!mSelectionControllerEnabled) {
            return null;
        }
@@ -3772,7 +3763,7 @@ public class Editor {
            mode.setSubtitle(null);
            mode.setTitleOptionalHint(true);
            populateMenuWithItems(menu);
            updateAssistMenuItem(menu, mTextClassificationResult);
            updateAssistMenuItem(menu);

            Callback customCallback = getCustomCallback();
            if (customCallback != null) {
@@ -3840,7 +3831,7 @@ public class Editor {
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            updateSelectAllItem(menu);
            updateReplaceItem(menu);
            updateAssistMenuItem(menu, mTextClassificationResult);
            updateAssistMenuItem(menu);

            Callback customCallback = getCustomCallback();
            if (customCallback != null) {
@@ -3873,9 +3864,10 @@ public class Editor {
            }
        }

        private void updateAssistMenuItem(
                Menu menu, TextClassificationResult textClassificationResult) {
        private void updateAssistMenuItem(Menu menu) {
            menu.removeItem(TextView.ID_ASSIST);
            final TextClassificationResult textClassificationResult =
                    getSelectionActionModeHelper().getTextClassificationResult();
            if (textClassificationResult != null) {
                final Drawable icon = textClassificationResult.getIcon();
                final CharSequence label = textClassificationResult.getLabel();
@@ -3900,7 +3892,8 @@ public class Editor {
            if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
                return true;
            }
            final TextClassificationResult textClassificationResult = mTextClassificationResult;
            final TextClassificationResult textClassificationResult =
                    getSelectionActionModeHelper().getTextClassificationResult();
            if (TextView.ID_ASSIST == item.getItemId() && textClassificationResult != null) {
                final OnClickListener onClickListener =
                        textClassificationResult.getOnClickListener();
@@ -3923,8 +3916,8 @@ public class Editor {
        @Override
        public void onDestroyActionMode(ActionMode mode) {
            // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
            getSelectionActionModeHelper().cancelAsyncTask();
            mTextActionMode = null;
            mTextClassificationResult = null;
            Callback customCallback = getCustomCallback();
            if (customCallback != null) {
                customCallback.onDestroyActionMode(mode);
@@ -4683,7 +4676,7 @@ public class Editor {
            }
            positionAtCursorOffset(offset, false);
            if (mTextActionMode != null) {
                invalidateActionMode(getTextClassifierInfo(false));
                invalidateActionModeAsync();
            }
        }

@@ -4767,7 +4760,7 @@ public class Editor {
            }
            updateDrawable();
            if (mTextActionMode != null) {
                invalidateActionMode(getTextClassifierInfo(false));
                invalidateActionModeAsync();
            }
        }

@@ -5416,8 +5409,12 @@ public class Editor {

                    if (mTextView.hasSelection()) {
                        // Do not invoke the text assistant if this was a drag selection.
                        startSelectionActionMode(
                                mHaventMovedEnoughToStartDrag ? getTextClassifierInfo(true) : null);
                        if (mHaventMovedEnoughToStartDrag) {
                            startSelectionActionModeAsync();
                        } else {
                            startSelectionActionMode();
                        }

                    }
                    break;
            }
+288 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.widget;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.annotation.WorkerThread;
import android.os.AsyncTask;
import android.text.Selection;
import android.text.Spannable;
import android.text.TextUtils;
import android.view.ActionMode;
import android.view.textclassifier.TextClassificationResult;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextSelection;
import android.widget.Editor.SelectionModifierCursorController;

import com.android.internal.util.Preconditions;

import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * Helper class for starting selection action mode
 * (synchronously without the TextClassifier, asynchronously with the TextClassifier).
 */
@UiThread
final class SelectionActionModeHelper {

    /**
     * Maximum time (in milliseconds) to wait for a result before timing out.
     */
    // TODO: Consider making this a ViewConfiguration.
    private static final int TIMEOUT_DURATION = 200;

    private final Editor mEditor;
    private final TextClassificationHelper mTextClassificationHelper;

    private TextClassificationResult mTextClassificationResult;
    private AsyncTask mTextClassificationAsyncTask;

    SelectionActionModeHelper(@NonNull Editor editor) {
        mEditor = Preconditions.checkNotNull(editor);
        final TextView textView = mEditor.getTextView();
        mTextClassificationHelper = new TextClassificationHelper(
                textView.getTextClassifier(), textView.getText(),
                textView.getSelectionStart(), textView.getSelectionEnd());
    }

    public void startActionModeAsync() {
        cancelAsyncTask();
        if (isNoOpTextClassifier()) {
            // No need to make an async call for a no-op TextClassifier.
            startActionMode(null);
        } else {
            resetTextClassificationHelper();
            mTextClassificationAsyncTask = new TextClassificationAsyncTask(
                    mEditor.getTextView(), TIMEOUT_DURATION,
                    mTextClassificationHelper::suggestSelection, this::startActionMode)
                    .execute();
        }
    }

    public void startActionMode() {
        startActionMode(null);
    }

    public void invalidateActionModeAsync() {
        cancelAsyncTask();
        if (isNoOpTextClassifier()) {
            // No need to make an async call for a no-op TextClassifier.
            invalidateActionMode(null);
        } else {
            resetTextClassificationHelper();
            mTextClassificationAsyncTask = new TextClassificationAsyncTask(
                    mEditor.getTextView(), TIMEOUT_DURATION,
                    mTextClassificationHelper::classifyText, this::invalidateActionMode)
                    .execute();
        }
    }

    public void cancelAsyncTask() {
        if (mTextClassificationAsyncTask != null) {
            mTextClassificationAsyncTask.cancel(true);
            mTextClassificationAsyncTask = null;
        }
        mTextClassificationResult = null;
    }

    @Nullable
    public TextClassificationResult getTextClassificationResult() {
        return mTextClassificationResult;
    }

    private boolean isNoOpTextClassifier() {
        return mEditor.getTextView().getTextClassifier() == TextClassifier.NO_OP;
    }

    private void startActionMode(@Nullable SelectionResult result) {
        final CharSequence text = mEditor.getTextView().getText();
        if (result != null && text instanceof Spannable) {
            Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
            mTextClassificationResult = result.mResult;
        } else {
            mTextClassificationResult = null;
        }
        if (mEditor.startSelectionActionModeInternal()) {
            final SelectionModifierCursorController controller = mEditor.getSelectionController();
            if (controller != null) {
                controller.show();
            }
        }
        mEditor.setRestartActionModeOnNextRefresh(false);
        mTextClassificationAsyncTask = null;
    }

    private void invalidateActionMode(@Nullable SelectionResult result) {
        mTextClassificationResult = result != null ? result.mResult : null;
        final ActionMode actionMode = mEditor.getTextActionMode();
        if (actionMode != null) {
            actionMode.invalidate();
        }
        mTextClassificationAsyncTask = null;
    }

    private void resetTextClassificationHelper() {
        final TextView textView = mEditor.getTextView();
        mTextClassificationHelper.reset(textView.getTextClassifier(), textView.getText(),
                textView.getSelectionStart(), textView.getSelectionEnd());
    }

    /**
     * AsyncTask for running a query on a background thread and returning the result on the
     * UiThread. The AsyncTask times out after a specified time, returning a null result if the
     * query has not yet returned.
     */
    private static final class TextClassificationAsyncTask
            extends AsyncTask<Void, Void, SelectionResult> {

        private final int mTimeOutDuration;
        private final Supplier<SelectionResult> mSelectionResultSupplier;
        private final Consumer<SelectionResult> mSelectionResultCallback;
        private final TextView mTextView;
        private final String mOriginalText;

        /**
         * @param textView the TextView
         * @param timeOut time in milliseconds to timeout the query if it has not completed
         * @param selectionResultSupplier fetches the selection results. Runs on a background thread
         * @param selectionResultCallback receives the selection results. Runs on the UiThread
         */
        TextClassificationAsyncTask(
                @NonNull TextView textView, int timeOut,
                @NonNull Supplier<SelectionResult> selectionResultSupplier,
                @NonNull Consumer<SelectionResult> selectionResultCallback) {
            mTextView = Preconditions.checkNotNull(textView);
            mTimeOutDuration = timeOut;
            mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier);
            mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback);
            // Make a copy of the original text.
            mOriginalText = mTextView.getText().toString();
        }

        @Override
        @WorkerThread
        protected SelectionResult doInBackground(Void... params) {
            final Runnable onTimeOut = this::onTimeOut;
            mTextView.postDelayed(onTimeOut, mTimeOutDuration);
            final SelectionResult result = mSelectionResultSupplier.get();
            mTextView.removeCallbacks(onTimeOut);
            return result;
        }

        @Override
        @UiThread
        protected void onPostExecute(SelectionResult result) {
            result = TextUtils.equals(mOriginalText, mTextView.getText()) ? result : null;
            mSelectionResultCallback.accept(result);
        }

        private void onTimeOut() {
            if (getStatus() == Status.RUNNING) {
                onPostExecute(null);
            }
            cancel(true);
        }
    }

    /**
     * Helper class for querying the TextClassifier.
     * It trims text so that only text necessary to provide context of the selected text is
     * sent to the TextClassifier.
     */
    private static final class TextClassificationHelper {

        private static final int TRIM_DELTA = 50;  // characters

        private TextClassifier mTextClassifier;

        /** The original TextView text. **/
        private String mText;
        /** Start index relative to mText. */
        private int mSelectionStart;
        /** End index relative to mText. */
        private int mSelectionEnd;

        /** Trimmed text starting from mTrimStart in mText. */
        private CharSequence mTrimmedText;
        /** Index indicating the start of mTrimmedText in mText. */
        private int mTrimStart;
        /** Start index relative to mTrimmedText */
        private int mRelativeStart;
        /** End index relative to mTrimmedText */
        private int mRelativeEnd;

        TextClassificationHelper(TextClassifier textClassifier,
                CharSequence text, int selectionStart, int selectionEnd) {
            reset(textClassifier, text, selectionStart, selectionEnd);
        }

        @UiThread
        public void reset(TextClassifier textClassifier,
                CharSequence text, int selectionStart, int selectionEnd) {
            mTextClassifier = Preconditions.checkNotNull(textClassifier);
            mText = Preconditions.checkNotNull(text).toString();
            mSelectionStart = selectionStart;
            mSelectionEnd = selectionEnd;
        }

        @WorkerThread
        public SelectionResult classifyText() {
            trimText();
            return new SelectionResult(
                    mSelectionStart,
                    mSelectionEnd,
                    mTextClassifier.getTextClassificationResult(
                            mTrimmedText, mRelativeStart, mRelativeEnd));
        }

        @WorkerThread
        public SelectionResult suggestSelection() {
            trimText();
            final TextSelection sel = mTextClassifier.suggestSelection(
                    mTrimmedText, mRelativeStart, mRelativeEnd);
            mSelectionStart = Math.max(0, sel.getSelectionStartIndex() + mTrimStart);
            mSelectionEnd = Math.min(mText.length(), sel.getSelectionEndIndex() + mTrimStart);
            return classifyText();
        }

        private void trimText() {
            mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA);
            final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA);
            mTrimmedText = mText.subSequence(mTrimStart, referenceEnd);
            mRelativeStart = mSelectionStart - mTrimStart;
            mRelativeEnd = mSelectionEnd - mTrimStart;
        }
    }

    /**
     * Selection result.
     */
    private static final class SelectionResult {
        private final int mStart;
        private final int mEnd;
        private final TextClassificationResult mResult;

        SelectionResult(int start, int end, TextClassificationResult result) {
            mStart = start;
            mEnd = end;
            mResult = Preconditions.checkNotNull(result);
        }
    }
}
+5 −4
Original line number Diff line number Diff line
@@ -6642,7 +6642,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
     */
    public boolean handleBackInTextActionModeIfNeeded(KeyEvent event) {
        // Do nothing unless mEditor is in text action mode.
        if (mEditor == null || mEditor.mTextActionMode == null) {
        if (mEditor == null || mEditor.getTextActionMode() == null) {
            return false;
        }

@@ -6826,7 +6826,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener

                // Has to be done on key down (and not on key up) to correctly be intercepted.
            case KeyEvent.KEYCODE_BACK:
                if (mEditor != null && mEditor.mTextActionMode != null) {
                if (mEditor != null && mEditor.getTextActionMode() != null) {
                    stopTextActionMode();
                    return KEY_EVENT_HANDLED;
                }
@@ -9019,7 +9019,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener

                if (mEditor != null) {
                    mEditor.refreshTextActionMode();
                    if (!hasSelection() && mEditor.mTextActionMode == null && hasTransientState()) {
                    if (!hasSelection()
                            && mEditor.getTextActionMode() == null && hasTransientState()) {
                        // User generated selection has been removed.
                        setHasTransientState(false);
                    }
@@ -10166,7 +10167,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                        Selection.setSelection((Spannable) text, start, end);
                        // Make sure selection mode is engaged.
                        if (mEditor != null) {
                            mEditor.startSelectionActionMode(null);
                            mEditor.startSelectionActionModeAsync();
                        }
                        return true;
                    }