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