• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  */
14 package android.support.v17.leanback.widget;
15 
16 import android.content.Context;
17 import android.database.DataSetObserver;
18 import android.media.AudioManager;
19 import android.support.v17.leanback.R;
20 import android.support.v7.widget.RecyclerView;
21 import android.support.v7.widget.RecyclerView.ViewHolder;
22 import android.util.Log;
23 import android.view.KeyEvent;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.view.ViewParent;
28 import android.view.inputmethod.EditorInfo;
29 import android.view.inputmethod.InputMethodManager;
30 import android.widget.AdapterView.OnItemSelectedListener;
31 import android.widget.EditText;
32 import android.widget.ImageView;
33 import android.widget.TextView;
34 import android.widget.TextView.OnEditorActionListener;
35 
36 import java.util.ArrayList;
37 import 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  */
46 public 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          */
onGuidedActionClicked(GuidedAction action)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          */
onGuidedActionFocused(GuidedAction action)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          */
onGuidedActionEditCanceled(GuidedAction action)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          */
onGuidedActionEditedAndProceed(GuidedAction action)89         public long onGuidedActionEditedAndProceed(GuidedAction action);
90 
91         /**
92          * Called when Ime Open
93          */
onImeOpen()94         public void onImeOpen();
95 
96         /**
97          * Called when Ime Close
98          */
onImeClose()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      */
GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener, FocusListener focusListener, GuidedActionsStylist presenter, boolean isSubAdapter)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      */
setActions(List<GuidedAction> actions)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      */
getCount()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      */
getItem(int position)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      */
indexOf(GuidedAction action)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      */
getGuidedActionsStylist()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      */
setClickListener(ClickListener clickListener)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      */
setFocusListener(FocusListener focusListener)210     public void setFocusListener(FocusListener focusListener) {
211         mActionOnFocusListener.setFocusListener(focusListener);
212     }
213 
214     /**
215      * Used for serialization only.
216      * @hide
217      */
getActions()218     public List<GuidedAction> getActions() {
219         return new ArrayList<GuidedAction>(mActions);
220     }
221 
222     /**
223      * {@inheritDoc}
224      */
225     @Override
getItemViewType(int position)226     public int getItemViewType(int position) {
227         return mStylist.getItemViewType(mActions.get(position));
228     }
229 
getRecyclerView()230     private RecyclerView getRecyclerView() {
231         return mIsSubAdapter ? mStylist.getSubActionsGridView() : mStylist.getActionsGridView();
232     }
233 
234     /**
235      * {@inheritDoc}
236      */
237     @Override
onCreateViewHolder(ViewGroup parent, int viewType)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 
setupListeners(EditText edit)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
onBindViewHolder(ViewHolder holder, int position)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
getItemCount()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 
ActionOnFocusListener(FocusListener focusListener)288         ActionOnFocusListener(FocusListener focusListener) {
289             mFocusListener = focusListener;
290         }
291 
setFocusListener(FocusListener focusListener)292         public void setFocusListener(FocusListener focusListener) {
293             mFocusListener = focusListener;
294         }
295 
unFocus()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
onFocusChange(View v, boolean hasFocus)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 
findSubChildViewHolder(View v)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 
handleCheckedActions(GuidedActionsStylist.ViewHolder avh)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 
performOnActionClick(GuidedActionsStylist.ViewHolder avh)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
onKey(View v, int keyCode, KeyEvent event)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
onEditorAction(TextView v, int actionId, KeyEvent event)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
onKeyPreIme(EditText editText, int keyCode, KeyEvent event)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