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