1/*
2 * Copyright (C) 2015 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.app;
15
16import android.content.Context;
17import android.database.DataSetObserver;
18import android.media.AudioManager;
19import android.support.v17.leanback.R;
20import android.support.v17.leanback.widget.GuidedAction;
21import android.support.v17.leanback.widget.GuidedActionsStylist;
22import android.support.v7.widget.RecyclerView;
23import android.support.v7.widget.RecyclerView.ViewHolder;
24import android.util.Log;
25import android.view.KeyEvent;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.ViewGroup;
29import android.widget.AdapterView.OnItemSelectedListener;
30import android.widget.ImageView;
31import android.widget.TextView;
32
33import java.util.ArrayList;
34import java.util.List;
35
36/**
37 * GuidedActionAdapter instantiates views for guided actions, and manages their interactions.
38 * Presentation (view creation and state animation) is delegated to a {@link
39 * GuidedActionsStylist}, while clients are notified of interactions via
40 * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}.
41 */
42class GuidedActionAdapter extends RecyclerView.Adapter {
43    private static final String TAG = "GuidedActionAdapter";
44    private static final boolean DEBUG = false;
45
46    /**
47     * Object listening for click events within a {@link GuidedActionAdapter}.
48     */
49    public interface ClickListener {
50
51        /**
52         * Called when the user clicks on an action.
53         */
54        public void onGuidedActionClicked(GuidedAction action);
55    }
56
57    /**
58     * Object listening for focus events within a {@link GuidedActionAdapter}.
59     */
60    public interface FocusListener {
61
62        /**
63         * Called when the user focuses on an action.
64         */
65        public void onGuidedActionFocused(GuidedAction action);
66    }
67
68    /**
69     * View holder containing a {@link GuidedAction}.
70     */
71    private static class ActionViewHolder extends ViewHolder {
72
73        private final GuidedActionsStylist.ViewHolder mStylistViewHolder;
74        private GuidedAction mAction;
75
76        /**
77         * Constructs a view holder that can be associated with a GuidedAction.
78         */
79        public ActionViewHolder(View v, GuidedActionsStylist.ViewHolder subViewHolder) {
80            super(v);
81            mStylistViewHolder = subViewHolder;
82        }
83
84        /**
85         * Retrieves the action associated with this view holder.
86         * @return The GuidedAction associated with this view holder.
87         */
88        public GuidedAction getAction() {
89            return mAction;
90        }
91
92        /**
93         * Sets the action associated with this view holder.
94         * @param action The GuidedAction associated with this view holder.
95         */
96        public void setAction(GuidedAction action) {
97            mAction = action;
98        }
99    }
100
101    private RecyclerView mRecyclerView;
102    private final ActionOnKeyListener mActionOnKeyListener;
103    private final ActionOnFocusListener mActionOnFocusListener;
104    private final List<GuidedAction> mActions;
105    private ClickListener mClickListener;
106    private GuidedActionsStylist mStylist;
107    private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
108        @Override
109        public void onClick(View v) {
110            if (v != null && v.getWindowToken() != null && mClickListener != null) {
111                ActionViewHolder avh = (ActionViewHolder)mRecyclerView.getChildViewHolder(v);
112                GuidedAction action = avh.getAction();
113                if (action.isEnabled() && !action.infoOnly()) {
114                    mClickListener.onGuidedActionClicked(action);
115                }
116            }
117        }
118    };
119
120    /**
121     * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and
122     * focus listeners, and the given presenter.
123     * @param actions The list of guided actions this adapter will manage.
124     * @param clickListener The click listener for items in this adapter.
125     * @param focusListener The focus listener for items in this adapter.
126     * @param presenter The presenter that will manage the display of items in this adapter.
127     */
128    public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener,
129            FocusListener focusListener, GuidedActionsStylist presenter) {
130        super();
131        mActions = new ArrayList<GuidedAction>(actions);
132        mClickListener = clickListener;
133        mStylist = presenter;
134        mActionOnKeyListener = new ActionOnKeyListener(clickListener, mActions);
135        mActionOnFocusListener = new ActionOnFocusListener(focusListener);
136    }
137
138    /**
139     * Sets the list of actions managed by this adapter.
140     * @param actions The list of actions to be managed.
141     */
142    public void setActions(List<GuidedAction> actions) {
143        mActionOnFocusListener.unFocus();
144        mActions.clear();
145        mActions.addAll(actions);
146        notifyDataSetChanged();
147    }
148
149    /**
150     * Returns the count of actions managed by this adapter.
151     * @return The count of actions managed by this adapter.
152     */
153    public int getCount() {
154        return mActions.size();
155    }
156
157    /**
158     * Returns the GuidedAction at the given position in the managed list.
159     * @param position The position of the desired GuidedAction.
160     * @return The GuidedAction at the given position.
161     */
162    public GuidedAction getItem(int position) {
163        return mActions.get(position);
164    }
165
166    /**
167     * Sets the click listener for items managed by this adapter.
168     * @param clickListener The click listener for this adapter.
169     */
170    public void setClickListener(ClickListener clickListener) {
171        mClickListener = clickListener;
172        mActionOnKeyListener.setListener(clickListener);
173    }
174
175    /**
176     * Sets the focus listener for items managed by this adapter.
177     * @param focusListener The focus listener for this adapter.
178     */
179    public void setFocusListener(FocusListener focusListener) {
180        mActionOnFocusListener.setFocusListener(focusListener);
181    }
182
183    /**
184     * Used for serialization only.
185     * @hide
186     */
187    public List<GuidedAction> getActions() {
188        return new ArrayList<GuidedAction>(mActions);
189    }
190
191    /**
192     * {@inheritDoc}
193     */
194    @Override
195    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
196        mRecyclerView = recyclerView;
197    }
198
199    /**
200     * {@inheritDoc}
201     */
202    @Override
203    public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
204        mRecyclerView = null;
205    }
206
207    /**
208     * {@inheritDoc}
209     */
210    @Override
211    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
212        GuidedActionsStylist.ViewHolder vh = mStylist.onCreateViewHolder(parent);
213        View v = vh.view;
214        v.setOnKeyListener(mActionOnKeyListener);
215        v.setOnClickListener(mOnClickListener);
216        v.setOnFocusChangeListener(mActionOnFocusListener);
217
218        return new ActionViewHolder(v, vh);
219    }
220
221    /**
222     * {@inheritDoc}
223     */
224    @Override
225    public void onBindViewHolder(ViewHolder holder, int position) {
226        if (position >= mActions.size()) {
227            return;
228        }
229        ActionViewHolder avh = (ActionViewHolder)holder;
230        GuidedAction action = mActions.get(position);
231        avh.setAction(action);
232        mStylist.onBindViewHolder(avh.mStylistViewHolder, action);
233    }
234
235    /**
236     * {@inheritDoc}
237     */
238    @Override
239    public int getItemCount() {
240        return mActions.size();
241    }
242
243    private class ActionOnFocusListener implements View.OnFocusChangeListener {
244
245        private FocusListener mFocusListener;
246        private View mSelectedView;
247
248        ActionOnFocusListener(FocusListener focusListener) {
249            mFocusListener = focusListener;
250        }
251
252        public void setFocusListener(FocusListener focusListener) {
253            mFocusListener = focusListener;
254        }
255
256        public void unFocus() {
257            if (mSelectedView != null) {
258                ViewHolder vh = mRecyclerView.getChildViewHolder(mSelectedView);
259                if (vh != null) {
260                    ActionViewHolder avh = (ActionViewHolder)vh;
261                    mStylist.onAnimateItemFocused(avh.mStylistViewHolder, false);
262                } else {
263                    Log.w(TAG, "RecyclerView returned null view holder",
264                            new Throwable());
265                }
266            }
267        }
268
269        @Override
270        public void onFocusChange(View v, boolean hasFocus) {
271            ActionViewHolder avh = (ActionViewHolder)mRecyclerView.getChildViewHolder(v);
272            mStylist.onAnimateItemFocused(avh.mStylistViewHolder, hasFocus);
273            if (hasFocus) {
274                mSelectedView = v;
275                if (mFocusListener != null) {
276                    // We still call onGuidedActionFocused so that listeners can clear
277                    // state if they want.
278                    mFocusListener.onGuidedActionFocused(avh.getAction());
279                }
280            } else {
281                if (mSelectedView == v) {
282                    mSelectedView = null;
283                }
284            }
285        }
286    }
287
288    private class ActionOnKeyListener implements View.OnKeyListener {
289
290        private final List<GuidedAction> mActions;
291        private boolean mKeyPressed = false;
292        private ClickListener mClickListener;
293
294        public ActionOnKeyListener(ClickListener listener,
295                List<GuidedAction> actions) {
296            mClickListener = listener;
297            mActions = actions;
298        }
299
300        public void setListener(ClickListener listener) {
301            mClickListener = listener;
302        }
303
304        private void playSound(View v, int soundEffect) {
305            if (v.isSoundEffectsEnabled()) {
306                Context ctx = v.getContext();
307                AudioManager manager = (AudioManager)ctx.getSystemService(Context.AUDIO_SERVICE);
308                manager.playSoundEffect(soundEffect);
309            }
310        }
311
312        /**
313         * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
314         */
315        @Override
316        public boolean onKey(View v, int keyCode, KeyEvent event) {
317            if (v == null || event == null) {
318                return false;
319            }
320            boolean handled = false;
321            switch (keyCode) {
322                case KeyEvent.KEYCODE_DPAD_CENTER:
323                case KeyEvent.KEYCODE_NUMPAD_ENTER:
324                case KeyEvent.KEYCODE_BUTTON_X:
325                case KeyEvent.KEYCODE_BUTTON_Y:
326                case KeyEvent.KEYCODE_ENTER:
327
328                    ActionViewHolder avh = (ActionViewHolder)mRecyclerView.getChildViewHolder(v);
329                    GuidedAction action = avh.getAction();
330
331                    if (!action.isEnabled() || action.infoOnly()) {
332                        if (event.getAction() == KeyEvent.ACTION_DOWN) {
333                            // TODO: requires API 19
334                            //playSound(v, AudioManager.FX_KEYPRESS_INVALID);
335                        }
336                        return true;
337                    }
338
339                    switch (event.getAction()) {
340                        case KeyEvent.ACTION_DOWN:
341                            if (!mKeyPressed) {
342                                mKeyPressed = true;
343
344                                playSound(v, AudioManager.FX_KEY_CLICK);
345
346                                if (DEBUG) {
347                                    Log.d(TAG, "Enter Key down");
348                                }
349
350                                mStylist.onAnimateItemPressed(avh.mStylistViewHolder,
351                                        mKeyPressed);
352                                handled = true;
353                            }
354                            break;
355                        case KeyEvent.ACTION_UP:
356                            if (mKeyPressed) {
357                                mKeyPressed = false;
358
359                                if (DEBUG) {
360                                    Log.d(TAG, "Enter Key up");
361                                }
362
363                                mStylist.onAnimateItemPressed(avh.mStylistViewHolder,
364                                            mKeyPressed);
365                                handleCheckedActions(avh, action);
366                                mClickListener.onGuidedActionClicked(action);
367                                handled = true;
368                            }
369                            break;
370                        default:
371                            break;
372                    }
373                    break;
374                default:
375                    break;
376            }
377            return handled;
378        }
379
380        private void handleCheckedActions(ActionViewHolder avh, GuidedAction action) {
381            int actionCheckSetId = action.getCheckSetId();
382            if (actionCheckSetId != GuidedAction.NO_CHECK_SET) {
383                // Find any actions that are checked and are in the same group
384                // as the selected action. Fade their checkmarks out.
385                for (int i = 0, size = mActions.size(); i < size; i++) {
386                    GuidedAction a = mActions.get(i);
387                    if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
388                        a.setChecked(false);
389                        ViewHolder vh = mRecyclerView.findViewHolderForPosition(i);
390                        if (vh != null) {
391                            GuidedActionsStylist.ViewHolder subViewHolder =
392                                    ((ActionViewHolder)vh).mStylistViewHolder;
393                            mStylist.onAnimateItemChecked(subViewHolder, false);
394                        }
395                    }
396                }
397
398                // If we we'ren't already checked, fade our checkmark in.
399                if (!action.isChecked()) {
400                    action.setChecked(true);
401                    mStylist.onAnimateItemChecked(avh.mStylistViewHolder, true);
402                }
403            }
404        }
405    }
406}
407