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