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