1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content;
18
19import android.os.Parcel;
20import android.os.Parcelable;
21import android.os.ParcelableParcel;
22import android.text.TextUtils;
23import android.util.ArrayMap;
24
25import java.util.ArrayList;
26
27/**
28 * Top-level class for managing and interacting with the global undo state for
29 * a document or application.  This class supports both undo and redo and has
30 * helpers for merging undoable operations together as they are performed.
31 *
32 * <p>A single undoable operation is represented by {@link UndoOperation} which
33 * apps implement to define their undo/redo behavior.  The UndoManager keeps
34 * a stack of undo states; each state can have one or more undo operations
35 * inside of it.</p>
36 *
37 * <p>Updates to the stack must be done inside of a {@link #beginUpdate}/{@link #endUpdate()}
38 * pair.  During this time you can add new operations to the stack with
39 * {@link #addOperation}, retrieve and modify existing operations with
40 * {@link #getLastOperation}, control the label shown to the user for this operation
41 * with {@link #setUndoLabel} and {@link #suggestUndoLabel}, etc.</p>
42 *
43 * <p>Every {link UndoOperation} is associated with an {@link UndoOwner}, which identifies
44 * the data it belongs to.  The owner is used to indicate how operations are dependent
45 * on each other -- operations with the same owner are dependent on others with the
46 * same owner.  For example, you may have a document with multiple embedded objects.  If the
47 * document itself and each embedded object use different owners, then you
48 * can provide undo semantics appropriate to the user's context: while within
49 * an embedded object, only edits to that object are seen and the user can
50 * undo/redo them without needing to impact edits in other objects; while
51 * within the larger document, all edits can be seen and the user must
52 * undo/redo them as a single stream.</p>
53 *
54 * @hide
55 */
56public class UndoManager {
57    // The common case is a single undo owner (e.g. for a TextView), so default to that capacity.
58    private final ArrayMap<String, UndoOwner> mOwners =
59            new ArrayMap<String, UndoOwner>(1 /* capacity */);
60    private final ArrayList<UndoState> mUndos = new ArrayList<UndoState>();
61    private final ArrayList<UndoState> mRedos = new ArrayList<UndoState>();
62    private int mUpdateCount;
63    private int mHistorySize = 20;
64    private UndoState mWorking;
65    private int mCommitId = 1;
66    private boolean mInUndo;
67    private boolean mMerged;
68
69    private int mStateSeq;
70    private int mNextSavedIdx;
71    private UndoOwner[] mStateOwners;
72
73    /**
74     * Never merge with the last undo state.
75     */
76    public static final int MERGE_MODE_NONE = 0;
77
78    /**
79     * Allow merge with the last undo state only if it contains
80     * operations with the caller's owner.
81     */
82    public static final int MERGE_MODE_UNIQUE = 1;
83
84    /**
85     * Always allow merge with the last undo state, if possible.
86     */
87    public static final int MERGE_MODE_ANY = 2;
88
89    public UndoOwner getOwner(String tag, Object data) {
90        if (tag == null) {
91            throw new NullPointerException("tag can't be null");
92        }
93        if (data == null) {
94            throw new NullPointerException("data can't be null");
95        }
96        UndoOwner owner = mOwners.get(tag);
97        if (owner != null) {
98            if (owner.mData != data) {
99                if (owner.mData != null) {
100                    throw new IllegalStateException("Owner " + owner + " already exists with data "
101                            + owner.mData + " but giving different data " + data);
102                }
103                owner.mData = data;
104            }
105            return owner;
106        }
107
108        owner = new UndoOwner(tag, this);
109        owner.mData = data;
110        mOwners.put(tag, owner);
111        return owner;
112    }
113
114    void removeOwner(UndoOwner owner) {
115        // XXX need to figure out how to prune.
116        if (false) {
117            mOwners.remove(owner.mTag);
118        }
119    }
120
121    /**
122     * Flatten the current undo state into a Parcel object, which can later be restored
123     * with {@link #restoreInstanceState(android.os.Parcel, java.lang.ClassLoader)}.
124     */
125    public void saveInstanceState(Parcel p) {
126        if (mUpdateCount > 0) {
127            throw new IllegalStateException("Can't save state while updating");
128        }
129        mStateSeq++;
130        if (mStateSeq <= 0) {
131            mStateSeq = 0;
132        }
133        mNextSavedIdx = 0;
134        p.writeInt(mHistorySize);
135        p.writeInt(mOwners.size());
136        // XXX eventually we need to be smart here about limiting the
137        // number of undo states we write to not exceed X bytes.
138        int i = mUndos.size();
139        while (i > 0) {
140            p.writeInt(1);
141            i--;
142            mUndos.get(i).writeToParcel(p);
143        }
144        i = mRedos.size();
145        p.writeInt(i);
146        while (i > 0) {
147            p.writeInt(2);
148            i--;
149            mRedos.get(i).writeToParcel(p);
150        }
151        p.writeInt(0);
152    }
153
154    void saveOwner(UndoOwner owner, Parcel out) {
155        if (owner.mStateSeq == mStateSeq) {
156            out.writeInt(owner.mSavedIdx);
157        } else {
158            owner.mStateSeq = mStateSeq;
159            owner.mSavedIdx = mNextSavedIdx;
160            out.writeInt(owner.mSavedIdx);
161            out.writeString(owner.mTag);
162            out.writeInt(owner.mOpCount);
163            mNextSavedIdx++;
164        }
165    }
166
167    /**
168     * Restore an undo state previously created with {@link #saveInstanceState(Parcel)}.  This
169     * will restore the UndoManager's state to almost exactly what it was at the point it had
170     * been previously saved; the only information not restored is the data object
171     * associated with each {@link UndoOwner}, which requires separate calls to
172     * {@link #getOwner(String, Object)} to re-associate the owner with its data.
173     */
174    public void restoreInstanceState(Parcel p, ClassLoader loader) {
175        if (mUpdateCount > 0) {
176            throw new IllegalStateException("Can't save state while updating");
177        }
178        forgetUndos(null, -1);
179        forgetRedos(null, -1);
180        mHistorySize = p.readInt();
181        mStateOwners = new UndoOwner[p.readInt()];
182
183        int stype;
184        while ((stype=p.readInt()) != 0) {
185            UndoState ustate = new UndoState(this, p, loader);
186            if (stype == 1) {
187                mUndos.add(0, ustate);
188            } else {
189                mRedos.add(0, ustate);
190            }
191        }
192    }
193
194    UndoOwner restoreOwner(Parcel in) {
195        int idx = in.readInt();
196        UndoOwner owner = mStateOwners[idx];
197        if (owner == null) {
198            String tag = in.readString();
199            int opCount = in.readInt();
200            owner = new UndoOwner(tag, this);
201            owner.mOpCount = opCount;
202            mStateOwners[idx] = owner;
203            mOwners.put(tag, owner);
204        }
205        return owner;
206    }
207
208    /**
209     * Set the maximum number of undo states that will be retained.
210     */
211    public void setHistorySize(int size) {
212        mHistorySize = size;
213        if (mHistorySize >= 0 && countUndos(null) > mHistorySize) {
214            forgetUndos(null, countUndos(null) - mHistorySize);
215        }
216    }
217
218    /**
219     * Return the current maximum number of undo states.
220     */
221    public int getHistorySize() {
222        return mHistorySize;
223    }
224
225    /**
226     * Perform undo of last/top <var>count</var> undo states.  The states impacted
227     * by this can be limited through <var>owners</var>.
228     * @param owners Optional set of owners that should be impacted.  If null, all
229     * undo states will be visible and available for undo.  If non-null, only those
230     * states that contain one of the owners specified here will be visible.
231     * @param count Number of undo states to pop.
232     * @return Returns the number of undo states that were actually popped.
233     */
234    public int undo(UndoOwner[] owners, int count) {
235        if (mWorking != null) {
236            throw new IllegalStateException("Can't be called during an update");
237        }
238
239        int num = 0;
240        int i = -1;
241
242        mInUndo = true;
243
244        UndoState us = getTopUndo(null);
245        if (us != null) {
246            us.makeExecuted();
247        }
248
249        while (count > 0 && (i=findPrevState(mUndos, owners, i)) >= 0) {
250            UndoState state = mUndos.remove(i);
251            state.undo();
252            mRedos.add(state);
253            count--;
254            num++;
255        }
256
257        mInUndo = false;
258
259        return num;
260    }
261
262    /**
263     * Perform redo of last/top <var>count</var> undo states in the transient redo stack.
264     * The states impacted by this can be limited through <var>owners</var>.
265     * @param owners Optional set of owners that should be impacted.  If null, all
266     * undo states will be visible and available for undo.  If non-null, only those
267     * states that contain one of the owners specified here will be visible.
268     * @param count Number of undo states to pop.
269     * @return Returns the number of undo states that were actually redone.
270     */
271    public int redo(UndoOwner[] owners, int count) {
272        if (mWorking != null) {
273            throw new IllegalStateException("Can't be called during an update");
274        }
275
276        int num = 0;
277        int i = -1;
278
279        mInUndo = true;
280
281        while (count > 0 && (i=findPrevState(mRedos, owners, i)) >= 0) {
282            UndoState state = mRedos.remove(i);
283            state.redo();
284            mUndos.add(state);
285            count--;
286            num++;
287        }
288
289        mInUndo = false;
290
291        return num;
292    }
293
294    /**
295     * Returns true if we are currently inside of an undo/redo operation.  This is
296     * useful for editors to know whether they should be generating new undo state
297     * when they see edit operations happening.
298     */
299    public boolean isInUndo() {
300        return mInUndo;
301    }
302
303    public int forgetUndos(UndoOwner[] owners, int count) {
304        if (count < 0) {
305            count = mUndos.size();
306        }
307
308        int removed = 0;
309        int i = 0;
310        while (i < mUndos.size() && removed < count) {
311            UndoState state = mUndos.get(i);
312            if (count > 0 && matchOwners(state, owners)) {
313                state.destroy();
314                mUndos.remove(i);
315                removed++;
316            } else {
317                i++;
318            }
319        }
320
321        return removed;
322    }
323
324    public int forgetRedos(UndoOwner[] owners, int count) {
325        if (count < 0) {
326            count = mRedos.size();
327        }
328
329        int removed = 0;
330        int i = 0;
331        while (i < mRedos.size() && removed < count) {
332            UndoState state = mRedos.get(i);
333            if (count > 0 && matchOwners(state, owners)) {
334                state.destroy();
335                mRedos.remove(i);
336                removed++;
337            } else {
338                i++;
339            }
340        }
341
342        return removed;
343    }
344
345    /**
346     * Return the number of undo states on the undo stack.
347     * @param owners If non-null, only those states containing an operation with one of
348     * the owners supplied here will be counted.
349     */
350    public int countUndos(UndoOwner[] owners) {
351        if (owners == null) {
352            return mUndos.size();
353        }
354
355        int count=0;
356        int i=0;
357        while ((i=findNextState(mUndos, owners, i)) >= 0) {
358            count++;
359            i++;
360        }
361        return count;
362    }
363
364    /**
365     * Return the number of redo states on the undo stack.
366     * @param owners If non-null, only those states containing an operation with one of
367     * the owners supplied here will be counted.
368     */
369    public int countRedos(UndoOwner[] owners) {
370        if (owners == null) {
371            return mRedos.size();
372        }
373
374        int count=0;
375        int i=0;
376        while ((i=findNextState(mRedos, owners, i)) >= 0) {
377            count++;
378            i++;
379        }
380        return count;
381    }
382
383    /**
384     * Return the user-visible label for the top undo state on the stack.
385     * @param owners If non-null, will select the top-most undo state containing an
386     * operation with one of the owners supplied here.
387     */
388    public CharSequence getUndoLabel(UndoOwner[] owners) {
389        UndoState state = getTopUndo(owners);
390        return state != null ? state.getLabel() : null;
391    }
392
393    /**
394     * Return the user-visible label for the top redo state on the stack.
395     * @param owners If non-null, will select the top-most undo state containing an
396     * operation with one of the owners supplied here.
397     */
398    public CharSequence getRedoLabel(UndoOwner[] owners) {
399        UndoState state = getTopRedo(owners);
400        return state != null ? state.getLabel() : null;
401    }
402
403    /**
404     * Start creating a new undo state.  Multiple calls to this function will nest until
405     * they are all matched by a later call to {@link #endUpdate}.
406     * @param label Optional user-visible label for this new undo state.
407     */
408    public void beginUpdate(CharSequence label) {
409        if (mInUndo) {
410            throw new IllegalStateException("Can't being update while performing undo/redo");
411        }
412        if (mUpdateCount <= 0) {
413            createWorkingState();
414            mMerged = false;
415            mUpdateCount = 0;
416        }
417
418        mWorking.updateLabel(label);
419        mUpdateCount++;
420    }
421
422    private void createWorkingState() {
423        mWorking = new UndoState(this, mCommitId++);
424        if (mCommitId < 0) {
425            mCommitId = 1;
426        }
427    }
428
429    /**
430     * Returns true if currently inside of a {@link #beginUpdate}.
431     */
432    public boolean isInUpdate() {
433        return mUpdateCount > 0;
434    }
435
436    /**
437     * Forcibly set a new for the new undo state being built within a {@link #beginUpdate}.
438     * Any existing label will be replaced with this one.
439     */
440    public void setUndoLabel(CharSequence label) {
441        if (mWorking == null) {
442            throw new IllegalStateException("Must be called during an update");
443        }
444        mWorking.setLabel(label);
445    }
446
447    /**
448     * Set a new for the new undo state being built within a {@link #beginUpdate}, but
449     * only if there is not a label currently set for it.
450     */
451    public void suggestUndoLabel(CharSequence label) {
452        if (mWorking == null) {
453            throw new IllegalStateException("Must be called during an update");
454        }
455        mWorking.updateLabel(label);
456    }
457
458    /**
459     * Return the number of times {@link #beginUpdate} has been called without a matching
460     * {@link #endUpdate} call.
461     */
462    public int getUpdateNestingLevel() {
463        return mUpdateCount;
464    }
465
466    /**
467     * Check whether there is an {@link UndoOperation} in the current {@link #beginUpdate}
468     * undo state.
469     * @param owner Optional owner of the operation to look for.  If null, will succeed
470     * if there is any operation; if non-null, will only succeed if there is an operation
471     * with the given owner.
472     * @return Returns true if there is a matching operation in the current undo state.
473     */
474    public boolean hasOperation(UndoOwner owner) {
475        if (mWorking == null) {
476            throw new IllegalStateException("Must be called during an update");
477        }
478        return mWorking.hasOperation(owner);
479    }
480
481    /**
482     * Return the most recent {@link UndoOperation} that was added to the update.
483     * @param mergeMode May be either {@link #MERGE_MODE_NONE} or {@link #MERGE_MODE_ANY}.
484     */
485    public UndoOperation<?> getLastOperation(int mergeMode) {
486        return getLastOperation(null, null, mergeMode);
487    }
488
489    /**
490     * Return the most recent {@link UndoOperation} that was added to the update and
491     * has the given owner.
492     * @param owner Optional owner of last operation to retrieve.  If null, the last
493     * operation regardless of owner will be retrieved; if non-null, the last operation
494     * matching the given owner will be retrieved.
495     * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
496     * or {@link #MERGE_MODE_ANY}.
497     */
498    public UndoOperation<?> getLastOperation(UndoOwner owner, int mergeMode) {
499        return getLastOperation(null, owner, mergeMode);
500    }
501
502    /**
503     * Return the most recent {@link UndoOperation} that was added to the update and
504     * has the given owner.
505     * @param clazz Optional class of the last operation to retrieve.  If null, the
506     * last operation regardless of class will be retrieved; if non-null, the last
507     * operation whose class is the same as the given class will be retrieved.
508     * @param owner Optional owner of last operation to retrieve.  If null, the last
509     * operation regardless of owner will be retrieved; if non-null, the last operation
510     * matching the given owner will be retrieved.
511     * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
512     * or {@link #MERGE_MODE_ANY}.
513     */
514    public <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner,
515            int mergeMode) {
516        if (mWorking == null) {
517            throw new IllegalStateException("Must be called during an update");
518        }
519        if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
520            UndoState state = getTopUndo(null);
521            UndoOperation<?> last;
522            if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners())
523                    && state.canMerge() && (last=state.getLastOperation(clazz, owner)) != null) {
524                if (last.allowMerge()) {
525                    mWorking.destroy();
526                    mWorking = state;
527                    mUndos.remove(state);
528                    mMerged = true;
529                    return (T)last;
530                }
531            }
532        }
533
534        return mWorking.getLastOperation(clazz, owner);
535    }
536
537    /**
538     * Add a new UndoOperation to the current update.
539     * @param op The new operation to add.
540     * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
541     * or {@link #MERGE_MODE_ANY}.
542     */
543    public void addOperation(UndoOperation<?> op, int mergeMode) {
544        if (mWorking == null) {
545            throw new IllegalStateException("Must be called during an update");
546        }
547        UndoOwner owner = op.getOwner();
548        if (owner.mManager != this) {
549            throw new IllegalArgumentException(
550                    "Given operation's owner is not in this undo manager.");
551        }
552        if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
553            UndoState state = getTopUndo(null);
554            if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners())
555                    && state.canMerge() && state.hasOperation(op.getOwner())) {
556                mWorking.destroy();
557                mWorking = state;
558                mUndos.remove(state);
559                mMerged = true;
560            }
561        }
562        mWorking.addOperation(op);
563    }
564
565    /**
566     * Finish the creation of an undo state, matching a previous call to
567     * {@link #beginUpdate}.
568     */
569    public void endUpdate() {
570        if (mWorking == null) {
571            throw new IllegalStateException("Must be called during an update");
572        }
573        mUpdateCount--;
574
575        if (mUpdateCount == 0) {
576            pushWorkingState();
577        }
578    }
579
580    private void pushWorkingState() {
581        int N = mUndos.size() + 1;
582
583        if (mWorking.hasData()) {
584            mUndos.add(mWorking);
585            forgetRedos(null, -1);
586            mWorking.commit();
587            if (N >= 2) {
588                // The state before this one can no longer be merged, ever.
589                // The only way to get back to it is for the user to perform
590                // an undo.
591                mUndos.get(N-2).makeExecuted();
592            }
593        } else {
594            mWorking.destroy();
595        }
596        mWorking = null;
597
598        if (mHistorySize >= 0 && N > mHistorySize) {
599            forgetUndos(null, N - mHistorySize);
600        }
601    }
602
603    /**
604     * Commit the last finished undo state.  This undo state can no longer be
605     * modified with further {@link #MERGE_MODE_UNIQUE} or
606     * {@link #MERGE_MODE_ANY} merge modes.  If called while inside of an update,
607     * this will push any changes in the current update on to the undo stack
608     * and result with a fresh undo state, behaving as if {@link #endUpdate()}
609     * had been called enough to unwind the current update, then the last state
610     * committed, and {@link #beginUpdate} called to restore the update nesting.
611     * @param owner The optional owner to determine whether to perform the commit.
612     * If this is non-null, the commit will only execute if the current top undo
613     * state contains an operation with the given owner.
614     * @return Returns an integer identifier for the committed undo state, which
615     * can later be used to try to uncommit the state to perform further edits on it.
616     */
617    public int commitState(UndoOwner owner) {
618        if (mWorking != null && mWorking.hasData()) {
619            if (owner == null || mWorking.hasOperation(owner)) {
620                mWorking.setCanMerge(false);
621                int commitId = mWorking.getCommitId();
622                pushWorkingState();
623                createWorkingState();
624                mMerged = true;
625                return commitId;
626            }
627        } else {
628            UndoState state = getTopUndo(null);
629            if (state != null && (owner == null || state.hasOperation(owner))) {
630                state.setCanMerge(false);
631                return state.getCommitId();
632            }
633        }
634        return -1;
635    }
636
637    /**
638     * Attempt to undo a previous call to {@link #commitState}.  This will work
639     * if the undo state at the top of the stack has the given id, and has not been
640     * involved in an undo operation.  Otherwise false is returned.
641     * @param commitId The identifier for the state to be uncommitted, as returned
642     * by {@link #commitState}.
643     * @param owner Optional owner that must appear in the committed state.
644     * @return Returns true if the uncommit is successful, else false.
645     */
646    public boolean uncommitState(int commitId, UndoOwner owner) {
647        if (mWorking != null && mWorking.getCommitId() == commitId) {
648            if (owner == null || mWorking.hasOperation(owner)) {
649                return mWorking.setCanMerge(true);
650            }
651        } else {
652            UndoState state = getTopUndo(null);
653            if (state != null && (owner == null || state.hasOperation(owner))) {
654                if (state.getCommitId() == commitId) {
655                    return state.setCanMerge(true);
656                }
657            }
658        }
659        return false;
660    }
661
662    UndoState getTopUndo(UndoOwner[] owners) {
663        if (mUndos.size() <= 0) {
664            return null;
665        }
666        int i = findPrevState(mUndos, owners, -1);
667        return i >= 0 ? mUndos.get(i) : null;
668    }
669
670    UndoState getTopRedo(UndoOwner[] owners) {
671        if (mRedos.size() <= 0) {
672            return null;
673        }
674        int i = findPrevState(mRedos, owners, -1);
675        return i >= 0 ? mRedos.get(i) : null;
676    }
677
678    boolean matchOwners(UndoState state, UndoOwner[] owners) {
679        if (owners == null) {
680            return true;
681        }
682        for (int i=0; i<owners.length; i++) {
683            if (state.matchOwner(owners[i])) {
684                return true;
685            }
686        }
687        return false;
688    }
689
690    int findPrevState(ArrayList<UndoState> states, UndoOwner[] owners, int from) {
691        final int N = states.size();
692
693        if (from == -1) {
694            from = N-1;
695        }
696        if (from >= N) {
697            return -1;
698        }
699        if (owners == null) {
700            return from;
701        }
702
703        while (from >= 0) {
704            UndoState state = states.get(from);
705            if (matchOwners(state, owners)) {
706                return from;
707            }
708            from--;
709        }
710
711        return -1;
712    }
713
714    int findNextState(ArrayList<UndoState> states, UndoOwner[] owners, int from) {
715        final int N = states.size();
716
717        if (from < 0) {
718            from = 0;
719        }
720        if (from >= N) {
721            return -1;
722        }
723        if (owners == null) {
724            return from;
725        }
726
727        while (from < N) {
728            UndoState state = states.get(from);
729            if (matchOwners(state, owners)) {
730                return from;
731            }
732            from++;
733        }
734
735        return -1;
736    }
737
738    final static class UndoState {
739        private final UndoManager mManager;
740        private final int mCommitId;
741        private final ArrayList<UndoOperation<?>> mOperations = new ArrayList<UndoOperation<?>>();
742        private ArrayList<UndoOperation<?>> mRecent;
743        private CharSequence mLabel;
744        private boolean mCanMerge = true;
745        private boolean mExecuted;
746
747        UndoState(UndoManager manager, int commitId) {
748            mManager = manager;
749            mCommitId = commitId;
750        }
751
752        UndoState(UndoManager manager, Parcel p, ClassLoader loader) {
753            mManager = manager;
754            mCommitId = p.readInt();
755            mCanMerge = p.readInt() != 0;
756            mExecuted = p.readInt() != 0;
757            mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
758            final int N = p.readInt();
759            for (int i=0; i<N; i++) {
760                UndoOwner owner = mManager.restoreOwner(p);
761                UndoOperation op = (UndoOperation)p.readParcelable(loader);
762                op.mOwner = owner;
763                mOperations.add(op);
764            }
765        }
766
767        void writeToParcel(Parcel p) {
768            if (mRecent != null) {
769                throw new IllegalStateException("Can't save state before committing");
770            }
771            p.writeInt(mCommitId);
772            p.writeInt(mCanMerge ? 1 : 0);
773            p.writeInt(mExecuted ? 1 : 0);
774            TextUtils.writeToParcel(mLabel, p, 0);
775            final int N = mOperations.size();
776            p.writeInt(N);
777            for (int i=0; i<N; i++) {
778                UndoOperation op = mOperations.get(i);
779                mManager.saveOwner(op.mOwner, p);
780                p.writeParcelable(op, 0);
781            }
782        }
783
784        int getCommitId() {
785            return mCommitId;
786        }
787
788        void setLabel(CharSequence label) {
789            mLabel = label;
790        }
791
792        void updateLabel(CharSequence label) {
793            if (mLabel != null) {
794                mLabel = label;
795            }
796        }
797
798        CharSequence getLabel() {
799            return mLabel;
800        }
801
802        boolean setCanMerge(boolean state) {
803            // Don't allow re-enabling of merging if state has been executed.
804            if (state && mExecuted) {
805                return false;
806            }
807            mCanMerge = state;
808            return true;
809        }
810
811        void makeExecuted() {
812            mExecuted = true;
813        }
814
815        boolean canMerge() {
816            return mCanMerge && !mExecuted;
817        }
818
819        int countOperations() {
820            return mOperations.size();
821        }
822
823        boolean hasOperation(UndoOwner owner) {
824            final int N = mOperations.size();
825            if (owner == null) {
826                return N != 0;
827            }
828            for (int i=0; i<N; i++) {
829                if (mOperations.get(i).getOwner() == owner) {
830                    return true;
831                }
832            }
833            return false;
834        }
835
836        boolean hasMultipleOwners() {
837            final int N = mOperations.size();
838            if (N <= 1) {
839                return false;
840            }
841            UndoOwner owner = mOperations.get(0).getOwner();
842            for (int i=1; i<N; i++) {
843                if (mOperations.get(i).getOwner() != owner) {
844                    return true;
845                }
846            }
847            return false;
848        }
849
850        void addOperation(UndoOperation<?> op) {
851            if (mOperations.contains(op)) {
852                throw new IllegalStateException("Already holds " + op);
853            }
854            mOperations.add(op);
855            if (mRecent == null) {
856                mRecent = new ArrayList<UndoOperation<?>>();
857                mRecent.add(op);
858            }
859            op.mOwner.mOpCount++;
860        }
861
862        <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner) {
863            final int N = mOperations.size();
864            if (clazz == null && owner == null) {
865                return N > 0 ? (T)mOperations.get(N-1) : null;
866            }
867            // First look for the top-most operation with the same owner.
868            for (int i=N-1; i>=0; i--) {
869                UndoOperation<?> op = mOperations.get(i);
870                if (owner != null && op.getOwner() != owner) {
871                    continue;
872                }
873                // Return this operation if it has the same class that the caller wants.
874                // Note that we don't search deeper for the class, because we don't want
875                // to end up with a different order of operations for the same owner.
876                if (clazz != null && op.getClass() != clazz) {
877                    return null;
878                }
879                return (T)op;
880            }
881
882            return null;
883        }
884
885        boolean matchOwner(UndoOwner owner) {
886            for (int i=mOperations.size()-1; i>=0; i--) {
887                if (mOperations.get(i).matchOwner(owner)) {
888                    return true;
889                }
890            }
891            return false;
892        }
893
894        boolean hasData() {
895            for (int i=mOperations.size()-1; i>=0; i--) {
896                if (mOperations.get(i).hasData()) {
897                    return true;
898                }
899            }
900            return false;
901        }
902
903        void commit() {
904            final int N = mRecent != null ? mRecent.size() : 0;
905            for (int i=0; i<N; i++) {
906                mRecent.get(i).commit();
907            }
908            mRecent = null;
909        }
910
911        void undo() {
912            for (int i=mOperations.size()-1; i>=0; i--) {
913                mOperations.get(i).undo();
914            }
915        }
916
917        void redo() {
918            final int N = mOperations.size();
919            for (int i=0; i<N; i++) {
920                mOperations.get(i).redo();
921            }
922        }
923
924        void destroy() {
925            for (int i=mOperations.size()-1; i>=0; i--) {
926                UndoOwner owner = mOperations.get(i).mOwner;
927                owner.mOpCount--;
928                if (owner.mOpCount <= 0) {
929                    if (owner.mOpCount < 0) {
930                        throw new IllegalStateException("Underflow of op count on owner " + owner
931                                + " in op " + mOperations.get(i));
932                    }
933                    mManager.removeOwner(owner);
934                }
935            }
936        }
937    }
938}
939