1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package androidx.leanback.widget;
15 
16 import android.util.Log;
17 
18 import androidx.recyclerview.widget.DiffUtil;
19 import androidx.recyclerview.widget.ListUpdateCallback;
20 
21 import org.jspecify.annotations.NonNull;
22 import org.jspecify.annotations.Nullable;
23 
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.List;
28 
29 /**
30  * An {@link ObjectAdapter} implemented with an {@link ArrayList}.
31  */
32 public class ArrayObjectAdapter extends ObjectAdapter {
33 
34     private static final Boolean DEBUG = false;
35     private static final String TAG = "ArrayObjectAdapter";
36 
37     private final List<Object> mItems = new ArrayList<>();
38 
39     // To compute the payload correctly, we should use a temporary list to hold all the old items.
40     @SuppressWarnings("WeakerAccess") /* synthetic access */
41     final List<Object> mOldItems = new ArrayList<>();
42 
43     // Un modifiable version of mItems;
44     private List<?> mUnmodifiableItems;
45 
46     /**
47      * Constructs an adapter with the given {@link PresenterSelector}.
48      */
ArrayObjectAdapter(@onNull PresenterSelector presenterSelector)49     public ArrayObjectAdapter(@NonNull PresenterSelector presenterSelector) {
50         super(presenterSelector);
51     }
52 
53     /**
54      * Constructs an adapter that uses the given {@link Presenter} for all items.
55      */
ArrayObjectAdapter(@onNull Presenter presenter)56     public ArrayObjectAdapter(@NonNull Presenter presenter) {
57         super(presenter);
58     }
59 
60     /**
61      * Constructs an adapter.
62      */
ArrayObjectAdapter()63     public ArrayObjectAdapter() {
64         super();
65     }
66 
67     @Override
size()68     public int size() {
69         return mItems.size();
70     }
71 
72     @Override
get(int index)73     public @Nullable Object get(int index) {
74         return mItems.get(index);
75     }
76 
77     /**
78      * Returns the index for the first occurrence of item in the adapter, or -1 if
79      * not found.
80      *
81      * @param item The item to find in the list.
82      * @return Index of the first occurrence of the item in the adapter, or -1
83      * if not found.
84      */
indexOf(@onNull Object item)85     public int indexOf(@NonNull Object item) {
86         return mItems.indexOf(item);
87     }
88 
89     /**
90      * Notify that the content of a range of items changed. Note that this is
91      * not same as items being added or removed.
92      *
93      * @param positionStart The position of first item that has changed.
94      * @param itemCount     The count of how many items have changed.
95      */
notifyArrayItemRangeChanged(int positionStart, int itemCount)96     public void notifyArrayItemRangeChanged(int positionStart, int itemCount) {
97         notifyItemRangeChanged(positionStart, itemCount);
98     }
99 
100     /**
101      * Adds an item to the end of the adapter.
102      *
103      * @param item The item to add to the end of the adapter.
104      */
add(@onNull Object item)105     public void add(@NonNull Object item) {
106         add(mItems.size(), item);
107     }
108 
109     /**
110      * Inserts an item into this adapter at the specified index.
111      * If the index is > {@link #size} an exception will be thrown.
112      *
113      * @param index The index at which the item should be inserted.
114      * @param item  The item to insert into the adapter.
115      */
add(int index, @NonNull Object item)116     public void add(int index, @NonNull Object item) {
117         mItems.add(index, item);
118         notifyItemRangeInserted(index, 1);
119     }
120 
121     /**
122      * Adds the objects in the given collection to the adapter, starting at the
123      * given index.  If the index is >= {@link #size} an exception will be thrown.
124      *
125      * @param index The index at which the items should be inserted.
126      * @param items A {@link Collection} of items to insert.
127      */
128     @SuppressWarnings("unchecked")
addAll(int index, @NonNull Collection<?> items)129     public void addAll(int index, @NonNull Collection<?> items) {
130         int itemsCount = items.size();
131         if (itemsCount == 0) {
132             return;
133         }
134         mItems.addAll(index, items);
135         notifyItemRangeInserted(index, itemsCount);
136     }
137 
138     /**
139      * Removes the first occurrence of the given item from the adapter.
140      *
141      * @param item The item to remove from the adapter.
142      * @return True if the item was found and thus removed from the adapter.
143      */
remove(@onNull Object item)144     public boolean remove(@NonNull Object item) {
145         int index = mItems.indexOf(item);
146         if (index >= 0) {
147             mItems.remove(index);
148             notifyItemRangeRemoved(index, 1);
149         }
150         return index >= 0;
151     }
152 
153     /**
154      * Moved the item at fromPosition to toPosition.
155      *
156      * @param fromPosition Previous position of the item.
157      * @param toPosition   New position of the item.
158      */
move(int fromPosition, int toPosition)159     public void move(int fromPosition, int toPosition) {
160         if (fromPosition == toPosition) {
161             // no-op
162             return;
163         }
164         Object item = mItems.remove(fromPosition);
165         mItems.add(toPosition, item);
166         notifyItemMoved(fromPosition, toPosition);
167     }
168 
169     /**
170      * Replaces item at position with a new item and calls notifyItemRangeChanged()
171      * at the given position.  Note that this method does not compare new item to
172      * existing item.
173      *
174      * @param position The index of item to replace.
175      * @param item     The new item to be placed at given position.
176      */
replace(int position, @NonNull Object item)177     public void replace(int position, @NonNull Object item) {
178         mItems.set(position, item);
179         notifyItemRangeChanged(position, 1);
180     }
181 
182     /**
183      * Removes a range of items from the adapter. The range is specified by giving
184      * the starting position and the number of elements to remove.
185      *
186      * @param position The index of the first item to remove.
187      * @param count    The number of items to remove.
188      * @return The number of items removed.
189      */
removeItems(int position, int count)190     public int removeItems(int position, int count) {
191         int itemsToRemove = Math.min(count, mItems.size() - position);
192         if (itemsToRemove <= 0) {
193             return 0;
194         }
195 
196         for (int i = 0; i < itemsToRemove; i++) {
197             mItems.remove(position);
198         }
199         notifyItemRangeRemoved(position, itemsToRemove);
200         return itemsToRemove;
201     }
202 
203     /**
204      * Removes all items from this adapter, leaving it empty.
205      */
clear()206     public void clear() {
207         int itemCount = mItems.size();
208         if (itemCount == 0) {
209             return;
210         }
211         mItems.clear();
212         notifyItemRangeRemoved(0, itemCount);
213     }
214 
215     /**
216      * Gets a read-only view of the list of object of this ArrayObjectAdapter.
217      */
218     @SuppressWarnings("unchecked")
unmodifiableList()219     public <E> @NonNull List<E> unmodifiableList() {
220 
221         // The mUnmodifiableItems will only be created once as long as the content of mItems has not
222         // been changed.
223         if (mUnmodifiableItems == null) {
224             mUnmodifiableItems = Collections.unmodifiableList(mItems);
225         }
226         return (List<E>) mUnmodifiableItems;
227     }
228 
229     @Override
isImmediateNotifySupported()230     public boolean isImmediateNotifySupported() {
231         return true;
232     }
233 
234     ListUpdateCallback mListUpdateCallback;
235 
236     /**
237      * Set a new item list to adapter. The DiffUtil will compute the difference and dispatch it to
238      * specified position.
239      *
240      * @param itemList List of new Items
241      * @param callback Optional DiffCallback Object to compute the difference between the old data
242      *                 set and new data set. When null, {@link #notifyChanged()} will be fired.
243      */
244     @SuppressWarnings("unchecked")
setItems( final @NonNull List itemList, final @Nullable DiffCallback callback )245     public void setItems(
246             final @NonNull List itemList,
247             final @Nullable DiffCallback callback
248     ) {
249         if (callback == null) {
250             // shortcut when DiffCallback is not provided
251             mItems.clear();
252             mItems.addAll(itemList);
253             notifyChanged();
254             return;
255         }
256         mOldItems.clear();
257         mOldItems.addAll(mItems);
258 
259         DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
260             @Override
261             public int getOldListSize() {
262                 return mOldItems.size();
263             }
264 
265             @Override
266             public int getNewListSize() {
267                 return itemList.size();
268             }
269 
270             @Override
271             public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
272                 return callback.areItemsTheSame(mOldItems.get(oldItemPosition),
273                         itemList.get(newItemPosition));
274             }
275 
276             @Override
277             public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
278                 return callback.areContentsTheSame(mOldItems.get(oldItemPosition),
279                         itemList.get(newItemPosition));
280             }
281 
282             @Override
283             public @Nullable Object getChangePayload(int oldItemPosition, int newItemPosition) {
284                 return callback.getChangePayload(mOldItems.get(oldItemPosition),
285                         itemList.get(newItemPosition));
286             }
287         });
288 
289         // update items.
290         mItems.clear();
291         mItems.addAll(itemList);
292 
293         // dispatch diff result
294         if (mListUpdateCallback == null) {
295             mListUpdateCallback = new ListUpdateCallback() {
296 
297                 @Override
298                 public void onInserted(int position, int count) {
299                     if (DEBUG) {
300                         Log.d(TAG, "onInserted");
301                     }
302                     notifyItemRangeInserted(position, count);
303                 }
304 
305                 @Override
306                 public void onRemoved(int position, int count) {
307                     if (DEBUG) {
308                         Log.d(TAG, "onRemoved");
309                     }
310                     notifyItemRangeRemoved(position, count);
311                 }
312 
313                 @Override
314                 public void onMoved(int fromPosition, int toPosition) {
315                     if (DEBUG) {
316                         Log.d(TAG, "onMoved");
317                     }
318                     notifyItemMoved(fromPosition, toPosition);
319                 }
320 
321                 @Override
322                 public void onChanged(int position, int count, Object payload) {
323                     if (DEBUG) {
324                         Log.d(TAG, "onChanged");
325                     }
326                     notifyItemRangeChanged(position, count, payload);
327                 }
328             };
329         }
330         diffResult.dispatchUpdatesTo(mListUpdateCallback);
331         mOldItems.clear();
332     }
333 }
334