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