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 static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
17
18import android.support.annotation.RestrictTo;
19import android.support.v7.widget.RecyclerView;
20import android.support.v7.widget.RecyclerView.ViewHolder;
21import android.util.Log;
22import android.view.KeyEvent;
23import android.view.View;
24import android.view.ViewGroup;
25import android.view.ViewParent;
26import android.view.inputmethod.EditorInfo;
27import android.widget.EditText;
28import android.widget.TextView;
29import android.widget.TextView.OnEditorActionListener;
30
31import java.util.ArrayList;
32import java.util.List;
33
34/**
35 * GuidedActionAdapter instantiates views for guided actions, and manages their interactions.
36 * Presentation (view creation and state animation) is delegated to a {@link
37 * GuidedActionsStylist}, while clients are notified of interactions via
38 * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}.
39 * @hide
40 */
41@RestrictTo(LIBRARY_GROUP)
42public class GuidedActionAdapter extends RecyclerView.Adapter {
43    static final String TAG = "GuidedActionAdapter";
44    static final boolean DEBUG = false;
45
46    static final String TAG_EDIT = "EditableAction";
47    static final boolean DEBUG_EDIT = false;
48
49    /**
50     * Object listening for click events within a {@link GuidedActionAdapter}.
51     */
52    public interface ClickListener {
53
54        /**
55         * Called when the user clicks on an action.
56         */
57        void onGuidedActionClicked(GuidedAction action);
58
59    }
60
61    /**
62     * Object listening for focus events within a {@link GuidedActionAdapter}.
63     */
64    public interface FocusListener {
65
66        /**
67         * Called when the user focuses on an action.
68         */
69        void onGuidedActionFocused(GuidedAction action);
70    }
71
72    /**
73     * Object listening for edit events within a {@link GuidedActionAdapter}.
74     */
75    public interface EditListener {
76
77        /**
78         * Called when the user exits edit mode on an action.
79         */
80        void onGuidedActionEditCanceled(GuidedAction action);
81
82        /**
83         * Called when the user exits edit mode on an action and process confirm button in IME.
84         */
85        long onGuidedActionEditedAndProceed(GuidedAction action);
86
87        /**
88         * Called when Ime Open
89         */
90        void onImeOpen();
91
92        /**
93         * Called when Ime Close
94         */
95        void onImeClose();
96    }
97
98    private final boolean mIsSubAdapter;
99    private final ActionOnKeyListener mActionOnKeyListener;
100    private final ActionOnFocusListener mActionOnFocusListener;
101    private final ActionEditListener mActionEditListener;
102    private final List<GuidedAction> mActions;
103    private ClickListener mClickListener;
104    final GuidedActionsStylist mStylist;
105    GuidedActionAdapterGroup mGroup;
106
107    private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
108        @Override
109        public void onClick(View v) {
110            if (v != null && v.getWindowToken() != null && getRecyclerView() != null) {
111                GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
112                        getRecyclerView().getChildViewHolder(v);
113                GuidedAction action = avh.getAction();
114                if (action.hasTextEditable()) {
115                    if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme by click");
116                    mGroup.openIme(GuidedActionAdapter.this, avh);
117                } else if (action.hasEditableActivatorView()) {
118                    if (DEBUG_EDIT) Log.v(TAG_EDIT, "toggle editing mode by click");
119                    performOnActionClick(avh);
120                } else {
121                    handleCheckedActions(avh);
122                    if (action.isEnabled() && !action.infoOnly()) {
123                        performOnActionClick(avh);
124                    }
125                }
126            }
127        }
128    };
129
130    /**
131     * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and
132     * focus listeners, and the given presenter.
133     * @param actions The list of guided actions this adapter will manage.
134     * @param focusListener The focus listener for items in this adapter.
135     * @param presenter The presenter that will manage the display of items in this adapter.
136     */
137    public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener,
138            FocusListener focusListener, GuidedActionsStylist presenter, boolean isSubAdapter) {
139        super();
140        mActions = actions == null ? new ArrayList<GuidedAction>() :
141                new ArrayList<GuidedAction>(actions);
142        mClickListener = clickListener;
143        mStylist = presenter;
144        mActionOnKeyListener = new ActionOnKeyListener();
145        mActionOnFocusListener = new ActionOnFocusListener(focusListener);
146        mActionEditListener = new ActionEditListener();
147        mIsSubAdapter = isSubAdapter;
148    }
149
150    /**
151     * Sets the list of actions managed by this adapter.
152     * @param actions The list of actions to be managed.
153     */
154    public void setActions(List<GuidedAction> actions) {
155        if (!mIsSubAdapter) {
156            mStylist.collapseAction(false);
157        }
158        mActionOnFocusListener.unFocus();
159        mActions.clear();
160        mActions.addAll(actions);
161        notifyDataSetChanged();
162    }
163
164    /**
165     * Returns the count of actions managed by this adapter.
166     * @return The count of actions managed by this adapter.
167     */
168    public int getCount() {
169        return mActions.size();
170    }
171
172    /**
173     * Returns the GuidedAction at the given position in the managed list.
174     * @param position The position of the desired GuidedAction.
175     * @return The GuidedAction at the given position.
176     */
177    public GuidedAction getItem(int position) {
178        return mActions.get(position);
179    }
180
181    /**
182     * Return index of action in array
183     * @param action Action to search index.
184     * @return Index of Action in array.
185     */
186    public int indexOf(GuidedAction action) {
187        return mActions.indexOf(action);
188    }
189
190    /**
191     * @return GuidedActionsStylist used to build the actions list UI.
192     */
193    public GuidedActionsStylist getGuidedActionsStylist() {
194        return mStylist;
195    }
196
197    /**
198     * Sets the click listener for items managed by this adapter.
199     * @param clickListener The click listener for this adapter.
200     */
201    public void setClickListener(ClickListener clickListener) {
202        mClickListener = clickListener;
203    }
204
205    /**
206     * Sets the focus listener for items managed by this adapter.
207     * @param focusListener The focus listener for this adapter.
208     */
209    public void setFocusListener(FocusListener focusListener) {
210        mActionOnFocusListener.setFocusListener(focusListener);
211    }
212
213    /**
214     * Used for serialization only.
215     * @hide
216     */
217    @RestrictTo(LIBRARY_GROUP)
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    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        ActionOnKeyListener() {
394        }
395
396        /**
397         * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
398         */
399        @Override
400        public boolean onKey(View v, int keyCode, KeyEvent event) {
401            if (v == null || event == null || getRecyclerView() == null) {
402                return false;
403            }
404            boolean handled = false;
405            switch (keyCode) {
406                case KeyEvent.KEYCODE_DPAD_CENTER:
407                case KeyEvent.KEYCODE_NUMPAD_ENTER:
408                case KeyEvent.KEYCODE_BUTTON_X:
409                case KeyEvent.KEYCODE_BUTTON_Y:
410                case KeyEvent.KEYCODE_ENTER:
411
412                    GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
413                            getRecyclerView().getChildViewHolder(v);
414                    GuidedAction action = avh.getAction();
415
416                    if (!action.isEnabled() || action.infoOnly()) {
417                        if (event.getAction() == KeyEvent.ACTION_DOWN) {
418                            // TODO: requires API 19
419                            //playSound(v, AudioManager.FX_KEYPRESS_INVALID);
420                        }
421                        return true;
422                    }
423
424                    switch (event.getAction()) {
425                        case KeyEvent.ACTION_DOWN:
426                            if (DEBUG) {
427                                Log.d(TAG, "Enter Key down");
428                            }
429                            if (!mKeyPressed) {
430                                mKeyPressed = true;
431                                mStylist.onAnimateItemPressed(avh, mKeyPressed);
432                            }
433                            break;
434                        case KeyEvent.ACTION_UP:
435                            if (DEBUG) {
436                                Log.d(TAG, "Enter Key up");
437                            }
438                            // Sometimes we are losing ACTION_DOWN for the first ENTER after pressed
439                            // Escape in IME.
440                            if (mKeyPressed) {
441                                mKeyPressed = false;
442                                mStylist.onAnimateItemPressed(avh, mKeyPressed);
443                            }
444                            break;
445                        default:
446                            break;
447                    }
448                    break;
449                default:
450                    break;
451            }
452            return handled;
453        }
454
455    }
456
457    private class ActionEditListener implements OnEditorActionListener,
458            ImeKeyMonitor.ImeKeyListener {
459
460        ActionEditListener() {
461        }
462
463        @Override
464        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
465            if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME action: " + actionId);
466            boolean handled = false;
467            if (actionId == EditorInfo.IME_ACTION_NEXT
468                    || actionId == EditorInfo.IME_ACTION_DONE) {
469                mGroup.fillAndGoNext(GuidedActionAdapter.this, v);
470                handled = true;
471            } else if (actionId == EditorInfo.IME_ACTION_NONE) {
472                if (DEBUG_EDIT) Log.v(TAG_EDIT, "closeIme escape north");
473                // Escape north handling: stay on current item, but close editor
474                handled = true;
475                mGroup.fillAndStay(GuidedActionAdapter.this, v);
476            }
477            return handled;
478        }
479
480        @Override
481        public boolean onKeyPreIme(EditText editText, int keyCode, KeyEvent event) {
482            if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME key: " + keyCode);
483            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
484                mGroup.fillAndStay(GuidedActionAdapter.this, editText);
485                return true;
486            } else if (keyCode == KeyEvent.KEYCODE_ENTER
487                    && event.getAction() == KeyEvent.ACTION_UP) {
488                mGroup.fillAndGoNext(GuidedActionAdapter.this, editText);
489                return true;
490            }
491            return false;
492        }
493
494    }
495
496}
497