Reland: Add basic support for Ctrl-Z to editable TextViews

Reland 9201e797833f35b9afb219f88c10d3b6fda02a4e which was reverted in
c8f08e07a47cc259a25caed3f731ee7044328635.

Original description:
* Add an UndoManager to the Editor for each editable TextView
* Record operations as being owned by that Editor
* Parcel the undo state
* Wire hardware keyboard shortcuts Ctrl-Z for undo and Ctrl-Shift-Z
  for redo into TextView shortcut handling.
* Expose IDs for "undo" and "redo" for symmetry with cut/copy/paste.

Additional fix:
* Ensure each UndoOwner always has a valid mManager, even after the
  UndoManager is parceled and restored.

Bug: 19332904
Bug: 19505388
Change-Id: Iad4476e6e9ca952281e69bf22c07cca915dfa7bd
diff --git a/api/current.txt b/api/current.txt
index 16dfcf7..5fad22c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1713,6 +1713,7 @@
     field public static final int pasteAsPlainText = 16908337; // 0x1020031
     field public static final int primary = 16908300; // 0x102000c
     field public static final int progress = 16908301; // 0x102000d
+    field public static final int redo = 16908339; // 0x1020033
     field public static final int secondaryProgress = 16908303; // 0x102000f
     field public static final int selectAll = 16908319; // 0x102001f
     field public static final int selectTextMode = 16908333; // 0x102002d
@@ -1729,6 +1730,7 @@
     field public static final int text2 = 16908309; // 0x1020015
     field public static final int title = 16908310; // 0x1020016
     field public static final int toggle = 16908311; // 0x1020017
+    field public static final int undo = 16908338; // 0x1020032
     field public static final int widget_frame = 16908312; // 0x1020018
   }
 
diff --git a/api/system-current.txt b/api/system-current.txt
index 1e9d64a..ae9b296 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1789,6 +1789,7 @@
     field public static final int pasteAsPlainText = 16908337; // 0x1020031
     field public static final int primary = 16908300; // 0x102000c
     field public static final int progress = 16908301; // 0x102000d
+    field public static final int redo = 16908339; // 0x1020033
     field public static final int secondaryProgress = 16908303; // 0x102000f
     field public static final int selectAll = 16908319; // 0x102001f
     field public static final int selectTextMode = 16908333; // 0x102002d
@@ -1805,6 +1806,7 @@
     field public static final int text2 = 16908309; // 0x1020015
     field public static final int title = 16908310; // 0x1020016
     field public static final int toggle = 16908311; // 0x1020017
+    field public static final int undo = 16908338; // 0x1020032
     field public static final int widget_frame = 16908312; // 0x1020018
   }
 
diff --git a/core/java/android/content/UndoManager.java b/core/java/android/content/UndoManager.java
index e3bc238..559b01c 100644
--- a/core/java/android/content/UndoManager.java
+++ b/core/java/android/content/UndoManager.java
@@ -105,8 +105,7 @@
             return owner;
         }
 
-        owner = new UndoOwner(tag);
-        owner.mManager = this;
+        owner = new UndoOwner(tag, this);
         owner.mData = data;
         mOwners.put(tag, owner);
         return owner;
@@ -116,7 +115,6 @@
         // XXX need to figure out how to prune.
         if (false) {
             mOwners.remove(owner.mTag);
-            owner.mManager = null;
         }
     }
 
@@ -202,7 +200,7 @@
         UndoOwner owner = mStateOwners[idx];
         if (owner == null) {
             String tag = in.readString();
-            owner = new UndoOwner(tag);
+            owner = new UndoOwner(tag, this);
             mStateOwners[idx] = owner;
             mOwners.put(tag, owner);
         }
