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

Commit 55cb4787 authored by James Cook's avatar James Cook Committed by The Android Automerger
Browse files

Revert "Improvements to TextView Ctrl-Z undo support"

This reverts commit 7713d188.

It causes crashes on text input after device orientation change.

Bug: 19332904
Bug: 19450037
BUG: 19505388
parent 7b8016c1
Loading
Loading
Loading
Loading
+105 −215
Original line number Diff line number Diff line
@@ -4198,13 +4198,6 @@ public class Editor {
        int mChangedStart, mChangedEnd, mChangedDelta;
    }

    /**
     * @return True iff (start, end) is a valid range within the text.
     */
    private static boolean isValidRange(CharSequence text, int start, int end) {
        return 0 <= start && start <= end && end <= text.length();
    }

    /**
     * An InputFilter that monitors text input to maintain undo history. It does not modify the
     * text being typed (and hence always returns null from the filter() method).
@@ -4220,123 +4213,97 @@ public class Editor {
        public CharSequence filter(CharSequence source, int start, int end,
                Spanned dest, int dstart, int dend) {
            if (DEBUG_UNDO) {
                Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") " +
                        "dest=" + dest + " (" + dstart + "-" + dend + ")");
                Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ")");
                Log.d(TAG, "filter: dest=" + dest + " (" + dstart + "-" + dend + ")");
            }
            final UndoManager um = mEditor.mUndoManager;
            if (um.isInUndo()) {
                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
                if (DEBUG_UNDO) Log.d(TAG, "*** skipping, currently performing undo/redo");
                return null;
            }

            // Text filters run before input operations are applied. However, some input operations
            // are invalid and will throw exceptions when applied. This is common in tests. Don't
            // attempt to undo invalid operations.
            if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
                if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
            um.beginUpdate("Edit text");
            TextModifyOperation op = um.getLastOperation(
                    TextModifyOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
            if (op != null) {
                if (DEBUG_UNDO) Log.d(TAG, "Last op: range=(" + op.mRangeStart + "-" + op.mRangeEnd
                        + "), oldText=" + op.mOldText);
                // See if we can continue modifying this operation.
                if (op.mOldText == null) {
                    // The current operation is an add...  are we adding more?  We are adding
                    // more if we are either appending new text to the end of the last edit or
                    // completely replacing some or all of the last edit.
                    // TODO: This sequence doesn't work right: a, left-arrow, b, undo, undo.
                    // The two edits are incorrectly merged, so there is only one undo available.
                    if (start < end && ((dstart >= op.mRangeStart && dend <= op.mRangeEnd)
                            || (dstart == op.mRangeEnd && dend == op.mRangeEnd))) {
                        op.mRangeEnd = dstart + (end-start);
                        um.endUpdate();
                        if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, mRangeEnd="
                                + op.mRangeEnd);
                        return null;
                    }

            // Earlier filters can rewrite input to be a no-op, for example due to a length limit
            // on an input field. Skip no-op changes.
            if (start == end && dstart == dend) {
                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
                } else {
                    // The current operation is a delete...  can we delete more?
                    if (start == end && dend == op.mRangeStart-1) {
                        SpannableStringBuilder str;
                        if (op.mOldText instanceof SpannableString) {
                            str = (SpannableStringBuilder)op.mOldText;
                        } else {
                            str = new SpannableStringBuilder(op.mOldText);
                        }
                        str.insert(0, dest, dstart, dend);
                        op.mRangeStart = dstart;
                        op.mOldText = str;
                        um.endUpdate();
                        if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, range=("
                                + op.mRangeStart + "-" + op.mRangeEnd
                                + "), oldText=" + op.mOldText);
                        return null;
                    }
                }

            // Build a new operation with all the information from this edit.
            EditOperation edit = new EditOperation(mEditor, source, start, end, dest, dstart, dend);
                // Couldn't add to the current undo operation, need to start a new
                // undo state for a new undo operation.
                um.commitState(null);
                um.setUndoLabel("Edit text");
            }

            // Fetch the last edit operation and attempt to merge in the new edit.
            um.beginUpdate("Edit text");
            EditOperation lastEdit = um.getLastOperation(
                  EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
            if (lastEdit == null) {
                // Add this as the first edit.
                if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
            } else if (lastEdit.mergeWith(edit)) {
                // Merge succeeded, nothing else to do.
                if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
            // Create a new undo state reflecting the operation being performed.
            op = new TextModifyOperation(mEditor.mUndoOwner);
            op.mRangeStart = dstart;
            if (start < end) {
                op.mRangeEnd = dstart + (end-start);
            } else {
                // Could not merge with the last edit, so commit the last edit and add this edit.
                if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
                um.commitState(mEditor.mUndoOwner);
                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
                op.mRangeEnd = dstart;
            }
            if (dstart < dend) {
                op.mOldText = dest.subSequence(dstart, dend);
            }
            if (DEBUG_UNDO) Log.d(TAG, "*** adding new op, range=(" + op.mRangeStart
                    + "-" + op.mRangeEnd + "), oldText=" + op.mOldText);
            um.addOperation(op, UndoManager.MERGE_MODE_NONE);
            um.endUpdate();
            return null;  // Text not changed.
            return null;
        }
    }

    /**
     * An operation to undo a single "edit" to a text view.
     */
    public static class EditOperation extends UndoOperation<Editor> {
        private static final int TYPE_INSERT = 0;
        private static final int TYPE_DELETE = 1;
        private static final int TYPE_REPLACE = 2;

        private int mType;
        private String mOldText;
        private int mOldTextStart;
        private String mNewText;
        private int mNewTextStart;
    public static class TextModifyOperation extends UndoOperation<Editor> {
        int mRangeStart, mRangeEnd;
        CharSequence mOldText;

        private int mOldCursorPos;
        private int mNewCursorPos;

        /**
         * Constructs an edit operation from a text input operation that replaces the range
         * (dstart, dend) of dest with (start, end) of source. See {@link InputFilter#filter}.
         */
        public EditOperation(Editor editor, CharSequence source, int start, int end,
                Spanned dest, int dstart, int dend) {
            super(editor.mUndoOwner);

            mOldText = dest.subSequence(dstart, dend).toString();
            mNewText = source.subSequence(start, end).toString();

            // Determine the type of the edit and store where it occurred. Avoid storing
            // irrevelant data (e.g. mNewTextStart for a delete) because that makes the
            // merging logic more complex (e.g. merging deletes could lead to mNewTextStart being
            // outside the bounds of the final text).
            if (mNewText.length() > 0 && mOldText.length() == 0) {
                mType = TYPE_INSERT;
                mNewTextStart = dstart;
            } else if (mNewText.length() == 0 && mOldText.length() > 0) {
                mType = TYPE_DELETE;
                mOldTextStart = dstart;
            } else {
                mType = TYPE_REPLACE;
                mOldTextStart = mNewTextStart = dstart;
            }

            // Store cursor data.
            mOldCursorPos = editor.mTextView.getSelectionStart();
            mNewCursorPos = dstart + (end - start);
        public TextModifyOperation(UndoOwner owner) {
            super(owner);
        }

        public EditOperation(Parcel src, ClassLoader loader) {
        public TextModifyOperation(Parcel src, ClassLoader loader) {
            super(src, loader);
            mType = src.readInt();
            mOldText = src.readString();
            mOldTextStart = src.readInt();
            mNewText = src.readString();
            mNewTextStart = src.readInt();
            mOldCursorPos = src.readInt();
            mNewCursorPos = src.readInt();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mType);
            dest.writeString(mOldText);
            dest.writeInt(mOldTextStart);
            dest.writeString(mNewText);
            dest.writeInt(mNewTextStart);
            dest.writeInt(mOldCursorPos);
            dest.writeInt(mNewCursorPos);
            mRangeStart = src.readInt();
            mRangeEnd = src.readInt();
            mOldText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src);
        }

        @Override
@@ -4345,139 +4312,62 @@ public class Editor {

        @Override
        public void undo() {
            if (DEBUG_UNDO) Log.d(TAG, "undo");
            // Remove the new text and insert the old.
            modifyText(mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart, mOldCursorPos);
            swapText();
        }

        @Override
        public void redo() {
            if (DEBUG_UNDO) Log.d(TAG, "redo");
            // Remove the old text and insert the new.
            modifyText(mOldTextStart, getOldTextEnd(), mNewText, mNewTextStart, mNewCursorPos);
            swapText();
        }

        /**
         * Attempts to merge this existing operation with a new edit.
         * @param edit The new edit operation.
         * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
         * object unchanged.
         */
        private boolean mergeWith(EditOperation edit) {
            switch (mType) {
                case TYPE_INSERT:
                    return mergeInsertWith(edit);
                case TYPE_DELETE:
                    return mergeDeleteWith(edit);
                case TYPE_REPLACE:
                    return mergeReplaceWith(edit);
                default:
                    return false;
            }
        }

        private boolean mergeInsertWith(EditOperation edit) {
            if (DEBUG_UNDO) Log.d(TAG, "mergeInsertWith " + edit);
            // Only merge continuous insertions.
            if (edit.mType != TYPE_INSERT) {
                return false;
            }
            // Only merge insertions that are contiguous.
            if (getNewTextEnd() != edit.mNewTextStart) {
                return false;
            }
            mNewText += edit.mNewText;
            mNewCursorPos = edit.mNewCursorPos;
            return true;
        }

        // TODO: Support forward delete.
        private boolean mergeDeleteWith(EditOperation edit) {
            if (DEBUG_UNDO) Log.d(TAG, "mergeDeleteWith " + edit);
            // Only merge continuous deletes.
            if (edit.mType != TYPE_DELETE) {
                return false;
            }
            // Only merge deletions that are contiguous.
            if (mOldTextStart != edit.getOldTextEnd()) {
                return false;
            }
            mOldTextStart = edit.mOldTextStart;
            mOldText = edit.mOldText + mOldText;
            mNewCursorPos = edit.mNewCursorPos;
            return true;
        }

        private boolean mergeReplaceWith(EditOperation edit) {
            if (DEBUG_UNDO) Log.d(TAG, "mergeReplaceWith " + edit);
            // Replacements can merge only with adjacent inserts and adjacent replacements.
            if (edit.mType == TYPE_DELETE ||
                    getNewTextEnd() != edit.mOldTextStart ||
                    edit.mOldTextStart != edit.mNewTextStart) {
                return false;
            }
            mOldText += edit.mOldText;
            mNewText += edit.mNewText;
            mNewCursorPos = edit.mNewCursorPos;
            return true;
        }

        private int getNewTextEnd() {
            return mNewTextStart + mNewText.length();
        }

        private int getOldTextEnd() {
            return mOldTextStart + mOldText.length();
        }

        private void modifyText(int deleteFrom, int deleteTo, CharSequence newText,
                int newTextInsertAt, int newCursorPos) {
        private void swapText() {
            // Both undo and redo involves swapping the contents of the range
            // in the text view with our local text.
            Editor editor = getOwnerData();
            Editable text = (Editable) editor.mTextView.getText();
            // Apply the edit if it is still valid.
            if (isValidRange(text, deleteFrom, deleteTo) &&
                    newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
                if (deleteFrom != deleteTo) {
                    text.delete(deleteFrom, deleteTo);
                }
                if (newText.length() != 0) {
                    text.insert(newTextInsertAt, newText);
            Editable editable = (Editable)editor.mTextView.getText();
            CharSequence curText;
            if (mRangeStart >= mRangeEnd) {
                curText = null;
            } else {
                curText = editable.subSequence(mRangeStart, mRangeEnd);
            }
            if (DEBUG_UNDO) {
                Log.d(TAG, "Swap: range=(" + mRangeStart + "-" + mRangeEnd
                        + "), oldText=" + mOldText);
                Log.d(TAG, "Swap: curText=" + curText);
            }
            // Restore the cursor position.
            // TODO: Select all the text that was undone.
            if (newCursorPos <= text.length()) {
                Selection.setSelection(text, newCursorPos);
            if (mOldText == null) {
                editable.delete(mRangeStart, mRangeEnd);
                mRangeEnd = mRangeStart;
            } else {
                editable.replace(mRangeStart, mRangeEnd, mOldText);
                mRangeEnd = mRangeStart + mOldText.length();
            }
            mOldText = curText;
        }

        @Override
        public String toString() {
            return "EditOperation: [" +
                    "mType=" + mType + ", " +
                    "mOldText=" + mOldText + ", " +
                    "mOldTextStart=" + mOldTextStart + ", " +
                    "mNewText=" + mNewText + ", " +
                    "mNewTextStart=" + mNewTextStart + ", " +
                    "mOldCursorPos=" + mOldCursorPos + ", " +
                    "mNewCursorPos=" + mNewCursorPos + "]";
        }

        public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR
                = new Parcelable.ClassLoaderCreator<EditOperation>() {
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mRangeStart);
            dest.writeInt(mRangeEnd);
            TextUtils.writeToParcel(mOldText, dest, flags);
        }

        public static final Parcelable.ClassLoaderCreator<TextModifyOperation> CREATOR
                = new Parcelable.ClassLoaderCreator<TextModifyOperation>() {
            @Override
            public EditOperation createFromParcel(Parcel in) {
                return new EditOperation(in, null);
            public TextModifyOperation createFromParcel(Parcel in) {
                return new TextModifyOperation(in, null);
            }

            @Override
            public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
                return new EditOperation(in, loader);
            public TextModifyOperation createFromParcel(Parcel in, ClassLoader loader) {
                return new TextModifyOperation(in, loader);
            }

            @Override
            public EditOperation[] newArray(int size) {
                return new EditOperation[size];
            public TextModifyOperation[] newArray(int size) {
                return new TextModifyOperation[size];
            }
        };
    }