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 static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
17 
18 import android.annotation.SuppressLint;
19 import android.database.Observable;
20 
21 import androidx.annotation.RestrictTo;
22 
23 import org.jspecify.annotations.NonNull;
24 import org.jspecify.annotations.Nullable;
25 
26 /**
27  * Base class adapter to be used in leanback activities.  Provides access to a data model and is
28  * decoupled from the presentation of the items via {@link PresenterSelector}.
29  */
30 public abstract class ObjectAdapter {
31 
32     /** Indicates that an id has not been set. */
33     public static final int NO_ID = -1;
34 
35     /**
36      * A DataObserver can be notified when an ObjectAdapter's underlying data
37      * changes. Separate methods provide notifications about different types of
38      * changes.
39      */
40     public static abstract class DataObserver {
41         /**
42          * Called whenever the ObjectAdapter's data has changed in some manner
43          * outside of the set of changes covered by the other range-based change
44          * notification methods.
45          */
onChanged()46         public void onChanged() {
47         }
48 
49         /**
50          * Called when a range of items in the ObjectAdapter has changed. The
51          * basic ordering and structure of the ObjectAdapter has not changed.
52          *
53          * @param positionStart The position of the first item that changed.
54          * @param itemCount     The number of items changed.
55          */
onItemRangeChanged(int positionStart, int itemCount)56         public void onItemRangeChanged(int positionStart, int itemCount) {
57             onChanged();
58         }
59 
60         /**
61          * Called when a range of items in the ObjectAdapter has changed. The
62          * basic ordering and structure of the ObjectAdapter has not changed.
63          *
64          * @param positionStart The position of the first item that changed.
65          * @param itemCount     The number of items changed.
66          * @param payload       Optional parameter, use null to identify a "full" update.
67          */
onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload)68         public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
69             onChanged();
70         }
71 
72         /**
73          * Called when a range of items is inserted into the ObjectAdapter.
74          *
75          * @param positionStart The position of the first inserted item.
76          * @param itemCount     The number of items inserted.
77          */
onItemRangeInserted(int positionStart, int itemCount)78         public void onItemRangeInserted(int positionStart, int itemCount) {
79             onChanged();
80         }
81 
82         /**
83          * Called when an item is moved from one position to another position
84          *
85          * @param fromPosition Previous position of the item.
86          * @param toPosition   New position of the item.
87          */
onItemMoved(int fromPosition, int toPosition)88         public void onItemMoved(int fromPosition, int toPosition) {
89             onChanged();
90         }
91 
92         /**
93          * Called when a range of items is removed from the ObjectAdapter.
94          *
95          * @param positionStart The position of the first removed item.
96          * @param itemCount     The number of items removed.
97          */
onItemRangeRemoved(int positionStart, int itemCount)98         public void onItemRangeRemoved(int positionStart, int itemCount) {
99             onChanged();
100         }
101     }
102 
103     private static final class DataObservable extends Observable<DataObserver> {
104 
DataObservable()105         DataObservable() {
106         }
107 
notifyChanged()108         public void notifyChanged() {
109             for (int i = mObservers.size() - 1; i >= 0; i--) {
110                 mObservers.get(i).onChanged();
111             }
112         }
113 
notifyItemRangeChanged(int positionStart, int itemCount)114         public void notifyItemRangeChanged(int positionStart, int itemCount) {
115             for (int i = mObservers.size() - 1; i >= 0; i--) {
116                 mObservers.get(i).onItemRangeChanged(positionStart, itemCount);
117             }
118         }
119 
notifyItemRangeChanged(int positionStart, int itemCount, Object payload)120         public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
121             for (int i = mObservers.size() - 1; i >= 0; i--) {
122                 mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
123             }
124         }
125 
notifyItemRangeInserted(int positionStart, int itemCount)126         public void notifyItemRangeInserted(int positionStart, int itemCount) {
127             for (int i = mObservers.size() - 1; i >= 0; i--) {
128                 mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
129             }
130         }
131 
notifyItemRangeRemoved(int positionStart, int itemCount)132         public void notifyItemRangeRemoved(int positionStart, int itemCount) {
133             for (int i = mObservers.size() - 1; i >= 0; i--) {
134                 mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
135             }
136         }
137 
notifyItemMoved(int positionStart, int toPosition)138         public void notifyItemMoved(int positionStart, int toPosition) {
139             for (int i = mObservers.size() - 1; i >= 0; i--) {
140                 mObservers.get(i).onItemMoved(positionStart, toPosition);
141             }
142         }
143 
hasObserver()144         boolean hasObserver() {
145             return mObservers.size() > 0;
146         }
147     }
148 
149     private final DataObservable mObservable = new DataObservable();
150     private boolean mHasStableIds;
151     private PresenterSelector mPresenterSelector;
152 
153     /**
154      * Constructs an adapter with the given {@link PresenterSelector}.
155      */
ObjectAdapter(@onNull PresenterSelector presenterSelector)156     public ObjectAdapter(@NonNull PresenterSelector presenterSelector) {
157         setPresenterSelector(presenterSelector);
158     }
159 
160     /**
161      * Constructs an adapter that uses the given {@link Presenter} for all items.
162      */
ObjectAdapter(@onNull Presenter presenter)163     public ObjectAdapter(@NonNull Presenter presenter) {
164         setPresenterSelector(new SinglePresenterSelector(presenter));
165     }
166 
167     /**
168      * Constructs an adapter.
169      */
ObjectAdapter()170     public ObjectAdapter() {
171     }
172 
173     /**
174      * Sets the presenter selector.  May not be null.
175      */
setPresenterSelector(@onNull PresenterSelector presenterSelector)176     public final void setPresenterSelector(@NonNull PresenterSelector presenterSelector) {
177         if (presenterSelector == null) {
178             throw new IllegalArgumentException("Presenter selector must not be null");
179         }
180         final boolean update = (mPresenterSelector != null);
181         final boolean selectorChanged = update && mPresenterSelector != presenterSelector;
182 
183         mPresenterSelector = presenterSelector;
184 
185         if (selectorChanged) {
186             onPresenterSelectorChanged();
187         }
188         if (update) {
189             notifyChanged();
190         }
191     }
192 
193     /**
194      * Called when {@link #setPresenterSelector(PresenterSelector)} is called
195      * and the PresenterSelector differs from the previous one.
196      */
onPresenterSelectorChanged()197     protected void onPresenterSelectorChanged() {
198     }
199 
200     /**
201      * Returns the presenter selector for this ObjectAdapter.
202      */
getPresenterSelector()203     public final @NonNull PresenterSelector getPresenterSelector() {
204         return mPresenterSelector;
205     }
206 
207     /**
208      * Registers a DataObserver for data change notifications.
209      */
registerObserver(@onNull DataObserver observer)210     public final void registerObserver(@NonNull DataObserver observer) {
211         mObservable.registerObserver(observer);
212     }
213 
214     /**
215      * Unregisters a DataObserver for data change notifications.
216      */
unregisterObserver(@onNull DataObserver observer)217     public final void unregisterObserver(@NonNull DataObserver observer) {
218         mObservable.unregisterObserver(observer);
219     }
220 
221     /**
222      */
223     @RestrictTo(LIBRARY_GROUP_PREFIX)
hasObserver()224     public final boolean hasObserver() {
225         return mObservable.hasObserver();
226     }
227 
228     /**
229      * Unregisters all DataObservers for this ObjectAdapter.
230      */
unregisterAllObservers()231     public final void unregisterAllObservers() {
232         mObservable.unregisterAll();
233     }
234 
235     /**
236      * Notifies UI that some items has changed.
237      *
238      * @param positionStart Starting position of the changed items.
239      * @param itemCount     Total number of items that changed.
240      */
notifyItemRangeChanged(int positionStart, int itemCount)241     public final void notifyItemRangeChanged(int positionStart, int itemCount) {
242         mObservable.notifyItemRangeChanged(positionStart, itemCount);
243     }
244 
245     /**
246      * Notifies UI that some items has changed.
247      *
248      * @param positionStart Starting position of the changed items.
249      * @param itemCount     Total number of items that changed.
250      * @param payload       Optional parameter, use null to identify a "full" update.
251      */
notifyItemRangeChanged( int positionStart, int itemCount, @Nullable Object payload )252     public final void notifyItemRangeChanged(
253             int positionStart,
254             int itemCount,
255             @Nullable Object payload
256     ) {
257         mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
258     }
259 
260     /**
261      * Notifies UI that new items has been inserted.
262      *
263      * @param positionStart Position where new items has been inserted.
264      * @param itemCount     Count of the new items has been inserted.
265      */
notifyItemRangeInserted(int positionStart, int itemCount)266     final protected void notifyItemRangeInserted(int positionStart, int itemCount) {
267         mObservable.notifyItemRangeInserted(positionStart, itemCount);
268     }
269 
270     /**
271      * Notifies UI that some items that has been removed.
272      *
273      * @param positionStart Starting position of the removed items.
274      * @param itemCount     Total number of items that has been removed.
275      */
notifyItemRangeRemoved(int positionStart, int itemCount)276     final protected void notifyItemRangeRemoved(int positionStart, int itemCount) {
277         mObservable.notifyItemRangeRemoved(positionStart, itemCount);
278     }
279 
280     /**
281      * Notifies UI that item at fromPosition has been moved to toPosition.
282      *
283      * @param fromPosition Previous position of the item.
284      * @param toPosition   New position of the item.
285      */
notifyItemMoved(int fromPosition, int toPosition)286     protected final void notifyItemMoved(int fromPosition, int toPosition) {
287         mObservable.notifyItemMoved(fromPosition, toPosition);
288     }
289 
290     /**
291      * Notifies UI that the underlying data has changed.
292      */
notifyChanged()293     final protected void notifyChanged() {
294         mObservable.notifyChanged();
295     }
296 
297     /**
298      * Returns true if the item ids are stable across changes to the
299      * underlying data.  When this is true, clients of the ObjectAdapter can use
300      * {@link #getId(int)} to correlate Objects across changes.
301      */
302     @SuppressLint("KotlinPropertyAccess")
hasStableIds()303     public final boolean hasStableIds() {
304         return mHasStableIds;
305     }
306 
307     /**
308      * Sets whether the item ids are stable across changes to the underlying
309      * data.
310      */
setHasStableIds(boolean hasStableIds)311     public final void setHasStableIds(boolean hasStableIds) {
312         boolean changed = mHasStableIds != hasStableIds;
313         mHasStableIds = hasStableIds;
314 
315         if (changed) {
316             onHasStableIdsChanged();
317         }
318     }
319 
320     /**
321      * Called when {@link #setHasStableIds(boolean)} is called and the status
322      * of stable ids has changed.
323      */
onHasStableIdsChanged()324     protected void onHasStableIdsChanged() {
325     }
326 
327     /**
328      * Returns the {@link Presenter} for the given item from the adapter.
329      */
getPresenter(@onNull Object item)330     public final @Nullable Presenter getPresenter(@NonNull Object item) {
331         if (mPresenterSelector == null) {
332             throw new IllegalStateException("Presenter selector must not be null");
333         }
334         return mPresenterSelector.getPresenter(item);
335     }
336 
337     /**
338      * Returns the number of items in the adapter.
339      */
size()340     public abstract int size();
341 
342     /**
343      * Returns the item for the given position.
344      */
get(int position)345     public abstract @Nullable Object get(int position);
346 
347     /**
348      * Returns the id for the given position.
349      */
getId(int position)350     public long getId(int position) {
351         return NO_ID;
352     }
353 
354     /**
355      * Returns true if the adapter pairs each underlying data change with a call to notify and
356      * false otherwise.
357      */
isImmediateNotifySupported()358     public boolean isImmediateNotifySupported() {
359         return false;
360     }
361 }
362