diff --git a/core/java/android/content/UndoOwner.java b/core/java/android/content/UndoOwner.java
index d0cdc95..9106588 100644
--- a/core/java/android/content/UndoOwner.java
+++ b/core/java/android/content/UndoOwner.java
@@ -23,8 +23,8 @@
  */
 public class UndoOwner {
     final String mTag;
+    final UndoManager mManager;
 
-    UndoManager mManager;
     Object mData;
     int mOpCount;
 
@@ -32,8 +32,15 @@
     int mStateSeq;
     int mSavedIdx;
 
-    UndoOwner(String tag) {
+    UndoOwner(String tag, UndoManager manager) {
+        if (tag == null) {
+            throw new NullPointerException("tag can't be null");
+        }
+        if (manager == null) {
+            throw new NullPointerException("manager can't be null");
+        }
         mTag = tag;
+        mManager = manager;
     }
 
     /**
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 4752594..8601d2b 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -50,6 +50,7 @@
 import android.inputmethodservice.ExtractEditText;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.ParcelableParcel;
 import android.os.SystemClock;
 import android.provider.Settings;
 import android.text.DynamicLayout;
@@ -118,15 +119,18 @@
  */
 public class Editor {
     private static final String TAG = "Editor";
-    static final boolean DEBUG_UNDO = false;
+    private static final boolean DEBUG_UNDO = false;
 
     static final int BLINK = 500;
     private static final float[] TEMP_POSITION = new float[2];
     private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
+    // Tag used when the Editor maintains its own separate UndoManager.
+    private static final String UNDO_OWNER_TAG = "Editor";
 
-    UndoManager mUndoManager;
-    UndoOwner mUndoOwner;
-    InputFilter mUndoInputFilter;
+    // Each Editor manages its own undo stack.
+    private final UndoManager mUndoManager = new UndoManager();
+    private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
+    final InputFilter mUndoInputFilter = new UndoInputFilter(this);
 
     // Cursor Controllers.
     InsertionPointCursorController mInsertionPointCursorController;
@@ -222,6 +226,39 @@
 
     Editor(TextView textView) {
         mTextView = textView;
+        // Synchronize the filter list, which places the undo input filter at the end.
+        mTextView.setFilters(mTextView.getFilters());
+    }
+
+    ParcelableParcel saveInstanceState() {
+        // For now there is only undo state.
+        return (ParcelableParcel) mUndoManager.saveInstanceState();
+    }
+
+    void restoreInstanceState(ParcelableParcel state) {
+        mUndoManager.restoreInstanceState(state);
+        // Re-associate this object as the owner of undo state.
+        mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
+    }
+
+    boolean canUndo() {
+        UndoOwner[] owners = { mUndoOwner };
+        return mUndoManager.countUndos(owners) > 0;
+    }
+
+    boolean canRedo() {
+        UndoOwner[] owners = { mUndoOwner };
+        return mUndoManager.countRedos(owners) > 0;
+    }
+
+    void undo() {
+        UndoOwner[] owners = { mUndoOwner };
+        mUndoManager.undo(owners, 1);  // Undo 1 action.
+    }
+
+    void redo() {
+        UndoOwner[] owners = { mUndoOwner };
+        mUndoManager.redo(owners, 1);  // Redo 1 action.
     }
 
     void onAttachedToWindow() {
@@ -1706,7 +1743,7 @@
 
     /**
      * Called by the framework in response to a text auto-correction (such as fixing a typo using a
-     * a dictionnary) from the current input method, provided by it calling
+     * a dictionary) from the current input method, provided by it calling
      * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
      * implementation flashes the background of the corrected word to provide feedback to the user.
      *
@@ -4161,8 +4198,12 @@
         int mChangedStart, mChangedEnd, mChangedDelta;
     }
 
+    /**
+     * 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).
+     */
     public static class UndoInputFilter implements InputFilter {
-        final Editor mEditor;
+        private final Editor mEditor;
 
         public UndoInputFilter(Editor editor) {
             mEditor = editor;
@@ -4192,6 +4233,8 @@
                     // 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);
@@ -4245,7 +4288,10 @@
         }
     }
 
-    public static class TextModifyOperation extends UndoOperation<TextView> {
+    /**
+     * An operation to undo a single "edit" to a text view.
+     */
+    public static class TextModifyOperation extends UndoOperation<Editor> {
         int mRangeStart, mRangeEnd;
         CharSequence mOldText;
 
@@ -4277,8 +4323,8 @@
         private void swapText() {
             // Both undo and redo involves swapping the contents of the range
             // in the text view with our local text.
-            TextView tv = getOwnerData();
-            Editable editable = (Editable)tv.getText();
+            Editor editor = getOwnerData();
+            Editable editable = (Editable)editor.mTextView.getText();
             CharSequence curText;
             if (mRangeStart >= mRangeEnd) {
                 curText = null;
@@ -4309,14 +4355,17 @@
 
         public static final Parcelable.ClassLoaderCreator<TextModifyOperation> CREATOR
                 = new Parcelable.ClassLoaderCreator<TextModifyOperation>() {
+            @Override
             public TextModifyOperation createFromParcel(Parcel in) {
                 return new TextModifyOperation(in, null);
             }
 
+            @Override
             public TextModifyOperation createFromParcel(Parcel in, ClassLoader loader) {
                 return new TextModifyOperation(in, loader);
             }
 
+            @Override
             public TextModifyOperation[] newArray(int size) {
                 return new TextModifyOperation[size];
             }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 27603f5..2d0a9cb 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -47,6 +47,7 @@
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ParcelableParcel;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -1612,7 +1613,8 @@
      * @hide
      */
     public final UndoManager getUndoManager() {
-        return mEditor == null ? null : mEditor.mUndoManager;
+        // TODO: Consider supporting a global undo manager.
+        throw new UnsupportedOperationException("not implemented");
     }
 
     /**
@@ -1630,22 +1632,12 @@
      * @hide
      */
     public final void setUndoManager(UndoManager undoManager, String tag) {
-        if (undoManager != null) {
-            createEditorIfNeeded();
-            mEditor.mUndoManager = undoManager;
-            mEditor.mUndoOwner = undoManager.getOwner(tag, this);
-            mEditor.mUndoInputFilter = new Editor.UndoInputFilter(mEditor);
-            if (!(mText instanceof Editable)) {
-                setText(mText, BufferType.EDITABLE);
-            }
-
-            setFilters((Editable) mText, mFilters);
-        } else if (mEditor != null) {
-            // XXX need to destroy all associated state.
-            mEditor.mUndoManager = null;
-            mEditor.mUndoOwner = null;
-            mEditor.mUndoInputFilter = null;
-        }
+        // TODO: Consider supporting a global undo manager. An implementation will need to:
+        // * createEditorIfNeeded()
+        // * Promote to BufferType.EDITABLE if needed.
+        // * Update the UndoManager and UndoOwner.
+        // Likewise it will need to be able to restore the default UndoManager.
+        throw new UnsupportedOperationException("not implemented");
     }
 
     /**
@@ -3899,6 +3891,9 @@
 
             ss.error = getError();
 
+            if (mEditor != null) {
+                ss.editorState = mEditor.saveInstanceState();
+            }
             return ss;
         }
 
@@ -3968,6 +3963,11 @@
                 }
             });
         }
+
+        if (ss.editorState != null) {
+            createEditorIfNeeded();
+            mEditor.restoreInstanceState(ss.editorState);
+        }
     }
 
     /**
@@ -8383,6 +8383,11 @@
                     return onTextContextMenuItem(ID_SELECT_ALL);
                 }
                 break;
+            case KeyEvent.KEYCODE_Z:
+                if (canUndo()) {
+                    return onTextContextMenuItem(ID_UNDO);
+                }
+                break;
             case KeyEvent.KEYCODE_X:
                 if (canCut()) {
                     return onTextContextMenuItem(ID_CUT);
@@ -8402,11 +8407,15 @@
         } else if (event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) {
             // Handle Ctrl-Shift shortcuts.
             switch (keyCode) {
+                case KeyEvent.KEYCODE_Z:
+                    if (canRedo()) {
+                        return onTextContextMenuItem(ID_REDO);
+                    }
+                    break;
                 case KeyEvent.KEYCODE_V:
                     if (canPaste()) {
                         return onTextContextMenuItem(ID_PASTE_AS_PLAIN_TEXT);
                     }
-                    break;
             }
         }
         return super.onKeyShortcut(keyCode, event);
@@ -8784,6 +8793,8 @@
     }
 
     static final int ID_SELECT_ALL = android.R.id.selectAll;
+    static final int ID_UNDO = android.R.id.undo;
+    static final int ID_REDO = android.R.id.redo;
     static final int ID_CUT = android.R.id.cut;
     static final int ID_COPY = android.R.id.copy;
     static final int ID_PASTE = android.R.id.paste;
@@ -8815,6 +8826,18 @@
                 selectAllText();
                 return true;
 
+            case ID_UNDO:
+                if (mEditor != null) {
+                    mEditor.undo();
+                }
+                return true;  // Returns true even if nothing was undone.
+
+            case ID_REDO:
+                if (mEditor != null) {
+                    mEditor.redo();
+                }
+                return true;  // Returns true even if nothing was undone.
+
             case ID_PASTE:
                 paste(min, max, true /* withFormatting */);
                 return true;
@@ -8948,7 +8971,17 @@
      * @hide
      */
     protected void stopSelectionActionMode() {
-        mEditor.stopSelectionActionMode();
+        if (mEditor != null) {
+            mEditor.stopSelectionActionMode();
+        }
+    }
+
+    boolean canUndo() {
+        return mEditor != null && mEditor.canUndo();
+    }
+
+    boolean canRedo() {
+        return mEditor != null && mEditor.canRedo();
     }
 
     boolean canCut() {
@@ -9325,6 +9358,7 @@
         CharSequence text;
         boolean frozenWithFocus;
         CharSequence error;
+        ParcelableParcel editorState;  // Optional state from Editor.
 
         SavedState(Parcelable superState) {
             super(superState);
@@ -9344,6 +9378,13 @@
                 out.writeInt(1);
                 TextUtils.writeToParcel(error, out, flags);
             }
+
+            if (editorState == null) {
+                out.writeInt(0);
+            } else {
+                out.writeInt(1);
+                editorState.writeToParcel(out, flags);
+            }
         }
 
         @Override
@@ -9379,6 +9420,10 @@
             if (in.readInt() != 0) {
                 error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
             }
+
+            if (in.readInt() != 0) {
+                editorState = ParcelableParcel.CREATOR.createFromParcel(in);
+            }
         }
     }
 
diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml
index 6e2f534..d657bad 100644
--- a/core/res/res/values/ids.xml
+++ b/core/res/res/values/ids.xml
@@ -90,4 +90,6 @@
   <item type="id" name="statusBarBackground" />
   <item type="id" name="navigationBarBackground" />
   <item type="id" name="pasteAsPlainText" />
+  <item type="id" name="undo" />
+  <item type="id" name="redo" />
 </resources>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 569c6f6..10b65a6 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2639,5 +2639,9 @@
   <!-- Context menu ID for the "Paste as plain text" menu item to to copy the current contents
           of the clipboard into the text view without formatting. -->
   <public type="id" name="pasteAsPlainText" />
-
+  <!-- Context menu ID for the "Undo" menu item to undo the last text edit operation. -->
+  <public type="id" name="undo" />
+  <!-- Context menu ID for the "Redo" menu item to redo the last text edit operation. -->
+  <public type="id" name="redo" />
+ 
 </resources>