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.widget;
15
16import android.content.Context;
17import android.database.DataSetObserver;
18import android.media.AudioManager;
19import android.support.v17.leanback.R;
20import android.support.v7.widget.RecyclerView;
21import android.support.v7.widget.RecyclerView.ViewHolder;
22import android.util.Log;
23import android.view.KeyEvent;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.ViewGroup;
27import android.view.ViewParent;
28import android.view.inputmethod.EditorInfo;
29import android.view.inputmethod.InputMethodManager;
30import android.widget.AdapterView.OnItemSelectedListener;
31import android.widget.EditText;
32import android.widget.ImageView;
33import android.widget.TextView;
34import android.widget.TextView.OnEditorActionListener;
35
36import java.util.ArrayList;
37import java.util.List;
38
39/**
40 * GuidedActionAdapter instantiates views for guided actions, and manages their interactions.
41 * Presentation (view creation and state animation) is delegated to a {@link
42 * GuidedActionsStylist}, while clients are notified of interactions via
43 * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}.
44 * @hide
45 */
46public class GuidedActionAdapter extends RecyclerView.Adapter {
47    private static final String TAG = "GuidedActionAdapter";
48    private static final boolean DEBUG = false;
49
50    private static final String TAG_EDIT = "EditableAction";
51    private static final boolean DEBUG_EDIT = false;
52
53    /**
54     * Object listening for click events within a {@link GuidedActionAdapter}.
55     */
56    public interface ClickListener {
57
58        /**
59         * Called when the user clicks on an action.
60         */
61        public void onGuidedActionClicked(GuidedAction action);
62
63    }
64
65    /**
66     * Object listening for focus events within a {@link GuidedActionAdapter}.
67     */
68    public interface FocusListener {
69
70        /**
71         * Called when the user focuses on an action.
72         */
73        public void onGuidedActionFocused(GuidedAction action);
74    }
75
76    /**
77     * Object listening for edit events within a {@link GuidedActionAdapter}.
78     */
79    public interface EditListener {
80
81        /**
82         * Called when the user exits edit mode on an action.
83         */
84        public void onGuidedActionEditCanceled(GuidedAction action);
85
86        /**
87         * Called when the user exits edit mode on an action and process confirm button in IME.
88         */
89        public long onGuidedActionEditedAndProceed(GuidedAction action);
90
91        /**
92         * Called when Ime Open
93         */
94        public void onImeOpen();
95
96        /**
97         * Called when Ime Close
98         */
99        public void onImeClose();
100    }
101
102    private final boolean mIsSubAdapter;
103    private final ActionOnKeyListener mActionOnKeyListener;
104    private final ActionOnFocusListener mActionOnFocusListener;
105    private final ActionEditListener mActionEditListener;
106    private final List<GuidedAction> mActions;
107    private ClickListener mClickListener;
108    private final GuidedActionsStylist mStylist;
109    private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
110        @Override
111        public void onClick(View v) {
112            if (v != null && v.getWindowToken() != null && getRecyclerView() != null) {
113                GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
114                        getRecyclerView().getChildViewHolder(v);
115                GuidedAction action = avh.getAction();
116                if (action.hasTextEditable()) {
117                    if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme by click");
118                    mGroup.openIme(GuidedActionAdapter.this, avh);
119                } else if (action.hasEditableActivatorView()) {
120                    if (DEBUG_EDIT) Log.v(TAG_EDIT, "toggle editing mode by click");
121                    getGuidedActionsStylist().setEditingMode(avh, avh.getAction(),
122                            !avh.isInEditingActivatorView());
123                } else {
124                    handleCheckedActions(avh);
125                    if (action.isEnabled() && !action.infoOnly()) {
126                        performOnActionClick(avh);
127                    }
128                }
129            }
130        }
131    };
132    GuidedActionAdapterGroup mGroup;
133
134    /**
135     * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and
136     * focus listeners, and the given presenter.
137     * @param actions The list of guided actions this adapter will manage.
138     * @param focusListener The focus listener for items in this adapter.
139     * @param presenter The presenter that will manage the display of items in this adapter.
140     */
141    public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener,
142            FocusListener focusListener, GuidedActionsStylist presenter, boolean isSubAdapter) {
143        super();
144        mActions = actions == null ? new ArrayList<GuidedAction>() :
145                new ArrayList<GuidedAction>(actions);
146        mClickListener = clickListener;
147        mStylist = presenter;
148        mActionOnKeyListener = new ActionOnKeyListener();
149        mActionOnFocusListener = new ActionOnFocusListener(focusListener);
150        mActionEditListener = new ActionEditListener();
151        mIsSubAdapter = isSubAdapter;
152    }
153
154    /**
155     * Sets the list of actions managed by this adapter.
156     * @param actions The list of actions to be managed.
157     */
158    public void setActions(List<GuidedAction> actions) {
159        mActionOnFocusListener.unFocus();
160        mActions.clear();
161        mActions.addAll(actions);
162        notifyDataSetChanged();
163    }
164
165    /**
166     * Returns the count of actions managed by this adapter.
167     * @return The count of actions managed by this adapter.
168     */
169    public int getCount() {
170        return mActions.size();
171    }
172
173    /**
174     * Returns the GuidedAction at the given position in the managed list.
175     * @param position The position of the desired GuidedAction.
176     * @return The GuidedAction at the given position.
177     */
178    public GuidedAction getItem(int position) {
179        return mActions.get(position);
180    }
181
182    /**
183     * Return index of action in array
184     * @param action Action to search index.
185     * @return Index of Action in array.
186     */
187    public int indexOf(GuidedAction action) {
188        return mActions.indexOf(action);
189    }
190
191    /**
192     * @return GuidedActionsStylist used to build the actions list UI.
193     */
194    public GuidedActionsStylist getGuidedActionsStylist() {
195        return mStylist;
196    }
197
198    /**
199     * Sets the click listener for items managed by this adapter.
200     * @param clickListener The click listener for this adapter.
201     */
202    public void setClickListener(ClickListener clickListener) {
203        mClickListener = clickListener;
204    }
205
206    /**
207     * Sets the focus listener for items managed by this adapter.
208     * @param focusListener The focus listener for this adapter.
209     */
210    public void setFocusListener(FocusListener focusListener) {
211        mActionOnFocusListener.setFocusListener(focusListener);
212    }
213
214    /**
215     * Used for serialization only.
216     * @hide
217     */
218    public List<GuidedAction> getActions() {
219        return new ArrayList<GuidedAction>(mActions);
220    }
221
222    /**
223     * {@inheritDoc}
224     */
225    @Override
226    public int getItemViewType(int position) {
227        return mStylist.getItemViewType(mActions.get(position));
228    }
229
230    private RecyclerView getRecyclerView() {
231        return mIsSubAdapter ? mStylist.getSubActionsGridView() : mStylist.getActionsGridView();
232    }
233
234    /**
235     * {@inheritDoc}
236     */
237    @Override
238    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
239        GuidedActionsStylist.ViewHolder vh = mStylist.onCreateViewHolder(parent, viewType);
240        View v = vh.itemView;
241        v.setOnKeyListener(mActionOnKeyListener);
242        v.setOnClickListener(mOnClickListener);
243        v.setOnFocusChangeListener(mActionOnFocusListener);
244
245        setupListeners(vh.getEditableTitleView());
246        setupListeners(vh.getEditableDescriptionView());
247
248        return vh;
249    }
250
251    private void setupListeners(EditText edit) {
252        if (edit != null) {
253            edit.setPrivateImeOptions("EscapeNorth=1;");
254            edit.setOnEditorActionListener(mActionEditListener);
255            if (edit instanceof ImeKeyMonitor) {
256                ImeKeyMonitor monitor = (ImeKeyMonitor)edit;
257                monitor.setImeKeyListener(mActionEditListener);
258            }
259        }
260    }
261
262    /**
263     * {@inheritDoc}
264     */
265    @Override
266    public void onBindViewHolder(ViewHolder holder, int position) {
267        if (position >= mActions.size()) {
268            return;
269        }
270        final GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)holder;
271        GuidedAction action = mActions.get(position);
272        mStylist.onBindViewHolder(avh, action);
273    }
274
275    /**
276     * {@inheritDoc}
277     */
278    @Override
279    public int getItemCount() {
280        return mActions.size();
281    }
282
283    private class ActionOnFocusListener implements View.OnFocusChangeListener {
284
285        private FocusListener mFocusListener;
286        private View mSelectedView;
287
288        ActionOnFocusListener(FocusListener focusListener) {
289            mFocusListener = focusListener;
290        }
291
292        public void setFocusListener(FocusListener focusListener) {
293            mFocusListener = focusListener;
294        }
295
296        public void unFocus() {
297            if (mSelectedView != null && getRecyclerView() != null) {
298                ViewHolder vh = getRecyclerView().getChildViewHolder(mSelectedView);
299                if (vh != null) {
300                    GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)vh;
301                    mStylist.onAnimateItemFocused(avh, false);
302                } else {
303                    Log.w(TAG, "RecyclerView returned null view holder",
304                            new Throwable());
305                }
306            }
307        }
308
309        @Override
310        public void onFocusChange(View v, boolean hasFocus) {
311            if (getRecyclerView() == null) {
312                return;
313            }
314            GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
315                    getRecyclerView().getChildViewHolder(v);
316            if (hasFocus) {
317                mSelectedView = v;
318                if (mFocusListener != null) {
319                    // We still call onGuidedActionFocused so that listeners can clear
320                    // state if they want.
321                    mFocusListener.onGuidedActionFocused(avh.getAction());
322                }
323            } else {
324                if (mSelectedView == v) {
325                    mStylist.onAnimateItemPressedCancelled(avh);
326                    mSelectedView = null;
327                }
328            }
329            mStylist.onAnimateItemFocused(avh, hasFocus);
330        }
331    }
332
333    public GuidedActionsStylist.ViewHolder findSubChildViewHolder(View v) {
334        // Needed because RecyclerView.getChildViewHolder does not traverse the hierarchy
335        if (getRecyclerView() == null) {
336            return null;
337        }
338        GuidedActionsStylist.ViewHolder result = null;
339        ViewParent parent = v.getParent();
340        while (parent != getRecyclerView() && parent != null && v != null) {
341            v = (View)parent;
342            parent = parent.getParent();
343        }
344        if (parent != null && v != null) {
345            result = (GuidedActionsStylist.ViewHolder)getRecyclerView().getChildViewHolder(v);
346        }
347        return result;
348    }
349
350    public void handleCheckedActions(GuidedActionsStylist.ViewHolder avh) {
351        GuidedAction action = avh.getAction();
352        int actionCheckSetId = action.getCheckSetId();
353        if (getRecyclerView() != null && actionCheckSetId != GuidedAction.NO_CHECK_SET) {
354            // Find any actions that are checked and are in the same group
355            // as the selected action. Fade their checkmarks out.
356            if (actionCheckSetId != GuidedAction.CHECKBOX_CHECK_SET_ID) {
357                for (int i = 0, size = mActions.size(); i < size; i++) {
358                    GuidedAction a = mActions.get(i);
359                    if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
360                        a.setChecked(false);
361                        GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder)
362                                getRecyclerView().findViewHolderForPosition(i);
363                        if (vh != null) {
364                            mStylist.onAnimateItemChecked(vh, false);
365                        }
366                    }
367                }
368            }
369
370            // If we we'ren't already checked, fade our checkmark in.
371            if (!action.isChecked()) {
372                action.setChecked(true);
373                mStylist.onAnimateItemChecked(avh, true);
374            } else {
375                if (actionCheckSetId == GuidedAction.CHECKBOX_CHECK_SET_ID) {
376                    action.setChecked(false);
377                    mStylist.onAnimateItemChecked(avh, false);
378                }
379            }
380        }
381    }
382
383    public void performOnActionClick(GuidedActionsStylist.ViewHolder avh) {
384        if (mClickListener != null) {
385            mClickListener.onGuidedActionClicked(avh.getAction());
386        }
387    }
388
389    private class ActionOnKeyListener implements View.OnKeyListener {
390
391        private boolean mKeyPressed = false;
392
393        /**
394         * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
395         */
396        @Override
397        public boolean onKey(View v, int keyCode, KeyEvent event) {
398            if (v == null || event == null || getRecyclerView() == null) {
399                return false;
400            }
401            boolean handled = false;
402            switch (keyCode) {
403                case KeyEvent.KEYCODE_DPAD_CENTER:
404                case KeyEvent.KEYCODE_NUMPAD_ENTER:
405                case KeyEvent.KEYCODE_BUTTON_X:
406                case KeyEvent.KEYCODE_BUTTON_Y:
407                case KeyEvent.KEYCODE_ENTER:
408
409                    GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
410                            getRecyclerView().getChildViewHolder(v);
411                    GuidedAction action = avh.getAction();
412
413                    if (!action.isEnabled() || action.infoOnly()) {
414                        if (event.getAction() == KeyEvent.ACTION_DOWN) {
415                            // TODO: requires API 19
416                            //playSound(v, AudioManager.FX_KEYPRESS_INVALID);
417                        }
418                        return true;
419                    }
420
421                    switch (event.getAction()) {
422                        case KeyEvent.ACTION_DOWN:
423                            if (DEBUG) {
424                                Log.d(TAG, "Enter Key down");
425                            }
426                            if (!mKeyPressed) {
427                                mKeyPressed = true;
428                                mStylist.onAnimateItemPressed(avh, mKeyPressed);
429                            }
430                            break;
431                        case KeyEvent.ACTION_UP:
432                            if (DEBUG) {
433                                Log.d(TAG, "Enter Key up");
434                            }
435                            // Sometimes we are losing ACTION_DOWN for the first ENTER after pressed
436                            // Escape in IME.
437                            if (mKeyPressed) {
438                                mKeyPressed = false;
439                                mStylist.onAnimateItemPressed(avh, mKeyPressed);
440                            }
441                            break;
442                        default:
443                            break;
444                    }
445                    break;
446                default:
447                    break;
448            }
449            return handled;
450        }
451
452    }
453
454    private class ActionEditListener implements OnEditorActionListener,
455            ImeKeyMonitor.ImeKeyListener {
456
457        @Override
458        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
459            if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME action: " + actionId);
460            boolean handled = false;
461            if (actionId == EditorInfo.IME_ACTION_NEXT ||
462                actionId == EditorInfo.IME_ACTION_DONE) {
463                mGroup.fillAndGoNext(GuidedActionAdapter.this, v);
464                handled = true;
465            } else if (actionId == EditorInfo.IME_ACTION_NONE) {
466                if (DEBUG_EDIT) Log.v(TAG_EDIT, "closeIme escape north");
467                // Escape north handling: stay on current item, but close editor
468                handled = true;
469                mGroup.fillAndStay(GuidedActionAdapter.this, v);
470            }
471            return handled;
472        }
473
474        @Override
475        public boolean onKeyPreIme(EditText editText, int keyCode, KeyEvent event) {
476            if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME key: " + keyCode);
477            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
478                mGroup.fillAndStay(GuidedActionAdapter.this, editText);
479                return true;
480            } else if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() ==
481                    KeyEvent.ACTION_UP) {
482                mGroup.fillAndGoNext(GuidedActionAdapter.this, editText);
483                return true;
484            }
485            return false;
486        }
487
488    }
489
490}
491