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