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        while (i > 0) {
146            p.writeInt(2);
147            i--;
148            mRedos.get(i).writeToParcel(p);
149        }
150        p.writeInt(0);
151    }
152
153    void saveOwner(UndoOwner owner, Parcel out) {
154        if (owner.mStateSeq == mStateSeq) {
155            out.writeInt(owner.mSavedIdx);
156        } else {
157            owner.mStateSeq = mStateSeq;
158            owner.mSavedIdx = mNextSavedIdx;
159            out.writeInt(owner.mSavedIdx);
160            out.writeString(owner.mTag);
161            out.writeInt(owner.mOpCount);
162            mNextSavedIdx++;
163        }
164    }
165
166    /**
167     * Restore an undo state previously created with {@link #saveInstanceState(Parcel)}.  This
168     * will restore the UndoManager's state to almost exactly what it was at the point it had
169     * been previously saved; the only information not restored is the data object
170     * associated with each {@link UndoOwner}, which requires separate calls to
171     * {@link #getOwner(String, Object)} to re-associate the owner with its data.
172     */
173    public void restoreInstanceState(Parcel p, ClassLoader loader) {
174        if (mUpdateCount > 0) {
175            throw new IllegalStateException("Can't save state while updating");
176        }
177        forgetUndos(null, -1);
178        forgetRedos(null, -1);
179        mHistorySize = p.readInt();
180        mStateOwners = new UndoOwner[p.readInt()];
181
182        int stype;
183        while ((stype=p.readInt()) != 0) {
184            UndoState ustate = new UndoState(this, p, loader);
185            if (stype == 1) {
186                mUndos.add(0, ustate);
187            } else {
188                mRedos.add(0, ustate);
189            }
190        }
191    }
192
193    UndoOwner restoreOwner(Parcel in) {
194        int idx = in.readInt();
195        UndoOwner owner = mStateOwners[idx];
196        if (owner == null) {
197            String tag = in.readString();
198            int opCount = in.readInt();
199            owner = new UndoOwner(tag, this);
200            owner.mOpCount = opCount;
201            mStateOwners[idx] = owner;
202            mOwners.put(tag, owner);
203        }
204        return owner;
205    }
206
207    /**
208     * Set the maximum number of undo states that will be retained.
209     */
210    public void setHistorySize(int size) {
211        mHistorySize = size;
212        if (mHistorySize >= 0 && countUndos(null) > mHistorySize) {
213            forgetUndos(null, countUndos(null) - mHistorySize);
214        }
215    }
216
217    /**
218     * Return the current maximum number of undo states.
219     */
220    public int getHistorySize() {
221        return mHistorySize;
222    }
223
224    /**
225     * Perform undo of last/top <var>count</var> undo states.  The states impacted
226     * by this can be limited through <var>owners</var>.
227     * @param owners Optional set of owners that should be impacted.  If null, all
228     * undo states will be visible and available for undo.  If non-null, only those
229     * states that contain one of the owners specified here will be visible.
230     * @param count Number of undo states to pop.
231     * @return Returns the number of undo states that were actually popped.
232     */
233    public int undo(UndoOwner[] owners, int count) {
234        if (mWorking != null) {
235            throw new IllegalStateException("Can't be called during an update");
236        }
237
238        int num = 0;
239        int i = -1;
240
241        mInUndo = true;
242
243        UndoState us = getTopUndo(null);
244        if (us != null) {
245            us.makeExecuted();
246        }
247
248        while (count > 0 && (i=findPrevState(mUndos, owners, i)) >= 0) {
249            UndoState state = mUndos.remove(i);
250            state.undo();
251            mRedos.add(state);
252            count--;
253            num++;
254        }
255
256        mInUndo = false;
257
258        return num;
259    }
260
261    /**
262     * Perform redo of last/top <var>count</var> undo states in the transient redo stack.
263     * The states impacted by this can be limited through <var>owners</var>.
264     * @param owners Optional set of owners that should be impacted.  If null, all
265     * undo states will be visible and available for undo.  If non-null, only those
266     * states that contain one of the owners specified here will be visible.
267     * @param count Number of undo states to pop.
268     * @return Returns the number of undo states that were actually redone.
269     */
270    public int redo(UndoOwner[] owners, int count) {
271        if (mWorking != null) {
272            throw new IllegalStateException("Can't be called during an update");
273        }
274
275        int num = 0;
276        int i = -1;
277
278        mInUndo = true;
279
280        while (count > 0 && (i=findPrevState(mRedos, owners, i)) >= 0) {
281            UndoState state = mRedos.remove(i);
282            state.redo();
283            mUndos.add(state);
284            count--;
285            num++;
286        }
287
288        mInUndo = false;
289
290        return num;
291    }
292
293    /**
294     * Returns true if we are currently inside of an undo/redo operation.  This is
295     * useful for editors to know whether they should be generating new undo state
296     * when they see edit operations happening.
297     */
298    public boolean isInUndo() {
299        return mInUndo;
300    }
301
302    public int forgetUndos(UndoOwner[] owners, int count) {
303        if (count < 0) {
304            count = mUndos.size();
305        }
306
307        int removed = 0;
308        int i = 0;
309        while (i < mUndos.size() && removed < count) {
310            UndoState state = mUndos.get(i);
311            if (count > 0 && matchOwners(state, owners)) {
312                state.destroy();
313                mUndos.remove(i);
314                removed++;
315            } else {
316                i++;
317            }
318        }
319
320        return removed;
321    }
322
323    public int forgetRedos(UndoOwner[] owners, int count) {
324        if (count < 0) {
325            count = mRedos.size();
326        }
327
328        int removed = 0;
329        int i = 0;
330        while (i < mRedos.size() && removed < count) {
331            UndoState state = mRedos.get(i);
332            if (count > 0 && matchOwners(state, owners)) {
333                state.destroy();
334                mRedos.remove(i);
335                removed++;
336            } else {
337                i++;
338            }
339        }
340
341        return removed;
342    }
343
344    /**
345     * Return the number of undo states on the undo stack.
346     * @param owners If non-null, only those states containing an operation with one of
347     * the owners supplied here will be counted.
348     */
349    public int countUndos(UndoOwner[] owners) {
350        if (owners == null) {
351            return mUndos.size();
352        }
353
354        int count=0;
355        int i=0;
356        while ((i=findNextState(mUndos, owners, i)) >= 0) {
357            count++;
358            i++;
359        }
360        return count;
361    }
362
363    /**
364     * Return the number of redo states on the undo stack.
365     * @param owners If non-null, only those states containing an operation with one of
366     * the owners supplied here will be counted.
367     */
368    public int countRedos(UndoOwner[] owners) {
369        if (owners == null) {
370            return mRedos.size();
371        }
372
373        int count=0;
374        int i=0;
375        while ((i=findNextState(mRedos, owners, i)) >= 0) {
376            count++;
377            i++;
378        }
379        return count;
380    }
381
382    /**
383     * Return the user-visible label for the top undo state on the stack.
384     * @param owners If non-null, will select the top-most undo state containing an
385     * operation with one of the owners supplied here.
386     */
387    public CharSequence getUndoLabel(UndoOwner[] owners) {
388        UndoState state = getTopUndo(owners);
389        return state != null ? state.getLabel() : null;
390    }
391
392    /**
393     * Return the user-visible label for the top redo state on the stack.
394     * @param owners If non-null, will select the top-most undo state containing an
395     * operation with one of the owners supplied here.
396     */
397    public CharSequence getRedoLabel(UndoOwner[] owners) {
398        UndoState state = getTopRedo(owners);
399        return state != null ? state.getLabel() : null;
400    }
401
402    /**
403     * Start creating a new undo state.  Multiple calls to this function will nest until
404     * they are all matched by a later call to {@link #endUpdate}.
405     * @param label Optional user-visible label for this new undo state.
406     */
407    public void beginUpdate(CharSequence label) {
408        if (mInUndo) {
409            throw new IllegalStateException("Can't being update while performing undo/redo");
410        }
411        if (mUpdateCount <= 0) {
412            createWorkingState();
413            mMerged = false;
414            mUpdateCount = 0;
415        }
416
417        mWorking.updateLabel(label);
418        mUpdateCount++;
419    }
420
421    private void createWorkingState() {
422        mWorking = new UndoState(this, mCommitId++);
423        if (mCommitId < 0) {
424            mCommitId = 1;
425        }
426    }
427
428    /**
429     * Returns true if currently inside of a {@link #beginUpdate}.
430     */
431    public boolean isInUpdate() {
432        return mUpdateCount > 0;
433    }
434
435    /**
436     * Forcibly set a new for the new undo state being built within a {@link #beginUpdate}.
437     * Any existing label will be replaced with this one.
438     */
439    public void setUndoLabel(CharSequence label) {
440        if (mWorking == null) {
441            throw new IllegalStateException("Must be called during an update");
442        }
443        mWorking.setLabel(label);
444    }
445
446    /**
447     * Set a new for the new undo state being built within a {@link #beginUpdate}, but
448     * only if there is not a label currently set for it.
449     */
450    public void suggestUndoLabel(CharSequence label) {
451        if (mWorking == null) {
452            throw new IllegalStateException("Must be called during an update");
453        }
454        mWorking.updateLabel(label);
455    }
456
457    /**
458     * Return the number of times {@link #beginUpdate} has been called without a matching
459     * {@link #endUpdate} call.
460     */
461    public int getUpdateNestingLevel() {
462        return mUpdateCount;
463    }
464
465    /**
466     * Check whether there is an {@link UndoOperation} in the current {@link #beginUpdate}
467     * undo state.
468     * @param owner Optional owner of the operation to look for.  If null, will succeed
469     * if there is any operation; if non-null, will only succeed if there is an operation
470     * with the given owner.
471     * @return Returns true if there is a matching operation in the current undo state.
472     */
473    public boolean hasOperation(UndoOwner owner) {
474        if (mWorking == null) {
475            throw new IllegalStateException("Must be called during an update");
476        }
477        return mWorking.hasOperation(owner);
478    }
479
480    /**
481     * Return the most recent {@link UndoOperation} that was added to the update.
482     * @param mergeMode May be either {@link #MERGE_MODE_NONE} or {@link #MERGE_MODE_ANY}.
483     */
484    public UndoOperation<?> getLastOperation(int mergeMode) {
485        return getLastOperation(null, null, mergeMode);
486    }
487
488    /**
489     * Return the most recent {@link UndoOperation} that was added to the update and
490     * has the given owner.
491     * @param owner Optional owner of last operation to retrieve.  If null, the last
492     * operation regardless of owner will be retrieved; if non-null, the last operation
493     * matching the given owner will be retrieved.
494     * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
495     * or {@link #MERGE_MODE_ANY}.
496     */
497    public UndoOperation<?> getLastOperation(UndoOwner owner, int mergeMode) {
498        return getLastOperation(null, owner, mergeMode);
499    }
500
501    /**
502     * Return the most recent {@link UndoOperation} that was added to the update and
503     * has the given owner.
504     * @param clazz Optional class of the last operation to retrieve.  If null, the
505     * last operation regardless of class will be retrieved; if non-null, the last
506     * operation whose class is the same as the given class will be retrieved.
507     * @param owner Optional owner of last operation to retrieve.  If null, the last
508     * operation regardless of owner will be retrieved; if non-null, the last operation
509     * matching the given owner will be retrieved.
510     * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
511     * or {@link #MERGE_MODE_ANY}.
512     */
513    public <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner,
514            int mergeMode) {
515        if (mWorking == null) {
516            throw new IllegalStateException("Must be called during an update");
517        }
518        if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
519            UndoState state = getTopUndo(null);
520            UndoOperation<?> last;
521            if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners())
522                    && state.canMerge() && (last=state.getLastOperation(clazz, owner)) != null) {
523                if (last.allowMerge()) {
524                    mWorking.destroy();
525                    mWorking = state;
526                    mUndos.remove(state);
527                    mMerged = true;
528                    return (T)last;
529                }
530            }
531        }
532
533        return mWorking.getLastOperation(clazz, owner);
534    }
535
536    /**
537     * Add a new UndoOperation to the current update.
538     * @param op The new operation to add.
539     * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
540     * or {@link #MERGE_MODE_ANY}.
541     */
542    public void addOperation(UndoOperation<?> op, int mergeMode) {
543        if (mWorking == null) {
544            throw new IllegalStateException("Must be called during an update");
545        }
546        UndoOwner owner = op.getOwner();
547        if (owner.mManager != this) {
548            throw new IllegalArgumentException(
549                    "Given operation's owner is not in this undo manager.");
550        }
551        if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
552            UndoState state = getTopUndo(null);
553            if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners())
554                    && state.canMerge() && state.hasOperation(op.getOwner())) {
555                mWorking.destroy();
556                mWorking = state;
557                mUndos.remove(state);
558                mMerged = true;
559            }
560        }
561        mWorking.addOperation(op);
562    }
563
564    /**
565     * Finish the creation of an undo state, matching a previous call to
566     * {@link #beginUpdate}.
567     */
568    public void endUpdate() {
569        if (mWorking == null) {
570            throw new IllegalStateException("Must be called during an update");
571        }
572        mUpdateCount--;
573
574        if (mUpdateCount == 0) {
575            pushWorkingState();
576        }
577    }
578
579    private void pushWorkingState() {
580        int N = mUndos.size() + 1;
581
582        if (mWorking.hasData()) {
583            mUndos.add(mWorking);
584            forgetRedos(null, -1);
585            mWorking.commit();
586            if (N >= 2) {
587                // The state before this one can no longer be merged, ever.
588                // The only way to get back to it is for the user to perform
589                // an undo.
590                mUndos.get(N-2).makeExecuted();
591            }
592        } else {
593            mWorking.destroy();
594        }
595        mWorking = null;
596
597        if (mHistorySize >= 0 && N > mHistorySize) {
598            forgetUndos(null, N - mHistorySize);
599        }
600    }
601
602    /**
603     * Commit the last finished undo state.  This undo state can no longer be
604     * modified with further {@link #MERGE_MODE_UNIQUE} or
605     * {@link #MERGE_MODE_ANY} merge modes.  If called while inside of an update,
606     * this will push any changes in the current update on to the undo stack
607     * and result with a fresh undo state, behaving as if {@link #endUpdate()}
608     * had been called enough to unwind the current update, then the last state
609     * committed, and {@link #beginUpdate} called to restore the update nesting.
610     * @param owner The optional owner to determine whether to perform the commit.
611     * If this is non-null, the commit will only execute if the current top undo
612     * state contains an operation with the given owner.
613     * @return Returns an integer identifier for the committed undo state, which
614     * can later be used to try to uncommit the state to perform further edits on it.
615     */
616    public int commitState(UndoOwner owner) {
617        if (mWorking != null && mWorking.hasData()) {
618            if (owner == null || mWorking.hasOperation(owner)) {
619                mWorking.setCanMerge(false);
620                int commitId = mWorking.getCommitId();
621                pushWorkingState();
622                createWorkingState();
623                mMerged = true;
624                return commitId;
625            }
626        } else {
627            UndoState state = getTopUndo(null);
628            if (state != null && (owner == null || state.hasOperation(owner))) {
629                state.setCanMerge(false);
630                return state.getCommitId();
631            }
632        }
633        return -1;
634    }
635
636    /**
637     * Attempt to undo a previous call to {@link #commitState}.  This will work
638     * if the undo state at the top of the stack has the given id, and has not been
639     * involved in an undo operation.  Otherwise false is returned.
640     * @param commitId The identifier for the state to be uncommitted, as returned
641     * by {@link #commitState}.
642     * @param owner Optional owner that must appear in the committed state.
643     * @return Returns true if the uncommit is successful, else false.
644     */
645    public boolean uncommitState(int commitId, UndoOwner owner) {
646        if (mWorking != null && mWorking.getCommitId() == commitId) {
647            if (owner == null || mWorking.hasOperation(owner)) {
648                return mWorking.setCanMerge(true);
649            }
650        } else {
651            UndoState state = getTopUndo(null);
652            if (state != null && (owner == null || state.hasOperation(owner))) {
653                if (state.getCommitId() == commitId) {
654                    return state.setCanMerge(true);
655                }
656            }
657        }
658        return false;
659    }
660
661    UndoState getTopUndo(UndoOwner[] owners) {
662        if (mUndos.size() <= 0) {
663            return null;
664        }
665        int i = findPrevState(mUndos, owners, -1);
666        return i >= 0 ? mUndos.get(i) : null;
667    }
668
669    UndoState getTopRedo(UndoOwner[] owners) {
670        if (mRedos.size() <= 0) {
671            return null;
672        }
673        int i = findPrevState(mRedos, owners, -1);
674        return i >= 0 ? mRedos.get(i) : null;
675    }
676
677    boolean matchOwners(UndoState state, UndoOwner[] owners) {
678        if (owners == null) {
679            return true;
680        }
681        for (int i=0; i<owners.length; i++) {
682            if (state.matchOwner(owners[i])) {
683                return true;
684            }
685        }
686        return false;
687    }
688
689    int findPrevState(ArrayList<UndoState> states, UndoOwner[] owners, int from) {
690        final int N = states.size();
691
692        if (from == -1) {
693            from = N-1;
694        }
695        if (from >= N) {
696            return -1;
697        }
698        if (owners == null) {
699            return from;
700        }
701
702        while (from >= 0) {
703            UndoState state = states.get(from);
704            if (matchOwners(state, owners)) {
705                return from;
706            }
707            from--;
708        }
709
710        return -1;
711    }
712
713    int findNextState(ArrayList<UndoState> states, UndoOwner[] owners, int from) {
714        final int N = states.size();
715
716        if (from < 0) {
717            from = 0;
718        }
719        if (from >= N) {
720            return -1;
721        }
722        if (owners == null) {
723            return from;
724        }
725
726        while (from < N) {
727            UndoState state = states.get(from);
728            if (matchOwners(state, owners)) {
729                return from;
730            }
731            from++;
732        }
733
734        return -1;
735    }
736
737    final static class UndoState {
738        private final UndoManager mManager;
739        private final int mCommitId;
740        private final ArrayList<UndoOperation<?>> mOperations = new ArrayList<UndoOperation<?>>();
741        private ArrayList<UndoOperation<?>> mRecent;
742        private CharSequence mLabel;
743        private boolean mCanMerge = true;
744        private boolean mExecuted;
745
746        UndoState(UndoManager manager, int commitId) {
747            mManager = manager;
748            mCommitId = commitId;
749        }
750
751        UndoState(UndoManager manager, Parcel p, ClassLoader loader) {
752            mManager = manager;
753            mCommitId = p.readInt();
754            mCanMerge = p.readInt() != 0;
755            mExecuted = p.readInt() != 0;
756            mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
757            final int N = p.readInt();
758            for (int i=0; i<N; i++) {
759                UndoOwner owner = mManager.restoreOwner(p);
760                UndoOperation op = (UndoOperation)p.readParcelable(loader);
761                op.mOwner = owner;
762                mOperations.add(op);
763            }
764        }
765
766        void writeToParcel(Parcel p) {
767            if (mRecent != null) {
768                throw new IllegalStateException("Can't save state before committing");
769            }
770            p.writeInt(mCommitId);
771            p.writeInt(mCanMerge ? 1 : 0);
772            p.writeInt(mExecuted ? 1 : 0);
773            TextUtils.writeToParcel(mLabel, p, 0);
774            final int N = mOperations.size();
775            p.writeInt(N);
776            for (int i=0; i<N; i++) {
777                UndoOperation op = mOperations.get(i);
778                mManager.saveOwner(op.mOwner, p);
779                p.writeParcelable(op, 0);
780            }
781        }
782
783        int getCommitId() {
784            return mCommitId;
785        }
786
787        void setLabel(CharSequence label) {
788            mLabel = label;
789        }
790
791        void updateLabel(CharSequence label) {
792            if (mLabel != null) {
793                mLabel = label;
794            }
795        }
796
797        CharSequence getLabel() {
798            return mLabel;
799        }
800
801        boolean setCanMerge(boolean state) {
802            // Don't allow re-enabling of merging if state has been executed.
803            if (state && mExecuted) {
804                return false;
805            }
806            mCanMerge = state;
807            return true;
808        }
809
810        void makeExecuted() {
811            mExecuted = true;
812        }
813
814        boolean canMerge() {
815            return mCanMerge && !mExecuted;
816        }
817
818        int countOperations() {
819            return mOperations.size();
820        }
821
822        boolean hasOperation(UndoOwner owner) {
823            final int N = mOperations.size();
824            if (owner == null) {
825                return N != 0;
826            }
827            for (int i=0; i<N; i++) {
828                if (mOperations.get(i).getOwner() == owner) {
829                    return true;
830                }
831            }
832            return false;
833        }
834
835        boolean hasMultipleOwners() {
836            final int N = mOperations.size();
837            if (N <= 1) {
838                return false;
839            }
840            UndoOwner owner = mOperations.get(0).getOwner();
841            for (int i=1; i<N; i++) {
842                if (mOperations.get(i).getOwner() != owner) {
843                    return true;
844                }
845            }
846            return false;
847        }
848
849        void addOperation(UndoOperation<?> op) {
850            if (mOperations.contains(op)) {
851                throw new IllegalStateException("Already holds " + op);
852            }
853            mOperations.add(op);
854            if (mRecent == null) {
855                mRecent = new ArrayList<UndoOperation<?>>();
856                mRecent.add(op);
857            }
858            op.mOwner.mOpCount++;
859        }
860
861        <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner) {
862            final int N = mOperations.size();
863            if (clazz == null && owner == null) {
864                return N > 0 ? (T)mOperations.get(N-1) : null;
865            }
866            // First look for the top-most operation with the same owner.
867            for (int i=N-1; i>=0; i--) {
868                UndoOperation<?> op = mOperations.get(i);
869                if (owner != null && op.getOwner() != owner) {
870                    continue;
871                }
872                // Return this operation if it has the same class that the caller wants.
873                // Note that we don't search deeper for the class, because we don't want
874                // to end up with a different order of operations for the same owner.
875                if (clazz != null && op.getClass() != clazz) {
876                    return null;
877                }
878                return (T)op;
879            }
880
881            return null;
882        }
883
884        boolean matchOwner(UndoOwner owner) {
885            for (int i=mOperations.size()-1; i>=0; i--) {
886                if (mOperations.get(i).matchOwner(owner)) {
887                    return true;
888                }
889            }
890            return false;
891        }
892
893        boolean hasData() {
894            for (int i=mOperations.size()-1; i>=0; i--) {
895                if (mOperations.get(i).hasData()) {
896                    return true;
897                }
898            }
899            return false;
900        }
901
902        void commit() {
903            final int N = mRecent != null ? mRecent.size() : 0;
904            for (int i=0; i<N; i++) {
905                mRecent.get(i).commit();
906            }
907            mRecent = null;
908        }
909
910        void undo() {
911            for (int i=mOperations.size()-1; i>=0; i--) {
912                mOperations.get(i).undo();
913            }
914        }
915
916        void redo() {
917            final int N = mOperations.size();
918            for (int i=0; i<N; i++) {
919                mOperations.get(i).redo();
920            }
921        }
922
923        void destroy() {
924            for (int i=mOperations.size()-1; i>=0; i--) {
925                UndoOwner owner = mOperations.get(i).mOwner;
926                owner.mOpCount--;
927                if (owner.mOpCount <= 0) {
928                    if (owner.mOpCount < 0) {
929                        throw new IllegalStateException("Underflow of op count on owner " + owner
930                                + " in op " + mOperations.get(i));
931                    }
932                    mManager.removeOwner(owner);
933                }
934            }
935        }
936    }
937}
938