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