1 /*
2  * Copyright 2018 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 androidx.recyclerview.widget;
18 
19 import android.annotation.SuppressLint;
20 
21 import org.jspecify.annotations.NonNull;
22 import org.jspecify.annotations.Nullable;
23 
24 import java.lang.reflect.Array;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Comparator;
28 
29 /**
30  * A Sorted list implementation that can keep items in order and also notify for changes in the
31  * list
32  * such that it can be bound to a {@link RecyclerView.Adapter
33  * RecyclerView.Adapter}.
34  * <p>
35  * It keeps items ordered using the {@link Callback#compare(Object, Object)} method and uses
36  * binary search to retrieve items. If the sorting criteria of your items may change, make sure you
37  * call appropriate methods while editing them to avoid data inconsistencies.
38  * <p>
39  * You can control the order of items and change notifications via the {@link Callback} parameter.
40  */
41 @SuppressWarnings("unchecked")
42 public class SortedList<T> {
43 
44     /**
45      * Used by {@link #indexOf(Object)} when the item cannot be found in the list.
46      */
47     public static final int INVALID_POSITION = -1;
48 
49     private static final int MIN_CAPACITY = 10;
50     private static final int CAPACITY_GROWTH = MIN_CAPACITY;
51     private static final int INSERTION = 1;
52     private static final int DELETION = 1 << 1;
53     private static final int LOOKUP = 1 << 2;
54     T[] mData;
55 
56     /**
57      * A reference to the previous set of data that is kept during a mutation operation (addAll or
58      * replaceAll).
59      */
60     private T[] mOldData;
61 
62     /**
63      * The current index into mOldData that has not yet been processed during a mutation operation
64      * (addAll or replaceAll).
65      */
66     private int mOldDataStart;
67     private int mOldDataSize;
68 
69     /**
70      * The current index into the new data that has not yet been processed during a mutation
71      * operation (addAll or replaceAll).
72      */
73     private int mNewDataStart;
74 
75     /**
76      * The callback instance that controls the behavior of the SortedList and get notified when
77      * changes happen.
78      */
79     private Callback mCallback;
80 
81     private BatchedCallback mBatchedCallback;
82 
83     private int mSize;
84     private final Class<T> mTClass;
85 
86     /**
87      * Creates a new SortedList of type T.
88      *
89      * @param klass    The class of the contents of the SortedList.
90      * @param callback The callback that controls the behavior of SortedList.
91      */
SortedList(@onNull Class<T> klass, @NonNull Callback<T> callback)92     public SortedList(@NonNull Class<T> klass, @NonNull Callback<T> callback) {
93         this(klass, callback, MIN_CAPACITY);
94     }
95 
96     /**
97      * Creates a new SortedList of type T.
98      *
99      * @param klass           The class of the contents of the SortedList.
100      * @param callback        The callback that controls the behavior of SortedList.
101      * @param initialCapacity The initial capacity to hold items.
102      */
SortedList(@onNull Class<T> klass, @NonNull Callback<T> callback, int initialCapacity)103     public SortedList(@NonNull Class<T> klass, @NonNull Callback<T> callback, int initialCapacity) {
104         mTClass = klass;
105         mData = (T[]) Array.newInstance(klass, initialCapacity);
106         mCallback = callback;
107         mSize = 0;
108     }
109 
110     /**
111      * The number of items in the list.
112      *
113      * @return The number of items in the list.
114      */
size()115     public int size() {
116         return mSize;
117     }
118 
119     /**
120      * Adds the given item to the list. If this is a new item, SortedList calls
121      * {@link Callback#onInserted(int, int)}.
122      * <p>
123      * If the item already exists in the list and its sorting criteria is not changed, it is
124      * replaced with the existing Item. SortedList uses
125      * {@link Callback#areItemsTheSame(Object, Object)} to check if two items are the same item
126      * and uses {@link Callback#areContentsTheSame(Object, Object)} to decide whether it should
127      * call {@link Callback#onChanged(int, int)} or not. In both cases, it always removes the
128      * reference to the old item and puts the new item into the backing array even if
129      * {@link Callback#areContentsTheSame(Object, Object)} returns false.
130      * <p>
131      * If the sorting criteria of the item is changed, SortedList won't be able to find
132      * its duplicate in the list which will result in having a duplicate of the Item in the list.
133      * If you need to update sorting criteria of an item that already exists in the list,
134      * use {@link #updateItemAt(int, Object)}. You can find the index of the item using
135      * {@link #indexOf(Object)} before you update the object.
136      *
137      * @param item The item to be added into the list.
138      *
139      * @return The index of the newly added item.
140      * @see Callback#compare(Object, Object)
141      * @see Callback#areItemsTheSame(Object, Object)
142      * @see Callback#areContentsTheSame(Object, Object)}
143      */
add(T item)144     public int add(T item) {
145         throwIfInMutationOperation();
146         return add(item, true);
147     }
148 
149     /**
150      * Adds the given items to the list. Equivalent to calling {@link SortedList#add} in a loop,
151      * except the callback events may be in a different order/granularity since addAll can batch
152      * them for better performance.
153      * <p>
154      * If allowed, will reference the input array during, and possibly after, the operation to avoid
155      * extra memory allocation, in which case you should not continue to reference or modify the
156      * array yourself.
157      * <p>
158      * @param items Array of items to be added into the list.
159      * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the
160      *                       input array.
161      * @see SortedList#addAll(T[] items)
162      */
addAll(T @onNull [] items, boolean mayModifyInput)163     public void addAll(T @NonNull [] items, boolean mayModifyInput) {
164         throwIfInMutationOperation();
165         if (items.length == 0) {
166             return;
167         }
168 
169         if (mayModifyInput) {
170             addAllInternal(items);
171         } else {
172             addAllInternal(copyArray(items));
173         }
174     }
175 
176     /**
177      * Adds the given items to the list. Does not modify or retain the input.
178      *
179      * @see SortedList#addAll(T[] items, boolean mayModifyInput)
180      *
181      * @param items Array of items to be added into the list.
182      */
addAll(T @onNull .... items)183     public void addAll(T @NonNull ... items) {
184         addAll(items, false);
185     }
186 
187     /**
188      * Adds the given items to the list. Does not modify or retain the input.
189      *
190      * @see SortedList#addAll(T[] items, boolean mayModifyInput)
191      *
192      * @param items Collection of items to be added into the list.
193      */
addAll(@onNull Collection<T> items)194     public void addAll(@NonNull Collection<T> items) {
195         T[] copy = (T[]) Array.newInstance(mTClass, items.size());
196         addAll(items.toArray(copy), true);
197     }
198 
199     /**
200      * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events
201      * for each change detected as appropriate.
202      * <p>
203      * If allowed, will reference the input array during, and possibly after, the operation to avoid
204      * extra memory allocation, in which case you should not continue to reference or modify the
205      * array yourself.
206      * <p>
207      * Note: this method does not detect moves or dispatch
208      * {@link ListUpdateCallback#onMoved(int, int)} events. It instead treats moves as a remove
209      * followed by an add and therefore dispatches {@link ListUpdateCallback#onRemoved(int, int)}
210      * and {@link ListUpdateCallback#onRemoved(int, int)} events.  See {@link DiffUtil} if you want
211      * your implementation to dispatch move events.
212      * <p>
213      * @param items Array of items to replace current items.
214      * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the
215      *                       input array.
216      * @see #replaceAll(T[])
217      */
replaceAll(T @onNull [] items, boolean mayModifyInput)218     public void replaceAll(T @NonNull [] items, boolean mayModifyInput) {
219         throwIfInMutationOperation();
220 
221         if (mayModifyInput) {
222             replaceAllInternal(items);
223         } else {
224             replaceAllInternal(copyArray(items));
225         }
226     }
227 
228     /**
229      * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events
230      * for each change detected as appropriate.  Does not modify or retain the input.
231      *
232      * @see #replaceAll(T[], boolean)
233      *
234      * @param items Array of items to replace current items.
235      */
replaceAll(T @onNull .... items)236     public void replaceAll(T @NonNull ... items) {
237         replaceAll(items, false);
238     }
239 
240     /**
241      * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events
242      * for each change detected as appropriate. Does not modify or retain the input.
243      *
244      * @see #replaceAll(T[], boolean)
245      *
246      * @param items Array of items to replace current items.
247      */
replaceAll(@onNull Collection<T> items)248     public void replaceAll(@NonNull Collection<T> items) {
249         T[] copy = (T[]) Array.newInstance(mTClass, items.size());
250         replaceAll(items.toArray(copy), true);
251     }
252 
addAllInternal(T[] newItems)253     private void addAllInternal(T[] newItems) {
254         if (newItems.length < 1) {
255             return;
256         }
257 
258         final int newSize = sortAndDedup(newItems);
259 
260         if (mSize == 0) {
261             mData = newItems;
262             mSize = newSize;
263             mCallback.onInserted(0, newSize);
264         } else {
265             merge(newItems, newSize);
266         }
267     }
268 
replaceAllInternal(T @onNull [] newData)269     private void replaceAllInternal(T @NonNull [] newData) {
270         final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback);
271         if (forceBatchedUpdates) {
272             beginBatchedUpdates();
273         }
274 
275         mOldDataStart = 0;
276         mOldDataSize = mSize;
277         mOldData = mData;
278 
279         mNewDataStart = 0;
280         int newSize = sortAndDedup(newData);
281         mData = (T[]) Array.newInstance(mTClass, newSize);
282 
283         while (mNewDataStart < newSize || mOldDataStart < mOldDataSize) {
284             if (mOldDataStart >= mOldDataSize) {
285                 int insertIndex = mNewDataStart;
286                 int itemCount = newSize - mNewDataStart;
287                 System.arraycopy(newData, insertIndex, mData, insertIndex, itemCount);
288                 mNewDataStart += itemCount;
289                 mSize += itemCount;
290                 mCallback.onInserted(insertIndex, itemCount);
291                 break;
292             }
293             if (mNewDataStart >= newSize) {
294                 int itemCount = mOldDataSize - mOldDataStart;
295                 mSize -= itemCount;
296                 mCallback.onRemoved(mNewDataStart, itemCount);
297                 break;
298             }
299 
300             T oldItem = mOldData[mOldDataStart];
301             T newItem = newData[mNewDataStart];
302 
303             int result = mCallback.compare(oldItem, newItem);
304             if (result < 0) {
305                 replaceAllRemove();
306             } else if (result > 0) {
307                 replaceAllInsert(newItem);
308             } else {
309                 if (!mCallback.areItemsTheSame(oldItem, newItem)) {
310                     // The items aren't the same even though they were supposed to occupy the same
311                     // place, so both notify to remove and add an item in the current location.
312                     replaceAllRemove();
313                     replaceAllInsert(newItem);
314                 } else {
315                     mData[mNewDataStart] = newItem;
316                     mOldDataStart++;
317                     mNewDataStart++;
318                     if (!mCallback.areContentsTheSame(oldItem, newItem)) {
319                         // The item is the same but the contents have changed, so notify that an
320                         // onChanged event has occurred.
321                         mCallback.onChanged(mNewDataStart - 1, 1,
322                                 mCallback.getChangePayload(oldItem, newItem));
323                     }
324                 }
325             }
326         }
327 
328         mOldData = null;
329 
330         if (forceBatchedUpdates) {
331             endBatchedUpdates();
332         }
333     }
334 
replaceAllInsert(T newItem)335     private void replaceAllInsert(T newItem) {
336         mData[mNewDataStart] = newItem;
337         mNewDataStart++;
338         mSize++;
339         mCallback.onInserted(mNewDataStart - 1, 1);
340     }
341 
replaceAllRemove()342     private void replaceAllRemove() {
343         mSize--;
344         mOldDataStart++;
345         mCallback.onRemoved(mNewDataStart, 1);
346     }
347 
348     /**
349      * Sorts and removes duplicate items, leaving only the last item from each group of "same"
350      * items. Move the remaining items to the beginning of the array.
351      *
352      * @return Number of deduplicated items at the beginning of the array.
353      */
sortAndDedup(T @onNull [] items)354     private int sortAndDedup(T @NonNull [] items) {
355         if (items.length == 0) {
356             return 0;
357         }
358 
359         // Arrays.sort is stable.
360         Arrays.sort(items, mCallback);
361 
362         // Keep track of the range of equal items at the end of the output.
363         // Start with the range containing just the first item.
364         int rangeStart = 0;
365         int rangeEnd = 1;
366 
367         for (int i = 1; i < items.length; ++i) {
368             T currentItem = items[i];
369 
370             int compare = mCallback.compare(items[rangeStart], currentItem);
371 
372             if (compare == 0) {
373                 // The range of equal items continues, update it.
374                 final int sameItemPos = findSameItem(currentItem, items, rangeStart, rangeEnd);
375                 if (sameItemPos != INVALID_POSITION) {
376                     // Replace the duplicate item.
377                     items[sameItemPos] = currentItem;
378                 } else {
379                     // Expand the range.
380                     if (rangeEnd != i) {  // Avoid redundant copy.
381                         items[rangeEnd] = currentItem;
382                     }
383                     rangeEnd++;
384                 }
385             } else {
386                 // The range has ended. Reset it to contain just the current item.
387                 if (rangeEnd != i) {  // Avoid redundant copy.
388                     items[rangeEnd] = currentItem;
389                 }
390                 rangeStart = rangeEnd++;
391             }
392         }
393         return rangeEnd;
394     }
395 
396 
findSameItem(T item, T[] items, int from, int to)397     private int findSameItem(T item, T[] items, int from, int to) {
398         for (int pos = from; pos < to; pos++) {
399             if (mCallback.areItemsTheSame(items[pos], item)) {
400                 return pos;
401             }
402         }
403         return INVALID_POSITION;
404     }
405 
406     /**
407      * This method assumes that newItems are sorted and deduplicated.
408      */
merge(T[] newData, int newDataSize)409     private void merge(T[] newData, int newDataSize) {
410         final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback);
411         if (forceBatchedUpdates) {
412             beginBatchedUpdates();
413         }
414 
415         mOldData = mData;
416         mOldDataStart = 0;
417         mOldDataSize = mSize;
418 
419         final int mergedCapacity = mSize + newDataSize + CAPACITY_GROWTH;
420         mData = (T[]) Array.newInstance(mTClass, mergedCapacity);
421         mNewDataStart = 0;
422 
423         int newDataStart = 0;
424         while (mOldDataStart < mOldDataSize || newDataStart < newDataSize) {
425             if (mOldDataStart == mOldDataSize) {
426                 // No more old items, copy the remaining new items.
427                 int itemCount = newDataSize - newDataStart;
428                 System.arraycopy(newData, newDataStart, mData, mNewDataStart, itemCount);
429                 mNewDataStart += itemCount;
430                 mSize += itemCount;
431                 mCallback.onInserted(mNewDataStart - itemCount, itemCount);
432                 break;
433             }
434 
435             if (newDataStart == newDataSize) {
436                 // No more new items, copy the remaining old items.
437                 int itemCount = mOldDataSize - mOldDataStart;
438                 System.arraycopy(mOldData, mOldDataStart, mData, mNewDataStart, itemCount);
439                 mNewDataStart += itemCount;
440                 break;
441             }
442 
443             T oldItem = mOldData[mOldDataStart];
444             T newItem = newData[newDataStart];
445             int compare = mCallback.compare(oldItem, newItem);
446             if (compare > 0) {
447                 // New item is lower, output it.
448                 mData[mNewDataStart++] = newItem;
449                 mSize++;
450                 newDataStart++;
451                 mCallback.onInserted(mNewDataStart - 1, 1);
452             } else if (compare == 0 && mCallback.areItemsTheSame(oldItem, newItem)) {
453                 // Items are the same. Output the new item, but consume both.
454                 mData[mNewDataStart++] = newItem;
455                 newDataStart++;
456                 mOldDataStart++;
457                 if (!mCallback.areContentsTheSame(oldItem, newItem)) {
458                     mCallback.onChanged(mNewDataStart - 1, 1,
459                             mCallback.getChangePayload(oldItem, newItem));
460                 }
461             } else {
462                 // Old item is lower than or equal to (but not the same as the new). Output it.
463                 // New item with the same sort order will be inserted later.
464                 mData[mNewDataStart++] = oldItem;
465                 mOldDataStart++;
466             }
467         }
468 
469         mOldData = null;
470 
471         if (forceBatchedUpdates) {
472             endBatchedUpdates();
473         }
474     }
475 
476     /**
477      * Throws an exception if called while we are in the middle of a mutation operation (addAll or
478      * replaceAll).
479      */
throwIfInMutationOperation()480     private void throwIfInMutationOperation() {
481         if (mOldData != null) {
482             throw new IllegalStateException("Data cannot be mutated in the middle of a batch "
483                     + "update operation such as addAll or replaceAll.");
484         }
485     }
486 
487     /**
488      * Batches adapter updates that happen after calling this method and before calling
489      * {@link #endBatchedUpdates()}. For example, if you add multiple items in a loop
490      * and they are placed into consecutive indices, SortedList calls
491      * {@link Callback#onInserted(int, int)} only once with the proper item count. If an event
492      * cannot be merged with the previous event, the previous event is dispatched
493      * to the callback instantly.
494      * <p>
495      * After running your data updates, you <b>must</b> call {@link #endBatchedUpdates()}
496      * which will dispatch any deferred data change event to the current callback.
497      * <p>
498      * A sample implementation may look like this:
499      * <pre>
500      *     mSortedList.beginBatchedUpdates();
501      *     try {
502      *         mSortedList.add(item1)
503      *         mSortedList.add(item2)
504      *         mSortedList.remove(item3)
505      *         ...
506      *     } finally {
507      *         mSortedList.endBatchedUpdates();
508      *     }
509      * </pre>
510      * <p>
511      * Instead of using this method to batch calls, you can use a Callback that extends
512      * {@link BatchedCallback}. In that case, you must make sure that you are manually calling
513      * {@link BatchedCallback#dispatchLastEvent()} right after you complete your data changes.
514      * Failing to do so may create data inconsistencies with the Callback.
515      * <p>
516      * If the current Callback is an instance of {@link BatchedCallback}, calling this method
517      * has no effect.
518      */
beginBatchedUpdates()519     public void beginBatchedUpdates() {
520         throwIfInMutationOperation();
521         if (mCallback instanceof BatchedCallback) {
522             return;
523         }
524         if (mBatchedCallback == null) {
525             mBatchedCallback = new BatchedCallback(mCallback);
526         }
527         mCallback = mBatchedCallback;
528     }
529 
530     /**
531      * Ends the update transaction and dispatches any remaining event to the callback.
532      */
endBatchedUpdates()533     public void endBatchedUpdates() {
534         throwIfInMutationOperation();
535         if (mCallback instanceof BatchedCallback) {
536             ((BatchedCallback) mCallback).dispatchLastEvent();
537         }
538         if (mCallback == mBatchedCallback) {
539             mCallback = mBatchedCallback.mWrappedCallback;
540         }
541     }
542 
add(T item, boolean notify)543     private int add(T item, boolean notify) {
544         int index = findIndexOf(item, mData, 0, mSize, INSERTION);
545         if (index == INVALID_POSITION) {
546             index = 0;
547         } else if (index < mSize) {
548             T existing = mData[index];
549             if (mCallback.areItemsTheSame(existing, item)) {
550                 if (mCallback.areContentsTheSame(existing, item)) {
551                     //no change but still replace the item
552                     mData[index] = item;
553                     return index;
554                 } else {
555                     mData[index] = item;
556                     mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item));
557                     return index;
558                 }
559             }
560         }
561         addToData(index, item);
562         if (notify) {
563             mCallback.onInserted(index, 1);
564         }
565         return index;
566     }
567 
568     /**
569      * Removes the provided item from the list and calls {@link Callback#onRemoved(int, int)}.
570      *
571      * @param item The item to be removed from the list.
572      *
573      * @return True if item is removed, false if item cannot be found in the list.
574      */
remove(T item)575     public boolean remove(T item) {
576         throwIfInMutationOperation();
577         return remove(item, true);
578     }
579 
580     /**
581      * Removes the item at the given index and calls {@link Callback#onRemoved(int, int)}.
582      *
583      * @param index The index of the item to be removed.
584      *
585      * @return The removed item.
586      */
removeItemAt(int index)587     public T removeItemAt(int index) {
588         throwIfInMutationOperation();
589         T item = get(index);
590         removeItemAtIndex(index, true);
591         return item;
592     }
593 
remove(T item, boolean notify)594     private boolean remove(T item, boolean notify) {
595         int index = findIndexOf(item, mData, 0, mSize, DELETION);
596         if (index == INVALID_POSITION) {
597             return false;
598         }
599         removeItemAtIndex(index, notify);
600         return true;
601     }
602 
removeItemAtIndex(int index, boolean notify)603     private void removeItemAtIndex(int index, boolean notify) {
604         System.arraycopy(mData, index + 1, mData, index, mSize - index - 1);
605         mSize--;
606         mData[mSize] = null;
607         if (notify) {
608             mCallback.onRemoved(index, 1);
609         }
610     }
611 
612     /**
613      * Updates the item at the given index and calls {@link Callback#onChanged(int, int)} and/or
614      * {@link Callback#onMoved(int, int)} if necessary.
615      * <p>
616      * You can use this method if you need to change an existing Item such that its position in the
617      * list may change.
618      * <p>
619      * If the new object is a different object (<code>get(index) != item</code>) and
620      * {@link Callback#areContentsTheSame(Object, Object)} returns <code>true</code>, SortedList
621      * avoids calling {@link Callback#onChanged(int, int)} otherwise it calls
622      * {@link Callback#onChanged(int, int)}.
623      * <p>
624      * If the new position of the item is different than the provided <code>index</code>,
625      * SortedList
626      * calls {@link Callback#onMoved(int, int)}.
627      *
628      * @param index The index of the item to replace
629      * @param item  The item to replace the item at the given Index.
630      * @see #add(Object)
631      */
updateItemAt(int index, T item)632     public void updateItemAt(int index, T item) {
633         throwIfInMutationOperation();
634         final T existing = get(index);
635         // assume changed if the same object is given back
636         boolean contentsChanged = existing == item || !mCallback.areContentsTheSame(existing, item);
637         if (existing != item) {
638             // different items, we can use comparison and may avoid lookup
639             final int cmp = mCallback.compare(existing, item);
640             if (cmp == 0) {
641                 mData[index] = item;
642                 if (contentsChanged) {
643                     mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item));
644                 }
645                 return;
646             }
647         }
648         if (contentsChanged) {
649             mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item));
650         }
651         // TODO this done in 1 pass to avoid shifting twice.
652         removeItemAtIndex(index, false);
653         int newIndex = add(item, false);
654         if (index != newIndex) {
655             mCallback.onMoved(index, newIndex);
656         }
657     }
658 
659     /**
660      * This method can be used to recalculate the position of the item at the given index, without
661      * triggering an {@link Callback#onChanged(int, int)} callback.
662      * <p>
663      * If you are editing objects in the list such that their position in the list may change but
664      * you don't want to trigger an onChange animation, you can use this method to re-position it.
665      * If the item changes position, SortedList will call {@link Callback#onMoved(int, int)}
666      * without
667      * calling {@link Callback#onChanged(int, int)}.
668      * <p>
669      * A sample usage may look like:
670      *
671      * <pre>
672      *     final int position = mSortedList.indexOf(item);
673      *     item.incrementPriority(); // assume items are sorted by priority
674      *     mSortedList.recalculatePositionOfItemAt(position);
675      * </pre>
676      * In the example above, because the sorting criteria of the item has been changed,
677      * mSortedList.indexOf(item) will not be able to find the item. This is why the code above
678      * first
679      * gets the position before editing the item, edits it and informs the SortedList that item
680      * should be repositioned.
681      *
682      * @param index The current index of the Item whose position should be re-calculated.
683      * @see #updateItemAt(int, Object)
684      * @see #add(Object)
685      */
recalculatePositionOfItemAt(int index)686     public void recalculatePositionOfItemAt(int index) {
687         throwIfInMutationOperation();
688         // TODO can be improved
689         final T item = get(index);
690         removeItemAtIndex(index, false);
691         int newIndex = add(item, false);
692         if (index != newIndex) {
693             mCallback.onMoved(index, newIndex);
694         }
695     }
696 
697     /**
698      * Returns the item at the given index.
699      *
700      * @param index The index of the item to retrieve.
701      *
702      * @return The item at the given index.
703      * @throws java.lang.IndexOutOfBoundsException if provided index is negative or larger than the
704      *                                             size of the list.
705      */
get(int index)706     public T get(int index) throws IndexOutOfBoundsException {
707         if (index >= mSize || index < 0) {
708             throw new IndexOutOfBoundsException("Asked to get item at " + index + " but size is "
709                     + mSize);
710         }
711         if (mOldData != null) {
712             // The call is made from a callback during addAll execution. The data is split
713             // between mData and mOldData.
714             if (index >= mNewDataStart) {
715                 return mOldData[index - mNewDataStart + mOldDataStart];
716             }
717         }
718         return mData[index];
719     }
720 
721     /**
722      * Returns the position of the provided item.
723      *
724      * @param item The item to query for position.
725      *
726      * @return The position of the provided item or {@link #INVALID_POSITION} if item is not in the
727      * list.
728      */
indexOf(T item)729     public int indexOf(T item) {
730         if (mOldData != null) {
731             int index = findIndexOf(item, mData, 0, mNewDataStart, LOOKUP);
732             if (index != INVALID_POSITION) {
733                 return index;
734             }
735             index = findIndexOf(item, mOldData, mOldDataStart, mOldDataSize, LOOKUP);
736             if (index != INVALID_POSITION) {
737                 return index - mOldDataStart + mNewDataStart;
738             }
739             return INVALID_POSITION;
740         }
741         return findIndexOf(item, mData, 0, mSize, LOOKUP);
742     }
743 
findIndexOf(T item, T[] mData, int left, int right, int reason)744     private int findIndexOf(T item, T[] mData, int left, int right, int reason) {
745         while (left < right) {
746             final int middle = (left + right) / 2;
747             T myItem = mData[middle];
748             final int cmp = mCallback.compare(myItem, item);
749             if (cmp < 0) {
750                 left = middle + 1;
751             } else if (cmp == 0) {
752                 if (mCallback.areItemsTheSame(myItem, item)) {
753                     return middle;
754                 } else {
755                     int exact = linearEqualitySearch(item, middle, left, right);
756                     if (reason == INSERTION) {
757                         return exact == INVALID_POSITION ? middle : exact;
758                     } else {
759                         return exact;
760                     }
761                 }
762             } else {
763                 right = middle;
764             }
765         }
766         return reason == INSERTION ? left : INVALID_POSITION;
767     }
768 
linearEqualitySearch(T item, int middle, int left, int right)769     private int linearEqualitySearch(T item, int middle, int left, int right) {
770         // go left
771         for (int next = middle - 1; next >= left; next--) {
772             T nextItem = mData[next];
773             int cmp = mCallback.compare(nextItem, item);
774             if (cmp != 0) {
775                 break;
776             }
777             if (mCallback.areItemsTheSame(nextItem, item)) {
778                 return next;
779             }
780         }
781         for (int next = middle + 1; next < right; next++) {
782             T nextItem = mData[next];
783             int cmp = mCallback.compare(nextItem, item);
784             if (cmp != 0) {
785                 break;
786             }
787             if (mCallback.areItemsTheSame(nextItem, item)) {
788                 return next;
789             }
790         }
791         return INVALID_POSITION;
792     }
793 
addToData(int index, T item)794     private void addToData(int index, T item) {
795         if (index > mSize) {
796             throw new IndexOutOfBoundsException(
797                     "cannot add item to " + index + " because size is " + mSize);
798         }
799         if (mSize == mData.length) {
800             // we are at the limit enlarge
801             T[] newData = (T[]) Array.newInstance(mTClass, mData.length + CAPACITY_GROWTH);
802             System.arraycopy(mData, 0, newData, 0, index);
803             newData[index] = item;
804             System.arraycopy(mData, index, newData, index + 1, mSize - index);
805             mData = newData;
806         } else {
807             // just shift, we fit
808             System.arraycopy(mData, index, mData, index + 1, mSize - index);
809             mData[index] = item;
810         }
811         mSize++;
812     }
813 
copyArray(T[] items)814     private T[] copyArray(T[] items) {
815         T[] copy = (T[]) Array.newInstance(mTClass, items.length);
816         System.arraycopy(items, 0, copy, 0, items.length);
817         return copy;
818     }
819 
820     /**
821      * Removes all items from the SortedList.
822      */
clear()823     public void clear() {
824         throwIfInMutationOperation();
825         if (mSize == 0) {
826             return;
827         }
828         final int prevSize = mSize;
829         Arrays.fill(mData, 0, prevSize, null);
830         mSize = 0;
831         mCallback.onRemoved(0, prevSize);
832     }
833 
834     /**
835      * The class that controls the behavior of the {@link SortedList}.
836      * <p>
837      * It defines how items should be sorted and how duplicates should be handled.
838      * <p>
839      * SortedList calls the callback methods on this class to notify changes about the underlying
840      * data.
841      */
842     public static abstract class Callback<T2> implements Comparator<T2>, ListUpdateCallback {
843 
844         /**
845          * Similar to {@link java.util.Comparator#compare(Object, Object)}, should compare two and
846          * return how they should be ordered.
847          *
848          * @param o1 The first object to compare.
849          * @param o2 The second object to compare.
850          *
851          * @return a negative integer, zero, or a positive integer as the
852          * first argument is less than, equal to, or greater than the
853          * second.
854          */
855         @Override
compare(T2 o1, T2 o2)856         abstract public int compare(T2 o1, T2 o2);
857 
858         /**
859          * Called by the SortedList when the item at the given position is updated.
860          *
861          * @param position The position of the item which has been updated.
862          * @param count    The number of items which has changed.
863          */
onChanged(int position, int count)864         abstract public void onChanged(int position, int count);
865 
866         /** {@inheritDoc} */
867         @Override
868         @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
onChanged(int position, int count, Object payload)869         public void onChanged(int position, int count, Object payload) {
870             onChanged(position, count);
871         }
872 
873         /**
874          * Called by the SortedList when it wants to check whether two items have the same data
875          * or not. SortedList uses this information to decide whether it should call
876          * {@link #onChanged(int, int)} or not.
877          * <p>
878          * SortedList uses this method to check equality instead of {@link Object#equals(Object)}
879          * so
880          * that you can change its behavior depending on your UI.
881          * <p>
882          * For example, if you are using SortedList with a
883          * {@link RecyclerView.Adapter RecyclerView.Adapter}, you should
884          * return whether the items' visual representations are the same or not.
885          *
886          * @param oldItem The previous representation of the object.
887          * @param newItem The new object that replaces the previous one.
888          *
889          * @return True if the contents of the items are the same or false if they are different.
890          */
areContentsTheSame(T2 oldItem, T2 newItem)891         abstract public boolean areContentsTheSame(T2 oldItem, T2 newItem);
892 
893         /**
894          * Called by the SortedList to decide whether two objects represent the same Item or not.
895          * <p>
896          * For example, if your items have unique ids, this method should check their equality.
897          *
898          * @param item1 The first item to check.
899          * @param item2 The second item to check.
900          *
901          * @return True if the two items represent the same object or false if they are different.
902          */
areItemsTheSame(T2 item1, T2 item2)903         abstract public boolean areItemsTheSame(T2 item1, T2 item2);
904 
905         /**
906          * When {@link #areItemsTheSame(T2, T2)} returns {@code true} for two items and
907          * {@link #areContentsTheSame(T2, T2)} returns false for them, {@link Callback} calls this
908          * method to get a payload about the change.
909          * <p>
910          * For example, if you are using {@link Callback} with
911          * {@link RecyclerView}, you can return the particular field that
912          * changed in the item and your
913          * {@link RecyclerView.ItemAnimator ItemAnimator} can use that
914          * information to run the correct animation.
915          * <p>
916          * Default implementation returns {@code null}.
917          *
918          * @param item1 The first item to check.
919          * @param item2 The second item to check.
920          * @return A payload object that represents the changes between the two items.
921          */
getChangePayload(T2 item1, T2 item2)922         public @Nullable Object getChangePayload(T2 item1, T2 item2) {
923             return null;
924         }
925     }
926 
927     /**
928      * A callback implementation that can batch notify events dispatched by the SortedList.
929      * <p>
930      * This class can be useful if you want to do multiple operations on a SortedList but don't
931      * want to dispatch each event one by one, which may result in a performance issue.
932      * <p>
933      * For example, if you are going to add multiple items to a SortedList, BatchedCallback call
934      * convert individual <code>onInserted(index, 1)</code> calls into one
935      * <code>onInserted(index, N)</code> if items are added into consecutive indices. This change
936      * can help RecyclerView resolve changes much more easily.
937      * <p>
938      * If consecutive changes in the SortedList are not suitable for batching, BatchingCallback
939      * dispatches them as soon as such case is detected. After your edits on the SortedList is
940      * complete, you <b>must</b> always call {@link BatchedCallback#dispatchLastEvent()} to flush
941      * all changes to the Callback.
942      */
943     public static class BatchedCallback<T2> extends Callback<T2> {
944 
945         final Callback<T2> mWrappedCallback;
946         private final BatchingListUpdateCallback mBatchingListUpdateCallback;
947         /**
948          * Creates a new BatchedCallback that wraps the provided Callback.
949          *
950          * @param wrappedCallback The Callback which should received the data change callbacks.
951          *                        Other method calls (e.g. {@link #compare(Object, Object)} from
952          *                        the SortedList are directly forwarded to this Callback.
953          */
954         @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
BatchedCallback(Callback<T2> wrappedCallback)955         public BatchedCallback(Callback<T2> wrappedCallback) {
956             mWrappedCallback = wrappedCallback;
957             mBatchingListUpdateCallback = new BatchingListUpdateCallback(mWrappedCallback);
958         }
959 
960         /** {@inheritDoc} */
961         @Override
compare(T2 o1, T2 o2)962         public int compare(T2 o1, T2 o2) {
963             return mWrappedCallback.compare(o1, o2);
964         }
965 
966         /** {@inheritDoc} */
967         @Override
onInserted(int position, int count)968         public void onInserted(int position, int count) {
969             mBatchingListUpdateCallback.onInserted(position, count);
970         }
971 
972         /** {@inheritDoc} */
973         @Override
onRemoved(int position, int count)974         public void onRemoved(int position, int count) {
975             mBatchingListUpdateCallback.onRemoved(position, count);
976         }
977 
978         /** {@inheritDoc} */
979         @Override
onMoved(int fromPosition, int toPosition)980         public void onMoved(int fromPosition, int toPosition) {
981             mBatchingListUpdateCallback.onMoved(fromPosition, toPosition);
982         }
983 
984         /** {@inheritDoc} */
985         @Override
onChanged(int position, int count)986         public void onChanged(int position, int count) {
987             mBatchingListUpdateCallback.onChanged(position, count, null);
988         }
989 
990         /** {@inheritDoc} */
991         @Override
992         @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
onChanged(int position, int count, Object payload)993         public void onChanged(int position, int count, Object payload) {
994             mBatchingListUpdateCallback.onChanged(position, count, payload);
995         }
996 
997         /** {@inheritDoc} */
998         @Override
areContentsTheSame(T2 oldItem, T2 newItem)999         public boolean areContentsTheSame(T2 oldItem, T2 newItem) {
1000             return mWrappedCallback.areContentsTheSame(oldItem, newItem);
1001         }
1002 
1003         /** {@inheritDoc} */
1004         @Override
areItemsTheSame(T2 item1, T2 item2)1005         public boolean areItemsTheSame(T2 item1, T2 item2) {
1006             return mWrappedCallback.areItemsTheSame(item1, item2);
1007         }
1008 
1009         /** {@inheritDoc} */
1010         @Override
getChangePayload(T2 item1, T2 item2)1011         public @Nullable Object getChangePayload(T2 item1, T2 item2) {
1012             return mWrappedCallback.getChangePayload(item1, item2);
1013         }
1014 
1015         /**
1016          * This method dispatches any pending event notifications to the wrapped Callback.
1017          * You <b>must</b> always call this method after you are done with editing the SortedList.
1018          */
dispatchLastEvent()1019         public void dispatchLastEvent() {
1020             mBatchingListUpdateCallback.dispatchLastEvent();
1021         }
1022     }
1023 }
1024