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