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 */
14package android.support.v17.leanback.widget;
15
16import android.content.Context;
17import android.graphics.Bitmap;
18import android.graphics.drawable.BitmapDrawable;
19import android.graphics.drawable.Drawable;
20
21import java.lang.ref.WeakReference;
22import java.util.ArrayList;
23import java.util.List;
24
25/**
26 * An overview {@link Row} for a details fragment. This row consists of an image, a
27 * description view, and optionally a series of {@link Action}s that can be taken for
28 * the item.
29 *
30 * <h3>Actions</h3>
31 * Application uses {@link #setActionsAdapter(ObjectAdapter)} to set actions on the overview
32 * row.  {@link SparseArrayObjectAdapter} is recommended for easily updating actions while
33 * maintaining the order.  The application can add or remove actions on the UI thread after the
34 * row is bound to a view.
35 *
36 * <h3>Updating main item</h3>
37 * After the row is bound to a view, the application may call {@link #setItem(Object)}
38 * on UI thread and the view will be updated.
39 *
40 * <h3>Updating image</h3>
41 * After the row is bound to view, the application may change the image by calling {@link
42 * #setImageBitmap(Context, Bitmap)} or {@link #setImageDrawable(Drawable)} on the UI thread,
43 * and the view will be updated.
44 */
45public class DetailsOverviewRow extends Row {
46
47    /**
48     * Listener for changes of DetailsOverviewRow.
49     */
50    public static class Listener {
51
52        /**
53         * Called when DetailsOverviewRow has changed image drawable.
54         */
55        public void onImageDrawableChanged(DetailsOverviewRow row) {
56        }
57
58        /**
59         * Called when DetailsOverviewRow has changed main item.
60         */
61        public void onItemChanged(DetailsOverviewRow row) {
62        }
63
64        /**
65         * Called when DetailsOverviewRow has changed actions adapter.
66         */
67        public void onActionsAdapterChanged(DetailsOverviewRow row) {
68        }
69    }
70
71    private Object mItem;
72    private Drawable mImageDrawable;
73    private boolean mImageScaleUpAllowed = true;
74    private ArrayList<WeakReference<Listener>> mListeners;
75    private PresenterSelector mDefaultActionPresenter = new ActionPresenterSelector();
76    private ObjectAdapter mActionsAdapter = new ArrayObjectAdapter(mDefaultActionPresenter);
77
78    /**
79     * Constructor for a DetailsOverviewRow.
80     *
81     * @param item The main item for the details page.
82     */
83    public DetailsOverviewRow(Object item) {
84        super(null);
85        mItem = item;
86        verify();
87    }
88
89    /**
90     * Adds listener for the details page.
91     */
92    final void addListener(Listener listener) {
93        if (mListeners == null) {
94            mListeners = new ArrayList<WeakReference<Listener>>();
95        } else {
96            for (int i = 0; i < mListeners.size();) {
97                Listener l = mListeners.get(i).get();
98                if (l == null) {
99                    mListeners.remove(i);
100                } else {
101                    if (l == listener) {
102                        return;
103                    }
104                    i++;
105                }
106            }
107        }
108        mListeners.add(new WeakReference<Listener>(listener));
109    }
110
111    /**
112     * Removes listener of the details page.
113     */
114    final void removeListener(Listener listener) {
115        if (mListeners != null) {
116            for (int i = 0; i < mListeners.size();) {
117                Listener l = mListeners.get(i).get();
118                if (l == null) {
119                    mListeners.remove(i);
120                } else {
121                    if (l == listener) {
122                        mListeners.remove(i);
123                        return;
124                    }
125                    i++;
126                }
127            }
128        }
129    }
130
131    /**
132     * Notifies listeners for main item change on UI thread.
133     */
134    final void notifyItemChanged() {
135        if (mListeners != null) {
136            for (int i = 0; i < mListeners.size();) {
137                Listener l = mListeners.get(i).get();
138                if (l == null) {
139                    mListeners.remove(i);
140                } else {
141                    l.onItemChanged(this);
142                    i++;
143                }
144            }
145        }
146    }
147
148    /**
149     * Notifies listeners for image related change on UI thread.
150     */
151    final void notifyImageDrawableChanged() {
152        if (mListeners != null) {
153            for (int i = 0; i < mListeners.size();) {
154                Listener l = mListeners.get(i).get();
155                if (l == null) {
156                    mListeners.remove(i);
157                } else {
158                    l.onImageDrawableChanged(this);
159                    i++;
160                }
161            }
162        }
163    }
164
165    /**
166     * Notifies listeners for actions adapter changed on UI thread.
167     */
168    final void notifyActionsAdapterChanged() {
169        if (mListeners != null) {
170            for (int i = 0; i < mListeners.size();) {
171                Listener l = mListeners.get(i).get();
172                if (l == null) {
173                    mListeners.remove(i);
174                } else {
175                    l.onActionsAdapterChanged(this);
176                    i++;
177                }
178            }
179        }
180    }
181
182    /**
183     * Returns the main item for the details page.
184     */
185    public final Object getItem() {
186        return mItem;
187    }
188
189    /**
190     * Sets the main item for the details page.  Must be called on UI thread after
191     * row is bound to view.
192     */
193    public final void setItem(Object item) {
194        if (item != mItem) {
195            mItem = item;
196            notifyItemChanged();
197        }
198    }
199
200    /**
201     * Sets a drawable as the image of this details overview.  Must be called on UI thread
202     * after row is bound to view.
203     *
204     * @param drawable The drawable to set.
205     */
206    public final void setImageDrawable(Drawable drawable) {
207        if (mImageDrawable != drawable) {
208            mImageDrawable = drawable;
209            notifyImageDrawableChanged();
210        }
211    }
212
213    /**
214     * Sets a Bitmap as the image of this details overview.  Must be called on UI thread
215     * after row is bound to view.
216     *
217     * @param context The context to retrieve display metrics from.
218     * @param bm The bitmap to set.
219     */
220    public final void setImageBitmap(Context context, Bitmap bm) {
221        mImageDrawable = new BitmapDrawable(context.getResources(), bm);
222        notifyImageDrawableChanged();
223    }
224
225    /**
226     * Returns the image drawable of this details overview.
227     *
228     * @return The overview's image drawable, or null if no drawable has been
229     *         assigned.
230     */
231    public final Drawable getImageDrawable() {
232        return mImageDrawable;
233    }
234
235    /**
236     * Allows or disallows scaling up of images.
237     * Images will always be scaled down if necessary.  Must be called on UI thread
238     * after row is bound to view.
239     */
240    public void setImageScaleUpAllowed(boolean allowed) {
241        if (allowed != mImageScaleUpAllowed) {
242            mImageScaleUpAllowed = allowed;
243            notifyImageDrawableChanged();
244        }
245    }
246
247    /**
248     * Returns true if the image may be scaled up; false otherwise.
249     */
250    public boolean isImageScaleUpAllowed() {
251        return mImageScaleUpAllowed;
252    }
253
254    /**
255     * Returns the actions adapter.  Throws ClassCastException if the current
256     * actions adapter is not an instance of {@link ArrayObjectAdapter}.
257     */
258    private ArrayObjectAdapter getArrayObjectAdapter() {
259        return (ArrayObjectAdapter) mActionsAdapter;
260    }
261
262    /**
263     * Adds an Action to the overview. It will throw ClassCastException if the current actions
264     * adapter is not an instance of {@link ArrayObjectAdapter}. Must be called on the UI thread.
265     *
266     * @param action The Action to add.
267     * @deprecated Use {@link #setActionsAdapter(ObjectAdapter)} and {@link #getActionsAdapter()}
268     */
269    @Deprecated
270    public final void addAction(Action action) {
271        getArrayObjectAdapter().add(action);
272    }
273
274    /**
275     * Adds an Action to the overview at the specified position. It will throw ClassCastException if
276     * current actions adapter is not an instance of f{@link ArrayObjectAdapter}. Must be called
277     * on the UI thread.
278     *
279     * @param pos The position to insert the Action.
280     * @param action The Action to add.
281     * @deprecated Use {@link #setActionsAdapter(ObjectAdapter)} and {@link #getActionsAdapter()}
282     */
283    @Deprecated
284    public final void addAction(int pos, Action action) {
285        getArrayObjectAdapter().add(pos, action);
286    }
287
288    /**
289     * Removes the given Action from the overview. It will throw ClassCastException if current
290     * actions adapter is not {@link ArrayObjectAdapter}. Must be called on UI thread.
291     *
292     * @param action The Action to remove.
293     * @return true if the overview contained the specified Action.
294     * @deprecated Use {@link #setActionsAdapter(ObjectAdapter)} and {@link #getActionsAdapter()}
295     */
296    @Deprecated
297    public final boolean removeAction(Action action) {
298        return getArrayObjectAdapter().remove(action);
299    }
300
301    /**
302     * Returns a read-only view of the list of Actions of this details overview. It will throw
303     * ClassCastException if current actions adapter is not {@link ArrayObjectAdapter}. Must be
304     * called on UI thread.
305     *
306     * @return An unmodifiable view of the list of Actions.
307     * @deprecated Use {@link #setActionsAdapter(ObjectAdapter)} and {@link #getActionsAdapter()}
308     */
309    @Deprecated
310    public final List<Action> getActions() {
311        return getArrayObjectAdapter().unmodifiableList();
312    }
313
314    /**
315     * Returns the {@link ObjectAdapter} for actions.
316     */
317    public final ObjectAdapter getActionsAdapter() {
318        return mActionsAdapter;
319    }
320
321    /**
322     * Sets the {@link ObjectAdapter} for actions.  A default {@link PresenterSelector} will be
323     * attached to the adapter if it doesn't have one.
324     *
325     * @param adapter  Adapter for actions.
326     */
327    public final void setActionsAdapter(ObjectAdapter adapter) {
328        if (adapter != mActionsAdapter) {
329            mActionsAdapter = adapter;
330            if (mActionsAdapter.getPresenterSelector() == null) {
331                mActionsAdapter.setPresenterSelector(mDefaultActionPresenter);
332            }
333            notifyActionsAdapterChanged();
334        }
335    }
336
337    /**
338     * Returns the Action associated with the given keycode, or null if no associated action exists.
339     */
340    public Action getActionForKeyCode(int keyCode) {
341        ObjectAdapter adapter = getActionsAdapter();
342        if (adapter != null) {
343            for (int i = 0; i < adapter.size(); i++) {
344                Action action = (Action) adapter.get(i);
345                if (action.respondsToKeyCode(keyCode)) {
346                    return action;
347                }
348            }
349        }
350        return null;
351    }
352
353    private void verify() {
354        if (mItem == null) {
355            throw new IllegalArgumentException("Object cannot be null");
356        }
357    }
358}
359