1package androidx.leanback.app;
2
3import androidx.leanback.widget.ObjectAdapter;
4import androidx.leanback.widget.Row;
5
6/**
7 * Wrapper class for {@link ObjectAdapter} used by {@link BrowseFragment} to initialize
8 * {@link RowsFragment}. We use invisible rows to represent
9 * {@link androidx.leanback.widget.DividerRow},
10 * {@link androidx.leanback.widget.SectionRow} and
11 * {@link androidx.leanback.widget.PageRow} in RowsFragment. In case we have an
12 * invisible row at the end of a RowsFragment, it creates a jumping effect as the layout manager
13 * thinks there are items even though they're invisible. This class takes care of filtering out
14 * the invisible rows at the end. In case the data inside the adapter changes, it adjusts the
15 * bounds to reflect the latest data.
16 * {@link #detach()} must be called to release DataObserver from Adapter.
17 */
18class ListRowDataAdapter extends ObjectAdapter {
19    public static final int ON_ITEM_RANGE_CHANGED = 2;
20    public static final int ON_ITEM_RANGE_INSERTED = 4;
21    public static final int ON_ITEM_RANGE_REMOVED = 8;
22    public static final int ON_CHANGED = 16;
23
24    private final ObjectAdapter mAdapter;
25    int mLastVisibleRowIndex;
26    final DataObserver mDataObserver;
27
28    public ListRowDataAdapter(ObjectAdapter adapter) {
29        super(adapter.getPresenterSelector());
30        this.mAdapter = adapter;
31        initialize();
32
33        // If an user implements its own ObjectAdapter, notification corresponding to data
34        // updates can be batched e.g. remove, add might be followed by notifyRemove, notifyAdd.
35        // But underlying data would have changed during the notifyRemove call by the previous add
36        // operation. To handle this case, we use QueueBasedDataObserver which forces
37        // recyclerview to do a full data refresh after each update operation.
38        if (adapter.isImmediateNotifySupported()) {
39            mDataObserver = new SimpleDataObserver();
40        } else {
41            mDataObserver = new QueueBasedDataObserver();
42        }
43        attach();
44    }
45
46    void detach() {
47        mAdapter.unregisterObserver(mDataObserver);
48    }
49
50    void attach() {
51        initialize();
52        mAdapter.registerObserver(mDataObserver);
53    }
54
55    void initialize() {
56        mLastVisibleRowIndex = -1;
57        int i = mAdapter.size() - 1;
58        while (i >= 0) {
59            Row item = (Row) mAdapter.get(i);
60            if (item.isRenderedAsRowView()) {
61                mLastVisibleRowIndex = i;
62                break;
63            }
64            i--;
65        }
66    }
67
68    @Override
69    public int size() {
70        return mLastVisibleRowIndex + 1;
71    }
72
73    @Override
74    public Object get(int index) {
75        return mAdapter.get(index);
76    }
77
78    void doNotify(int eventType, int positionStart, int itemCount) {
79        switch (eventType) {
80            case ON_ITEM_RANGE_CHANGED:
81                notifyItemRangeChanged(positionStart, itemCount);
82                break;
83            case ON_ITEM_RANGE_INSERTED:
84                notifyItemRangeInserted(positionStart, itemCount);
85                break;
86            case ON_ITEM_RANGE_REMOVED:
87                notifyItemRangeRemoved(positionStart, itemCount);
88                break;
89            case ON_CHANGED:
90                notifyChanged();
91                break;
92            default:
93                throw new IllegalArgumentException("Invalid event type " + eventType);
94        }
95    }
96
97    private class SimpleDataObserver extends DataObserver {
98
99        SimpleDataObserver() {
100        }
101
102        @Override
103        public void onItemRangeChanged(int positionStart, int itemCount) {
104            if (positionStart <= mLastVisibleRowIndex) {
105                onEventFired(ON_ITEM_RANGE_CHANGED, positionStart,
106                        Math.min(itemCount, mLastVisibleRowIndex - positionStart + 1));
107            }
108        }
109
110        @Override
111        public void onItemRangeInserted(int positionStart, int itemCount) {
112            if (positionStart <= mLastVisibleRowIndex) {
113                mLastVisibleRowIndex += itemCount;
114                onEventFired(ON_ITEM_RANGE_INSERTED, positionStart, itemCount);
115                return;
116            }
117
118            int lastVisibleRowIndex = mLastVisibleRowIndex;
119            initialize();
120            if (mLastVisibleRowIndex > lastVisibleRowIndex) {
121                int totalItems = mLastVisibleRowIndex - lastVisibleRowIndex;
122                onEventFired(ON_ITEM_RANGE_INSERTED, lastVisibleRowIndex + 1, totalItems);
123            }
124        }
125
126        @Override
127        public void onItemRangeRemoved(int positionStart, int itemCount) {
128            if (positionStart + itemCount - 1 < mLastVisibleRowIndex) {
129                mLastVisibleRowIndex -= itemCount;
130                onEventFired(ON_ITEM_RANGE_REMOVED, positionStart, itemCount);
131                return;
132            }
133
134            int lastVisibleRowIndex = mLastVisibleRowIndex;
135            initialize();
136            int totalItems = lastVisibleRowIndex - mLastVisibleRowIndex;
137            if (totalItems > 0) {
138                onEventFired(ON_ITEM_RANGE_REMOVED,
139                        Math.min(mLastVisibleRowIndex + 1, positionStart),
140                        totalItems);
141            }
142        }
143
144        @Override
145        public void onChanged() {
146            initialize();
147            onEventFired(ON_CHANGED, -1, -1);
148        }
149
150        protected void onEventFired(int eventType, int positionStart, int itemCount) {
151            doNotify(eventType, positionStart, itemCount);
152        }
153    }
154
155
156    /**
157     * When using custom {@link ObjectAdapter}, it's possible that the user may make multiple
158     * changes to the underlying data at once. The notifications about those updates may be
159     * batched and the underlying data would have changed to reflect latest updates as opposed
160     * to intermediate changes. In order to force RecyclerView to refresh the view with access
161     * only to the final data, we call notifyChange().
162     */
163    private class QueueBasedDataObserver extends DataObserver {
164
165        QueueBasedDataObserver() {
166        }
167
168        @Override
169        public void onChanged() {
170            initialize();
171            notifyChanged();
172        }
173    }
174}
175