1 /*
2  * Copyright 2020 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 static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.ISOLATED_STABLE_IDS;
20 import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.NO_STABLE_IDS;
21 import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS;
22 import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.ALLOW;
23 import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT;
24 import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY;
25 import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
26 
27 import android.util.Log;
28 import android.util.Pair;
29 import android.view.ViewGroup;
30 
31 import androidx.core.util.Preconditions;
32 import androidx.recyclerview.widget.RecyclerView.Adapter;
33 import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy;
34 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
35 
36 import org.jspecify.annotations.NonNull;
37 import org.jspecify.annotations.Nullable;
38 
39 import java.lang.ref.WeakReference;
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.IdentityHashMap;
43 import java.util.List;
44 
45 /**
46  * All logic for the {@link ConcatAdapter} is here so that we can clearly see a separation
47  * between an adapter implementation and merging logic.
48  */
49 class ConcatAdapterController implements NestedAdapterWrapper.Callback {
50     private final ConcatAdapter mConcatAdapter;
51 
52     /**
53      * Holds the mapping from the view type to the adapter which reported that type.
54      */
55     private final ViewTypeStorage mViewTypeStorage;
56 
57     /**
58      * We hold onto the list of attached recyclerviews so that we can dispatch attach/detach to
59      * any adapter that was added later on.
60      * Probably does not need to be a weak reference but playing safe here.
61      */
62     private List<WeakReference<RecyclerView>> mAttachedRecyclerViews = new ArrayList<>();
63 
64     /**
65      * Keeps the information about which ViewHolder is bound by which adapter.
66      * It is set in onBind, reset at onRecycle.
67      */
68     private final IdentityHashMap<ViewHolder, NestedAdapterWrapper>
69             mBinderLookup = new IdentityHashMap<>();
70 
71     private List<NestedAdapterWrapper> mWrappers = new ArrayList<>();
72 
73     // keep one of these around so that we can return wrapper & position w/o allocation ¯\_(ツ)_/¯
74     private WrapperAndLocalPosition mReusableHolder = new WrapperAndLocalPosition();
75 
76     private final ConcatAdapter.Config.@NonNull StableIdMode mStableIdMode;
77 
78     /**
79      * This is where we keep stable ids, if supported
80      */
81     private final StableIdStorage mStableIdStorage;
82 
ConcatAdapterController( ConcatAdapter concatAdapter, ConcatAdapter.Config config)83     ConcatAdapterController(
84             ConcatAdapter concatAdapter,
85             ConcatAdapter.Config config) {
86         mConcatAdapter = concatAdapter;
87 
88         // setup view type handling
89         if (config.isolateViewTypes) {
90             mViewTypeStorage = new ViewTypeStorage.IsolatedViewTypeStorage();
91         } else {
92             mViewTypeStorage = new ViewTypeStorage.SharedIdRangeViewTypeStorage();
93         }
94 
95         // setup stable id handling
96         mStableIdMode = config.stableIdMode;
97         if (config.stableIdMode == NO_STABLE_IDS) {
98             mStableIdStorage = new StableIdStorage.NoStableIdStorage();
99         } else if (config.stableIdMode == ISOLATED_STABLE_IDS) {
100             mStableIdStorage = new StableIdStorage.IsolatedStableIdStorage();
101         } else if (config.stableIdMode == SHARED_STABLE_IDS) {
102             mStableIdStorage = new StableIdStorage.SharedPoolStableIdStorage();
103         } else {
104             throw new IllegalArgumentException("unknown stable id mode");
105         }
106     }
107 
findWrapperFor(Adapter<ViewHolder> adapter)108     private @Nullable NestedAdapterWrapper findWrapperFor(Adapter<ViewHolder> adapter) {
109         final int index = indexOfWrapper(adapter);
110         if (index == -1) {
111             return null;
112         }
113         return mWrappers.get(index);
114     }
115 
indexOfWrapper(Adapter<ViewHolder> adapter)116     private int indexOfWrapper(Adapter<ViewHolder> adapter) {
117         final int limit = mWrappers.size();
118         for (int i = 0; i < limit; i++) {
119             if (mWrappers.get(i).adapter == adapter) {
120                 return i;
121             }
122         }
123         return -1;
124     }
125 
126     /**
127      * return true if added, false otherwise.
128      *
129      * @see ConcatAdapter#addAdapter(Adapter)
130      */
addAdapter(Adapter<ViewHolder> adapter)131     boolean addAdapter(Adapter<ViewHolder> adapter) {
132         return addAdapter(mWrappers.size(), adapter);
133     }
134 
135     /**
136      * return true if added, false otherwise.
137      * throws exception if index is out of bounds
138      *
139      * @see ConcatAdapter#addAdapter(int, Adapter)
140      */
addAdapter(int index, Adapter<ViewHolder> adapter)141     boolean addAdapter(int index, Adapter<ViewHolder> adapter) {
142         if (index < 0 || index > mWrappers.size()) {
143             throw new IndexOutOfBoundsException("Index must be between 0 and "
144                     + mWrappers.size() + ". Given:" + index);
145         }
146         if (hasStableIds()) {
147             Preconditions.checkArgument(adapter.hasStableIds(),
148                     "All sub adapters must have stable ids when stable id mode "
149                     + "is ISOLATED_STABLE_IDS or SHARED_STABLE_IDS");
150         } else {
151             if (adapter.hasStableIds()) {
152                 Log.w(ConcatAdapter.TAG, "Stable ids in the adapter will be ignored as the"
153                         + " ConcatAdapter is configured not to have stable ids");
154             }
155         }
156         NestedAdapterWrapper existing = findWrapperFor(adapter);
157         if (existing != null) {
158             return false;
159         }
160         NestedAdapterWrapper wrapper = new NestedAdapterWrapper(adapter, this,
161                 mViewTypeStorage, mStableIdStorage.createStableIdLookup());
162         mWrappers.add(index, wrapper);
163         // notify attach for all recyclerview
164         for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) {
165             RecyclerView recyclerView = reference.get();
166             if (recyclerView != null) {
167                 adapter.onAttachedToRecyclerView(recyclerView);
168             }
169         }
170         // new items, notify add for them
171         if (wrapper.getCachedItemCount() > 0) {
172             mConcatAdapter.notifyItemRangeInserted(
173                     countItemsBefore(wrapper),
174                     wrapper.getCachedItemCount()
175             );
176         }
177         // reset state restoration strategy
178         calculateAndUpdateStateRestorationPolicy();
179         return true;
180     }
181 
removeAdapter(Adapter<ViewHolder> adapter)182     boolean removeAdapter(Adapter<ViewHolder> adapter) {
183         final int index = indexOfWrapper(adapter);
184         if (index == -1) {
185             return false;
186         }
187         NestedAdapterWrapper wrapper = mWrappers.get(index);
188         int offset = countItemsBefore(wrapper);
189         mWrappers.remove(index);
190         mConcatAdapter.notifyItemRangeRemoved(offset, wrapper.getCachedItemCount());
191         // notify detach for all recyclerviews
192         for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) {
193             RecyclerView recyclerView = reference.get();
194             if (recyclerView != null) {
195                 adapter.onDetachedFromRecyclerView(recyclerView);
196             }
197         }
198         wrapper.dispose();
199         calculateAndUpdateStateRestorationPolicy();
200         return true;
201     }
202 
countItemsBefore(NestedAdapterWrapper wrapper)203     private int countItemsBefore(NestedAdapterWrapper wrapper) {
204         int count = 0;
205         for (NestedAdapterWrapper item : mWrappers) {
206             if (item != wrapper) {
207                 count += item.getCachedItemCount();
208             } else {
209                 break;
210             }
211         }
212         return count;
213     }
214 
getItemId(int globalPosition)215     public long getItemId(int globalPosition) {
216         WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition);
217         long globalItemId = wrapperAndPos.mWrapper.getItemId(wrapperAndPos.mLocalPosition);
218         releaseWrapperAndLocalPosition(wrapperAndPos);
219         return globalItemId;
220     }
221 
222     @Override
onChanged(@onNull NestedAdapterWrapper wrapper)223     public void onChanged(@NonNull NestedAdapterWrapper wrapper) {
224         // TODO should we notify more cleverly, maybe in v2
225         mConcatAdapter.notifyDataSetChanged();
226         calculateAndUpdateStateRestorationPolicy();
227     }
228 
229     @Override
onItemRangeChanged(@onNull NestedAdapterWrapper nestedAdapterWrapper, int positionStart, int itemCount)230     public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
231             int positionStart, int itemCount) {
232         final int offset = countItemsBefore(nestedAdapterWrapper);
233         mConcatAdapter.notifyItemRangeChanged(
234                 positionStart + offset,
235                 itemCount
236         );
237     }
238 
239     @Override
onItemRangeChanged(@onNull NestedAdapterWrapper nestedAdapterWrapper, int positionStart, int itemCount, @Nullable Object payload)240     public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
241             int positionStart, int itemCount, @Nullable Object payload) {
242         final int offset = countItemsBefore(nestedAdapterWrapper);
243         mConcatAdapter.notifyItemRangeChanged(
244                 positionStart + offset,
245                 itemCount,
246                 payload
247         );
248     }
249 
250     @Override
onItemRangeInserted(@onNull NestedAdapterWrapper nestedAdapterWrapper, int positionStart, int itemCount)251     public void onItemRangeInserted(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
252             int positionStart, int itemCount) {
253         final int offset = countItemsBefore(nestedAdapterWrapper);
254         mConcatAdapter.notifyItemRangeInserted(
255                 positionStart + offset,
256                 itemCount
257         );
258     }
259 
260     @Override
onItemRangeRemoved(@onNull NestedAdapterWrapper nestedAdapterWrapper, int positionStart, int itemCount)261     public void onItemRangeRemoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
262             int positionStart, int itemCount) {
263         int offset = countItemsBefore(nestedAdapterWrapper);
264         mConcatAdapter.notifyItemRangeRemoved(
265                 positionStart + offset,
266                 itemCount
267         );
268     }
269 
270     @Override
onItemRangeMoved(@onNull NestedAdapterWrapper nestedAdapterWrapper, int fromPosition, int toPosition)271     public void onItemRangeMoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
272             int fromPosition, int toPosition) {
273         int offset = countItemsBefore(nestedAdapterWrapper);
274         mConcatAdapter.notifyItemMoved(
275                 fromPosition + offset,
276                 toPosition + offset
277         );
278     }
279 
280     @Override
onStateRestorationPolicyChanged(NestedAdapterWrapper nestedAdapterWrapper)281     public void onStateRestorationPolicyChanged(NestedAdapterWrapper nestedAdapterWrapper) {
282         calculateAndUpdateStateRestorationPolicy();
283     }
284 
calculateAndUpdateStateRestorationPolicy()285     private void calculateAndUpdateStateRestorationPolicy() {
286         StateRestorationPolicy newPolicy = computeStateRestorationPolicy();
287         if (newPolicy != mConcatAdapter.getStateRestorationPolicy()) {
288             mConcatAdapter.internalSetStateRestorationPolicy(newPolicy);
289         }
290     }
291 
computeStateRestorationPolicy()292     private StateRestorationPolicy computeStateRestorationPolicy() {
293         for (NestedAdapterWrapper wrapper : mWrappers) {
294             StateRestorationPolicy strategy =
295                     wrapper.adapter.getStateRestorationPolicy();
296             if (strategy == PREVENT) {
297                 // one adapter can block all
298                 return PREVENT;
299             } else if (strategy == PREVENT_WHEN_EMPTY && wrapper.getCachedItemCount() == 0) {
300                 // an adapter wants to allow w/ size but we need to make sure there is no prevent
301                 return PREVENT;
302             }
303         }
304         return ALLOW;
305     }
306 
getTotalCount()307     public int getTotalCount() {
308         // should we cache this as well ?
309         int total = 0;
310         for (NestedAdapterWrapper wrapper : mWrappers) {
311             total += wrapper.getCachedItemCount();
312         }
313         return total;
314     }
315 
getItemViewType(int globalPosition)316     public int getItemViewType(int globalPosition) {
317         WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition);
318         int itemViewType = wrapperAndPos.mWrapper.getItemViewType(wrapperAndPos.mLocalPosition);
319         releaseWrapperAndLocalPosition(wrapperAndPos);
320         return itemViewType;
321     }
322 
onCreateViewHolder(ViewGroup parent, int globalViewType)323     public ViewHolder onCreateViewHolder(ViewGroup parent, int globalViewType) {
324         NestedAdapterWrapper wrapper = mViewTypeStorage.getWrapperForGlobalType(globalViewType);
325         return wrapper.onCreateViewHolder(parent, globalViewType);
326     }
327 
getWrappedAdapterAndPosition( int globalPosition)328     public Pair<Adapter<? extends ViewHolder>, Integer> getWrappedAdapterAndPosition(
329             int globalPosition) {
330         WrapperAndLocalPosition wrapper = findWrapperAndLocalPosition(globalPosition);
331         Pair<Adapter<? extends ViewHolder>, Integer> pair = new Pair<>(wrapper.mWrapper.adapter,
332                 wrapper.mLocalPosition);
333         releaseWrapperAndLocalPosition(wrapper);
334         return pair;
335     }
336 
337     /**
338      * Always call {@link #releaseWrapperAndLocalPosition(WrapperAndLocalPosition)} when you are
339      * done with it
340      */
findWrapperAndLocalPosition( int globalPosition )341     private @NonNull WrapperAndLocalPosition findWrapperAndLocalPosition(
342             int globalPosition
343     ) {
344         WrapperAndLocalPosition result;
345         if (mReusableHolder.mInUse) {
346             result = new WrapperAndLocalPosition();
347         } else {
348             mReusableHolder.mInUse = true;
349             result = mReusableHolder;
350         }
351         int localPosition = globalPosition;
352         for (NestedAdapterWrapper wrapper : mWrappers) {
353             if (wrapper.getCachedItemCount() > localPosition) {
354                 result.mWrapper = wrapper;
355                 result.mLocalPosition = localPosition;
356                 break;
357             }
358             localPosition -= wrapper.getCachedItemCount();
359         }
360         if (result.mWrapper == null) {
361             throw new IllegalArgumentException("Cannot find wrapper for " + globalPosition);
362         }
363         return result;
364     }
365 
releaseWrapperAndLocalPosition(WrapperAndLocalPosition wrapperAndLocalPosition)366     private void releaseWrapperAndLocalPosition(WrapperAndLocalPosition wrapperAndLocalPosition) {
367         wrapperAndLocalPosition.mInUse = false;
368         wrapperAndLocalPosition.mWrapper = null;
369         wrapperAndLocalPosition.mLocalPosition = -1;
370         mReusableHolder = wrapperAndLocalPosition;
371     }
372 
onBindViewHolder(ViewHolder holder, int globalPosition)373     public void onBindViewHolder(ViewHolder holder, int globalPosition) {
374         WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition);
375         mBinderLookup.put(holder, wrapperAndPos.mWrapper);
376         wrapperAndPos.mWrapper.onBindViewHolder(holder, wrapperAndPos.mLocalPosition);
377         releaseWrapperAndLocalPosition(wrapperAndPos);
378     }
379 
canRestoreState()380     public boolean canRestoreState() {
381         for (NestedAdapterWrapper wrapper : mWrappers) {
382             if (!wrapper.adapter.canRestoreState()) {
383                 return false;
384             }
385         }
386         return true;
387     }
388 
onViewAttachedToWindow(ViewHolder holder)389     public void onViewAttachedToWindow(ViewHolder holder) {
390         NestedAdapterWrapper wrapper = getWrapper(holder);
391         wrapper.adapter.onViewAttachedToWindow(holder);
392     }
393 
onViewDetachedFromWindow(ViewHolder holder)394     public void onViewDetachedFromWindow(ViewHolder holder) {
395         NestedAdapterWrapper wrapper = getWrapper(holder);
396         wrapper.adapter.onViewDetachedFromWindow(holder);
397     }
398 
onViewRecycled(ViewHolder holder)399     public void onViewRecycled(ViewHolder holder) {
400         NestedAdapterWrapper wrapper = mBinderLookup.get(holder);
401         if (wrapper == null) {
402             throw new IllegalStateException("Cannot find wrapper for " + holder
403                     + ", seems like it is not bound by this adapter: " + this);
404         }
405         wrapper.adapter.onViewRecycled(holder);
406         mBinderLookup.remove(holder);
407     }
408 
onFailedToRecycleView(ViewHolder holder)409     public boolean onFailedToRecycleView(ViewHolder holder) {
410         NestedAdapterWrapper wrapper = mBinderLookup.get(holder);
411         if (wrapper == null) {
412             throw new IllegalStateException("Cannot find wrapper for " + holder
413                     + ", seems like it is not bound by this adapter: " + this);
414         }
415         final boolean result = wrapper.adapter.onFailedToRecycleView(holder);
416         mBinderLookup.remove(holder);
417         return result;
418     }
419 
getWrapper(ViewHolder holder)420     private @NonNull NestedAdapterWrapper getWrapper(ViewHolder holder) {
421         NestedAdapterWrapper wrapper = mBinderLookup.get(holder);
422         if (wrapper == null) {
423             throw new IllegalStateException("Cannot find wrapper for " + holder
424                     + ", seems like it is not bound by this adapter: " + this);
425         }
426         return wrapper;
427     }
428 
isAttachedTo(RecyclerView recyclerView)429     private boolean isAttachedTo(RecyclerView recyclerView) {
430         for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) {
431             if (reference.get() == recyclerView) {
432                 return true;
433             }
434         }
435         return false;
436     }
437 
onAttachedToRecyclerView(RecyclerView recyclerView)438     public void onAttachedToRecyclerView(RecyclerView recyclerView) {
439         if (isAttachedTo(recyclerView)) {
440             return;
441         }
442         mAttachedRecyclerViews.add(new WeakReference<>(recyclerView));
443         for (NestedAdapterWrapper wrapper : mWrappers) {
444             wrapper.adapter.onAttachedToRecyclerView(recyclerView);
445         }
446     }
447 
onDetachedFromRecyclerView(RecyclerView recyclerView)448     public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
449         for (int i = mAttachedRecyclerViews.size() - 1; i >= 0; i--) {
450             WeakReference<RecyclerView> reference = mAttachedRecyclerViews.get(i);
451             if (reference.get() == null) {
452                 mAttachedRecyclerViews.remove(i);
453             } else if (reference.get() == recyclerView) {
454                 mAttachedRecyclerViews.remove(i);
455                 break; // here we can break as we don't keep duplicates
456             }
457         }
458         for (NestedAdapterWrapper wrapper : mWrappers) {
459             wrapper.adapter.onDetachedFromRecyclerView(recyclerView);
460         }
461     }
462 
getLocalAdapterPosition( Adapter<? extends ViewHolder> adapter, ViewHolder viewHolder, int globalPosition )463     public int getLocalAdapterPosition(
464             Adapter<? extends ViewHolder> adapter,
465             ViewHolder viewHolder,
466             int globalPosition
467     ) {
468         NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder);
469         if (wrapper == null) {
470             return NO_POSITION;
471         }
472         int itemsBefore = countItemsBefore(wrapper);
473         // local position is globalPosition - itemsBefore
474         int localPosition = globalPosition - itemsBefore;
475         // Early error detection:
476         int itemCount = wrapper.adapter.getItemCount();
477         if (localPosition < 0 || localPosition >= itemCount) {
478             throw new IllegalStateException("Detected inconsistent adapter updates. The"
479                     + " local position of the view holder maps to " + localPosition + " which"
480                     + " is out of bounds for the adapter with size "
481                     + itemCount + "."
482                     + "Make sure to immediately call notify methods in your adapter when you "
483                     + "change the backing data"
484                     + "viewHolder:" + viewHolder
485                     + "adapter:" + adapter);
486         }
487         return wrapper.adapter.findRelativeAdapterPositionIn(adapter, viewHolder, localPosition);
488     }
489 
490 
getBoundAdapter(ViewHolder viewHolder)491     public @Nullable Adapter<? extends ViewHolder> getBoundAdapter(ViewHolder viewHolder) {
492         NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder);
493         if (wrapper == null) {
494             return null;
495         }
496         return wrapper.adapter;
497     }
498 
499     @SuppressWarnings("MixedMutabilityReturnType")
getCopyOfAdapters()500     public List<Adapter<? extends ViewHolder>> getCopyOfAdapters() {
501         if (mWrappers.isEmpty()) {
502             return Collections.emptyList();
503         }
504         List<Adapter<? extends ViewHolder>> adapters = new ArrayList<>(mWrappers.size());
505         for (NestedAdapterWrapper wrapper : mWrappers) {
506             adapters.add(wrapper.adapter);
507         }
508         return adapters;
509     }
510 
hasStableIds()511     public boolean hasStableIds() {
512         return mStableIdMode != NO_STABLE_IDS;
513     }
514 
515     /**
516      * Helper class to hold onto wrapper and local position without allocating objects as this is
517      * a very common call.
518      */
519     static class WrapperAndLocalPosition {
520         NestedAdapterWrapper mWrapper;
521         int mLocalPosition;
522         boolean mInUse;
523     }
524 }
525