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