adapters = new LinkedList<>();
+
+ private long nextUnassignedItemId;
+
+ /**
+ * Map of global view type to local adapter.
+ *
+ * Not the same as {@link #adapters}, it may have duplicates and may be in a different order.
+ */
+ private final List viewTypes = new LinkedList<>();
+
+ /** Observes a single sub adapter and maps the positions on the events to global positions. */
+ private static class AdapterDataObserver extends RecyclerView.AdapterDataObserver {
+
+ private final RecyclerViewConcatenateAdapter mergeAdapter;
+ private final RecyclerView.Adapter adapter;
+
+ AdapterDataObserver(RecyclerViewConcatenateAdapter mergeAdapter, RecyclerView.Adapter adapter) {
+ this.mergeAdapter = mergeAdapter;
+ this.adapter = adapter;
+ }
+
+ @Override
+ public void onChanged() {
+ mergeAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ int subAdapterOffset = mergeAdapter.getSubAdapterFirstGlobalPosition(adapter);
+
+ mergeAdapter.notifyItemRangeChanged(subAdapterOffset + positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ int subAdapterOffset = mergeAdapter.getSubAdapterFirstGlobalPosition(adapter);
+
+ mergeAdapter.notifyItemRangeInserted(subAdapterOffset + positionStart, itemCount);
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ int subAdapterOffset = mergeAdapter.getSubAdapterFirstGlobalPosition(adapter);
+
+ mergeAdapter.notifyItemRangeRemoved(subAdapterOffset + positionStart, itemCount);
+ }
+ }
+
+ private static class ChildAdapter {
+
+ final RecyclerView.Adapter adapter;
+
+ /** Map of global view types to local view types */
+ private final SparseIntArray globalViewTypesMap = new SparseIntArray();
+
+ /** Map of local view types to global view types */
+ private final SparseIntArray localViewTypesMap = new SparseIntArray();
+
+ private final AdapterDataObserver adapterDataObserver;
+
+ /** Map of local ids to global ids. */
+ private final LongSparseArray localItemIdMap = new LongSparseArray<>();
+
+ ChildAdapter(@NonNull RecyclerView.Adapter adapter, @NonNull AdapterDataObserver adapterDataObserver) {
+ this.adapter = adapter;
+ this.adapterDataObserver = adapterDataObserver;
+
+ this.adapter.registerAdapterDataObserver(this.adapterDataObserver);
+ }
+
+ int getGlobalItemViewType(int localPosition, int defaultValue) {
+ int localViewType = adapter.getItemViewType(localPosition);
+ int globalViewType = localViewTypesMap.get(localViewType, defaultValue);
+
+ if (globalViewType == defaultValue) {
+ globalViewTypesMap.append(globalViewType, localViewType);
+ localViewTypesMap.append(localViewType, globalViewType);
+ }
+
+ return globalViewType;
+ }
+
+ long getGlobalItemId(int localPosition, long defaultGlobalValue) {
+ final long localItemId = adapter.getItemId(localPosition);
+
+ if (RecyclerView.NO_ID == localItemId) {
+ return RecyclerView.NO_ID;
+ }
+
+ final Long globalItemId = localItemIdMap.get(localItemId);
+
+ if (globalItemId == null) {
+ localItemIdMap.put(localItemId, defaultGlobalValue);
+ return defaultGlobalValue;
+ }
+
+ return globalItemId;
+ }
+
+ void unregister() {
+ adapter.unregisterAdapterDataObserver(adapterDataObserver);
+ }
+
+ RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int globalViewType) {
+ int localViewType = globalViewTypesMap.get(globalViewType);
+
+ return adapter.onCreateViewHolder(viewGroup, localViewType);
+ }
+ }
+
+ static class ChildAdapterPositionPair {
+
+ final ChildAdapter childAdapter;
+ final int localPosition;
+
+ ChildAdapterPositionPair(@NonNull ChildAdapter adapter, int position) {
+ childAdapter = adapter;
+ localPosition = position;
+ }
+
+ RecyclerView.Adapter getAdapter() {
+ return childAdapter.adapter;
+ }
+ }
+
+ /**
+ * @param adapter Append an adapter to the list of adapters.
+ */
+ public void addAdapter(@NonNull RecyclerView.Adapter adapter) {
+ addAdapter(adapters.size(), adapter);
+ }
+
+ /**
+ * @param index The index at which to add an adapter to the list of adapters.
+ * @param adapter The adapter to add.
+ */
+ public void addAdapter(int index, @NonNull RecyclerView.Adapter adapter) {
+ AdapterDataObserver adapterDataObserver = new AdapterDataObserver(this, adapter);
+ adapters.add(index, new ChildAdapter(adapter, adapterDataObserver));
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Clear all adapters from the list of adapters.
+ */
+ public void clearAdapters() {
+ for (ChildAdapter childAdapter : adapters) {
+ childAdapter.unregister();
+ }
+
+ adapters.clear();
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Return a childAdapterPositionPair object for a given global position.
+ *
+ * @param globalPosition The global position in the entire set of items.
+ * @return A childAdapterPositionPair object containing a reference to the adapter and the local
+ * position in that adapter that corresponds to the given global position.
+ */
+ @NonNull
+ ChildAdapterPositionPair getLocalPosition(final int globalPosition) {
+ int count = 0;
+
+ for (ChildAdapter childAdapter : adapters) {
+ int newCount = count + childAdapter.adapter.getItemCount();
+
+ if (globalPosition < newCount) {
+ return new ChildAdapterPositionPair(childAdapter, globalPosition - count);
+ }
+
+ count = newCount;
+ }
+
+ throw new AssertionError("Position out of range");
+ }
+
+ @Override
+ @NonNull
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+ ChildAdapter childAdapter = viewTypes.get(viewType);
+ if (childAdapter == null) {
+ throw new AssertionError("Unknown view type");
+ }
+
+ return childAdapter.onCreateViewHolder(viewGroup, viewType);
+ }
+
+ /**
+ * Return the first global position in the entire set of items for a given adapter.
+ *
+ * @param adapter The adapter for which to the return the first global position.
+ * @return The first global position for the given adapter, or -1 if no such position could be found.
+ */
+ private int getSubAdapterFirstGlobalPosition(@NonNull RecyclerView.Adapter adapter) {
+ int count = 0;
+
+ for (ChildAdapter childAdapterWrapper : adapters) {
+ RecyclerView.Adapter childAdapter = childAdapterWrapper.adapter;
+
+ if (childAdapter == adapter) {
+ return count;
+ }
+
+ count += childAdapter.getItemCount();
+ }
+
+ throw new AssertionError("Adapter not found in list of adapters");
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
+ ChildAdapterPositionPair childAdapterPositionPair = getLocalPosition(position);
+ RecyclerView.Adapter adapter = childAdapterPositionPair.getAdapter();
+ //noinspection unchecked
+ adapter.onBindViewHolder(viewHolder, childAdapterPositionPair.localPosition);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ int nextUnassignedViewType = viewTypes.size();
+ ChildAdapterPositionPair localPosition = getLocalPosition(position);
+
+ int viewType = localPosition.childAdapter.getGlobalItemViewType(localPosition.localPosition, nextUnassignedViewType);
+
+ if (viewType == nextUnassignedViewType) {
+ viewTypes.add(viewType, localPosition.childAdapter);
+ }
+
+ return viewType;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ ChildAdapterPositionPair localPosition = getLocalPosition(position);
+
+ long itemId = localPosition.childAdapter.getGlobalItemId(localPosition.localPosition, nextUnassignedItemId);
+
+ if (itemId == nextUnassignedItemId) {
+ nextUnassignedItemId++;
+ }
+
+ return itemId;
+ }
+
+ @Override
+ public int getItemCount() {
+ int count = 0;
+
+ for (ChildAdapter adapter : adapters) {
+ count += adapter.adapter.getItemCount();
+ }
+
+ return count;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapterStickyHeader.java b/src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapterStickyHeader.java
new file mode 100644
index 0000000000..e6152507a9
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapterStickyHeader.java
@@ -0,0 +1,61 @@
+package org.thoughtcrime.securesms.util.adapter;
+
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
+import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
+import org.whispersystems.libsignal.util.Pair;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+public final class RecyclerViewConcatenateAdapterStickyHeader extends RecyclerViewConcatenateAdapter
+ implements StickyHeaderDecoration.StickyHeaderAdapter,
+ RecyclerViewFastScroller.FastScrollAdapter
+{
+
+ @Override
+ public long getHeaderId(int position) {
+ return getForPosition(position).transform(p -> p.first().getHeaderId(p.second())).or(-1L);
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
+ return getForPosition(position).transform(p -> p.first().onCreateHeaderViewHolder(parent, p.second())).orNull();
+ }
+
+ @Override
+ public void onBindHeaderViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+ Optional> forPosition = getForPosition(position);
+
+ if (forPosition.isPresent()) {
+ Pair stickyHeaderAdapterIntegerPair = forPosition.get();
+ //noinspection unchecked
+ stickyHeaderAdapterIntegerPair.first().onBindHeaderViewHolder(viewHolder, stickyHeaderAdapterIntegerPair.second());
+ }
+ }
+
+ @Override
+ public CharSequence getBubbleText(int position) {
+ Optional> forPosition = getForPosition(position);
+
+ return forPosition.transform(a -> {
+ if (a.first() instanceof RecyclerViewFastScroller.FastScrollAdapter) {
+ return ((RecyclerViewFastScroller.FastScrollAdapter) a.first()).getBubbleText(a.second());
+ } else {
+ return "";
+ }
+ }).or("");
+ }
+
+ private Optional> getForPosition(int position) {
+ ChildAdapterPositionPair localAdapterPosition = getLocalPosition(position);
+ RecyclerView.Adapter extends RecyclerView.ViewHolder> adapter = localAdapterPosition.getAdapter();
+
+ if (adapter instanceof StickyHeaderDecoration.StickyHeaderAdapter) {
+ StickyHeaderDecoration.StickyHeaderAdapter sticky = (StickyHeaderDecoration.StickyHeaderAdapter) adapter;
+ return Optional.of(new Pair<>(sticky, localAdapterPosition.localPosition));
+ }
+ return Optional.absent();
+ }
+}