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