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 
17 package android.content;
18 
19 import android.os.Parcel;
20 import android.os.Parcelable;
21 import android.os.ParcelableParcel;
22 import android.text.TextUtils;
23 
24 import java.util.ArrayList;
25 import 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  */
56 public 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 
getOwner(String tag, Object data)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 
removeOwner(UndoOwner owner)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      */
saveInstanceState()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 
saveOwner(UndoOwner owner, Parcel out)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      */
restoreInstanceState(Parcelable state)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 
restoreOwner(Parcel in)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      */
setHistorySize(int size)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      */
getHistorySize()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      */
undo(UndoOwner[] owners, int count)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      */
redo(UndoOwner[] owners, int count)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      */
isInUndo()301     public boolean isInUndo() {
302         return mInUndo;
303     }
304 
forgetUndos(UndoOwner[] owners, int count)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 
forgetRedos(UndoOwner[] owners, int count)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      */
countUndos(UndoOwner[] owners)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      */
countRedos(UndoOwner[] owners)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      */
getUndoLabel(UndoOwner[] owners)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      */
getRedoLabel(UndoOwner[] owners)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      */
beginUpdate(CharSequence label)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 
createWorkingState()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      */
isInUpdate()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      */
setUndoLabel(CharSequence label)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      */
suggestUndoLabel(CharSequence label)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      */
getUpdateNestingLevel()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      */
hasOperation(UndoOwner owner)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      */
getLastOperation(int mergeMode)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      */
getLastOperation(UndoOwner owner, int mergeMode)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      */
getLastOperation(Class<T> clazz, UndoOwner owner, int mergeMode)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      */
addOperation(UndoOperation<?> op, int mergeMode)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      */
endUpdate()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 
pushWorkingState()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      */
commitState(UndoOwner owner)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      */
uncommitState(int commitId, UndoOwner owner)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 
getTopUndo(UndoOwner[] owners)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 
getTopRedo(UndoOwner[] owners)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 
matchOwners(UndoState state, UndoOwner[] owners)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 
findPrevState(ArrayList<UndoState> states, UndoOwner[] owners, int from)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 
findNextState(ArrayList<UndoState> states, UndoOwner[] owners, int from)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 
UndoState(UndoManager manager, int commitId)743         UndoState(UndoManager manager, int commitId) {
744             mManager = manager;
745             mCommitId = commitId;
746         }
747 
UndoState(UndoManager manager, Parcel p, ClassLoader loader)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 
writeToParcel(Parcel p)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 
getCommitId()780         int getCommitId() {
781             return mCommitId;
782         }
783 
setLabel(CharSequence label)784         void setLabel(CharSequence label) {
785             mLabel = label;
786         }
787 
updateLabel(CharSequence label)788         void updateLabel(CharSequence label) {
789             if (mLabel != null) {
790                 mLabel = label;
791             }
792         }
793 
getLabel()794         CharSequence getLabel() {
795             return mLabel;
796         }
797 
setCanMerge(boolean state)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 
makeExecuted()807         void makeExecuted() {
808             mExecuted = true;
809         }
810 
canMerge()811         boolean canMerge() {
812             return mCanMerge && !mExecuted;
813         }
814 
countOperations()815         int countOperations() {
816             return mOperations.size();
817         }
818 
hasOperation(UndoOwner owner)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 
hasMultipleOwners()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 
addOperation(UndoOperation<?> op)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 
getLastOperation(Class<T> clazz, UndoOwner owner)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 
matchOwner(UndoOwner owner)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 
hasData()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 
commit()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 
undo()907         void undo() {
908             for (int i=mOperations.size()-1; i>=0; i--) {
909                 mOperations.get(i).undo();
910             }
911         }
912 
redo()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 
destroy()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