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