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

Commit c62589cb authored by Gilles Debunne's avatar Gilles Debunne
Browse files

Editor uses a SpanWatcher to track EasyEditSpans

Will also fix Bug 6344997

The previous TextWatcher mechanism was inneficient. It require an
expensive getSpans() call to retrieve all the spans and then search
for the one we're interested in in case it has been changed.

The SpanWatcher is faster, it will broadcast the add/changed/removed
events we're interested in.

Now that we can rely on SpanWatcher, use it to directly track
addition and removals of EasyEditSpans.

No unit test for this feature which require an integration with
the voice IME. Easy to test manually though.

Change-Id: Idabcacc48c479bf9868d5204c0b0ca709207ede2
parent e267f5f2
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ public class ExtractedText implements Parcelable {
    /**
     * If the content is a report of a partial text change, this is the offset
     * where the change ends.  Note that the actual text may be larger or
     * smaller than the difference between this and {@link #partialEndOffset},
     * smaller than the difference between this and {@link #partialStartOffset},
     * meaning a reduction or increase, respectively, in the total text.
     */
    public int partialEndOffset;
+69 −118
Original line number Diff line number Diff line
@@ -47,7 +47,6 @@ import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.KeyListener;
import android.text.method.MetaKeyKeyListener;
import android.text.method.MovementMethod;
@@ -183,8 +182,6 @@ public class Editor {

    Editor(TextView textView) {
        mTextView = textView;
        mEasyEditSpanController = new EasyEditSpanController();
        mTextView.addTextChangedListener(mEasyEditSpanController);
    }

    void onAttachedToWindow() {
@@ -1120,7 +1117,7 @@ public class Editor {
            if (contentChanged || ims.mSelectionModeChanged) {
                ims.mContentChanged = false;
                ims.mSelectionModeChanged = false;
                final ExtractedTextRequest req = ims.mExtracting;
                final ExtractedTextRequest req = ims.mExtractedTextRequest;
                if (req != null) {
                    InputMethodManager imm = InputMethodManager.peekInstance();
                    if (imm != null) {
@@ -1132,13 +1129,14 @@ public class Editor {
                            ims.mChangedStart = EXTRACT_NOTHING;
                        }
                        if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
                                ims.mChangedDelta, ims.mTmpExtracted)) {
                                ims.mChangedDelta, ims.mExtractedText)) {
                            if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
                                    "Reporting extracted start=" +
                                    ims.mTmpExtracted.partialStartOffset +
                                    " end=" + ims.mTmpExtracted.partialEndOffset +
                                    ": " + ims.mTmpExtracted.text);
                            imm.updateExtractedText(mTextView, req.token, ims.mTmpExtracted);
                                    ims.mExtractedText.partialStartOffset +
                                    " end=" + ims.mExtractedText.partialEndOffset +
                                    ": " + ims.mExtractedText.text);

                            imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
                            ims.mChangedStart = EXTRACT_UNKNOWN;
                            ims.mChangedEnd = EXTRACT_UNKNOWN;
                            ims.mChangedDelta = 0;
@@ -1734,45 +1732,50 @@ public class Editor {
        }
    }

    public void addSpanWatchers(Spannable text) {
        final int textLength = text.length();

        if (mKeyListener != null) {
            text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        }

        if (mEasyEditSpanController == null) {
            mEasyEditSpanController = new EasyEditSpanController();
        }
        text.setSpan(mEasyEditSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
    }

    /**
     * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
     * pop-up should be displayed.
     */
    class EasyEditSpanController implements TextWatcher {
    class EasyEditSpanController implements SpanWatcher {

        private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs

        private EasyEditPopupWindow mPopupWindow;

        private EasyEditSpan mEasyEditSpan;

        private Runnable mHidePopup;

        public void hide() {
            if (mPopupWindow != null) {
                mPopupWindow.hide();
                mTextView.removeCallbacks(mHidePopup);
            }
            removeSpans(mTextView.getText());
            mEasyEditSpan = null;
        @Override
        public void onSpanAdded(Spannable text, Object span, int start, int end) {
            if (span instanceof EasyEditSpan) {
                if (mPopupWindow == null) {
                    mPopupWindow = new EasyEditPopupWindow();
                    mHidePopup = new Runnable() {
                        @Override
                        public void run() {
                            hide();
                        }

        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            // Intentionally empty
                    };
                }

        public void afterTextChanged(Editable s) {
            // Intentionally empty
                // Make sure there is only at most one EasyEditSpan in the text
                if (mPopupWindow.mEasyEditSpan != null) {
                    text.removeSpan(mPopupWindow.mEasyEditSpan);
                }

        /**
         * Monitors the changes in the text.
         *
         * <p>{@link SpanWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
         * as the notifications are not sent when a spannable (with spans) is inserted.
         */
        public void onTextChanged(CharSequence buffer, int start, int before, int after) {
            adjustSpans(buffer, start, after);
                mPopupWindow.setEasyEditSpan((EasyEditSpan) span);

                if (mTextView.getWindowVisibility() != View.VISIBLE) {
                    // The window is not visible yet, ignore the text change.
@@ -1780,92 +1783,41 @@ public class Editor {
                }

                if (mTextView.getLayout() == null) {
                // The view has not been layout yet, ignore the text change
                    // The view has not been laid out yet, ignore the text change
                    return;
                }

            InputMethodManager imm = InputMethodManager.peekInstance();
            if (!(mTextView instanceof ExtractEditText) && imm != null && imm.isFullscreenMode()) {
                // The input is in extract mode. We do not have to handle the easy edit in the
                // original TextView, as the ExtractEditText will do
                if (extractedTextModeWillBeStarted()) {
                    // The input is in extract mode. Do not handle the easy edit in
                    // the original TextView, as the ExtractEditText will do
                    return;
                }

            // Remove the current easy edit span, as the text changed, and remove the pop-up
            // (if any)
            if (mEasyEditSpan != null) {
                if (buffer instanceof Spannable) {
                    ((Spannable) buffer).removeSpan(mEasyEditSpan);
                }
                mEasyEditSpan = null;
            }
            if (mPopupWindow != null && mPopupWindow.isShowing()) {
                mPopupWindow.hide();
            }

            // Display the new easy edit span (if any).
            if (buffer instanceof Spanned) {
                mEasyEditSpan = getSpan((Spanned) buffer);
                if (mEasyEditSpan != null) {
                    if (mPopupWindow == null) {
                        mPopupWindow = new EasyEditPopupWindow();
                        mHidePopup = new Runnable() {
                            @Override
                            public void run() {
                                hide();
                            }
                        };
                    }
                    mPopupWindow.show(mEasyEditSpan);
                mPopupWindow.show();
                mTextView.removeCallbacks(mHidePopup);
                mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
            }
        }
        }

        /**
         * Adjusts the spans by removing all of them except the last one.
         */
        private void adjustSpans(CharSequence buffer, int start, int after) {
            // This method enforces that only one easy edit span is attached to the text.
            // A better way to enforce this would be to listen for onSpanAdded, but this method
            // cannot be used in this scenario as no notification is triggered when a text with
            // spans is inserted into a text.
            if (buffer instanceof Spannable) {
                Spannable spannable = (Spannable) buffer;
                EasyEditSpan[] spans = spannable.getSpans(start, start + after, EasyEditSpan.class);
                if (spans.length > 0) {
                    // Assuming there was only one EasyEditSpan before, we only need check to
                    // check for a duplicate if a new one is found in the modified interval
                    spans = spannable.getSpans(0, spannable.length(),  EasyEditSpan.class);
                    for (int i = 1; i < spans.length; i++) {
                        spannable.removeSpan(spans[i]);
                    }
                }
        @Override
        public void onSpanRemoved(Spannable text, Object span, int start, int end) {
            if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
                hide();
            }
        }

        /**
         * Removes all the {@link EasyEditSpan} currently attached.
         */
        private void removeSpans(CharSequence buffer) {
            if (buffer instanceof Spannable) {
                Spannable spannable = (Spannable) buffer;
                EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
                        EasyEditSpan.class);
                for (int i = 0; i < spans.length; i++) {
                    spannable.removeSpan(spans[i]);
                }
        @Override
        public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
                int newStart, int newEnd) {
            if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
                text.removeSpan(mPopupWindow.mEasyEditSpan);
            }
        }

        private EasyEditSpan getSpan(Spanned spanned) {
            EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
                    EasyEditSpan.class);
            if (easyEditSpans.length == 0) {
                return null;
            } else {
                return easyEditSpans[0];
        public void hide() {
            if (mPopupWindow != null) {
                mPopupWindow.hide();
                mTextView.removeCallbacks(mHidePopup);
            }
        }
    }
@@ -1910,9 +1862,8 @@ public class Editor {
            mContentView.addView(mDeleteTextView);
        }

        public void show(EasyEditSpan easyEditSpan) {
        public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
            mEasyEditSpan = easyEditSpan;
            super.show();
        }

        @Override
@@ -3738,8 +3689,8 @@ public class Editor {
        Rect mCursorRectInWindow = new Rect();
        RectF mTmpRectF = new RectF();
        float[] mTmpOffset = new float[2];
        ExtractedTextRequest mExtracting;
        final ExtractedText mTmpExtracted = new ExtractedText();
        ExtractedTextRequest mExtractedTextRequest;
        final ExtractedText mExtractedText = new ExtractedText();
        int mBatchEditNesting;
        boolean mCursorChanged;
        boolean mSelectionModeChanged;
+3 −2
Original line number Diff line number Diff line
@@ -207,6 +207,7 @@ public class SpellChecker implements SpellCheckerSessionListener {

    public void spellCheck(int start, int end) {
        final Locale locale = mTextView.getTextServicesLocale();
        final boolean isSessionActive = isSessionActive();
        if (mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
            setLocale(locale);
            // Re-check the entire text
@@ -214,13 +215,13 @@ public class SpellChecker implements SpellCheckerSessionListener {
            end = mTextView.getText().length();
        } else {
            final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
            if (isSessionActive() != spellCheckerActivated) {
            if (isSessionActive != spellCheckerActivated) {
                // Spell checker has been turned of or off since last spellCheck
                resetSession();
            }
        }

        if (!isSessionActive()) return;
        if (!isSessionActive) return;

        // Find first available SpellParser from pool
        final int length = mSpellParsers.length;
+8 −11
Original line number Diff line number Diff line
@@ -3176,22 +3176,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        if (text instanceof Spannable && !mAllowTransformationLengthChange) {
            Spannable sp = (Spannable) text;

            // Remove any ChangeWatchers that might have come
            // from other TextViews.
            // Remove any ChangeWatchers that might have come from other TextViews.
            final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
            final int count = watchers.length;
            for (int i = 0; i < count; i++)
            for (int i = 0; i < count; i++) {
                sp.removeSpan(watchers[i]);
            }

            if (mChangeWatcher == null)
                mChangeWatcher = new ChangeWatcher();
            if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();

            sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
                       (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));

            if (mEditor != null && getEditor().mKeyListener != null) {
                sp.setSpan(getEditor().mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            }
            if (mEditor != null) mEditor.addSpanWatchers(sp);

            if (mTransformation != null) {
                sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
@@ -5231,7 +5228,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
     */
    public void setExtracting(ExtractedTextRequest req) {
        if (getEditor().mInputMethodState != null) {
            getEditor().mInputMethodState.mExtracting = req;
            getEditor().mInputMethodState.mExtractedTextRequest = req;
        }
        // This would stop a possible selection mode, but no such mode is started in case
        // extracted mode will start. Some text is selected though, and will trigger an action mode
@@ -6876,7 +6873,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        if (what instanceof ParcelableSpan) {
            // If this is a span that can be sent to a remote process,
            // the current extract editor would be interested in it.
            if (ims != null && ims.mExtracting != null) {
            if (ims != null && ims.mExtractedTextRequest != null) {
                if (ims.mBatchEditNesting != 0) {
                    if (oldStart >= 0) {
                        if (ims.mChangedStart > oldStart) {
@@ -6897,7 +6894,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                } else {
                    if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
                            + oldStart + "-" + oldEnd + ","
                            + newStart + "-" + newEnd + what);
                            + newStart + "-" + newEnd + " " + what);
                    ims.mContentChanged = true;
                }
            }