1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.ui;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.hardware.hdmi.HdmiDeviceInfo;
22 import android.media.tv.TvInputInfo;
23 import android.media.tv.TvInputManager;
24 import android.media.tv.TvInputManager.TvInputCallback;
25 import android.support.annotation.NonNull;
26 import android.support.v17.leanback.widget.VerticalGridView;
27 import android.support.v7.widget.RecyclerView;
28 import android.text.TextUtils;
29 import android.util.AttributeSet;
30 import android.util.Log;
31 import android.view.KeyEvent;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.widget.TextView;
36 
37 import com.android.tv.R;
38 import com.android.tv.ApplicationSingletons;
39 import com.android.tv.TvApplication;
40 import com.android.tv.analytics.DurationTimer;
41 import com.android.tv.analytics.Tracker;
42 import com.android.tv.data.Channel;
43 import com.android.tv.util.TvInputManagerHelper;
44 import com.android.tv.util.Utils;
45 
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.Comparator;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Map;
52 
53 public class SelectInputView extends VerticalGridView implements
54         TvTransitionManager.TransitionLayout {
55     private static final String TAG = "SelectInputView";
56     private static final boolean DEBUG = false;
57     public static final String SCREEN_NAME = "Input selection";
58     private static final int TUNER_INPUT_POSITION = 0;
59 
60     private final TvInputManagerHelper mTvInputManagerHelper;
61     private final List<TvInputInfo> mInputList = new ArrayList<>();
62     private final InputsComparator mComparator = new InputsComparator();
63     private final Tracker mTracker;
64     private final DurationTimer mViewDurationTimer = new DurationTimer();
65     private final TvInputCallback mTvInputCallback = new TvInputCallback() {
66         @Override
67         public void onInputAdded(String inputId) {
68             buildInputListAndNotify();
69             updateSelectedPositionIfNeeded();
70         }
71 
72         @Override
73         public void onInputRemoved(String inputId) {
74             buildInputListAndNotify();
75             updateSelectedPositionIfNeeded();
76         }
77 
78         @Override
79         public void onInputUpdated(String inputId) {
80             buildInputListAndNotify();
81             updateSelectedPositionIfNeeded();
82         }
83 
84         @Override
85         public void onInputStateChanged(String inputId, int state) {
86             buildInputListAndNotify();
87             updateSelectedPositionIfNeeded();
88         }
89 
90         private void updateSelectedPositionIfNeeded() {
91             if (!isFocusable() || mSelectedInput == null) {
92                 return;
93             }
94             if (!isInputEnabled(mSelectedInput)) {
95                 setSelectedPosition(TUNER_INPUT_POSITION);
96                 return;
97             }
98             if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) {
99                 setSelectedPosition(getInputPosition(mSelectedInput.getId()));
100             }
101         }
102     };
103 
104     private Channel mCurrentChannel;
105     private OnInputSelectedCallback mCallback;
106 
107     private final Runnable mHideRunnable = new Runnable() {
108         @Override
109         public void run() {
110             if (mSelectedInput == null) {
111                 return;
112             }
113             // TODO: pass english label to tracker http://b/22355024
114             final String label = mSelectedInput.loadLabel(getContext()).toString();
115             mTracker.sendInputSelected(label);
116             if (mCallback != null) {
117                 if (mSelectedInput.isPassthroughInput()) {
118                     mCallback.onPassthroughInputSelected(mSelectedInput);
119                 } else {
120                     mCallback.onTunerInputSelected();
121                 }
122             }
123         }
124     };
125 
126     private final int mInputItemHeight;
127     private final long mShowDurationMillis;
128     private final long mRippleAnimDurationMillis;
129     private final int mTextColorPrimary;
130     private final int mTextColorSecondary;
131     private final int mTextColorDisabled;
132     private final View mItemViewForMeasure;
133 
134     private boolean mResetTransitionAlpha;
135     private TvInputInfo mSelectedInput;
136     private int mMaxItemWidth;
137 
SelectInputView(Context context)138     public SelectInputView(Context context) {
139         this(context, null, 0);
140     }
141 
SelectInputView(Context context, AttributeSet attrs)142     public SelectInputView(Context context, AttributeSet attrs) {
143         this(context, attrs, 0);
144     }
145 
SelectInputView(Context context, AttributeSet attrs, int defStyleAttr)146     public SelectInputView(Context context, AttributeSet attrs, int defStyleAttr) {
147         super(context, attrs, defStyleAttr);
148         setAdapter(new InputListAdapter());
149 
150         ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
151         mTracker = appSingletons.getTracker();
152         mTvInputManagerHelper = appSingletons.getTvInputManagerHelper();
153 
154         Resources resources = context.getResources();
155         mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height);
156         mShowDurationMillis = resources.getInteger(R.integer.select_input_show_duration);
157         mRippleAnimDurationMillis = resources.getInteger(
158                 R.integer.select_input_ripple_anim_duration);
159         mTextColorPrimary = Utils.getColor(resources, R.color.select_input_text_color_primary);
160         mTextColorSecondary = Utils.getColor(resources, R.color.select_input_text_color_secondary);
161         mTextColorDisabled = Utils.getColor(resources, R.color.select_input_text_color_disabled);
162 
163         mItemViewForMeasure = LayoutInflater.from(context).inflate(
164                 R.layout.select_input_item, this, false);
165         buildInputListAndNotify();
166     }
167 
168     @Override
onKeyUp(int keyCode, KeyEvent event)169     public boolean onKeyUp(int keyCode, KeyEvent event) {
170         if (DEBUG) Log.d(TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
171         scheduleHide();
172 
173         if (keyCode == KeyEvent.KEYCODE_TV_INPUT) {
174             // Go down to the next available input.
175             int currentPosition = mInputList.indexOf(mSelectedInput);
176             int nextPosition = currentPosition;
177             while (true) {
178                 nextPosition = (nextPosition + 1) % mInputList.size();
179                 if (isInputEnabled(mInputList.get(nextPosition))) {
180                     break;
181                 }
182                 if (nextPosition == currentPosition) {
183                     nextPosition = 0;
184                     break;
185                 }
186             }
187             setSelectedPosition(nextPosition);
188             return true;
189         }
190         return super.onKeyUp(keyCode, event);
191     }
192 
193     @Override
onEnterAction(boolean fromEmptyScene)194     public void onEnterAction(boolean fromEmptyScene) {
195         mTracker.sendShowInputSelection();
196         mTracker.sendScreenView(SCREEN_NAME);
197         mViewDurationTimer.start();
198         scheduleHide();
199 
200         mResetTransitionAlpha = fromEmptyScene;
201         buildInputListAndNotify();
202         mTvInputManagerHelper.addCallback(mTvInputCallback);
203         String currentInputId = mCurrentChannel != null && mCurrentChannel.isPassthrough() ?
204                 mCurrentChannel.getInputId() : null;
205         if (currentInputId != null
206                 && !isInputEnabled(mTvInputManagerHelper.getTvInputInfo(currentInputId))) {
207             // If current input is disabled, the tuner input will be focused.
208             setSelectedPosition(TUNER_INPUT_POSITION);
209         } else {
210             setSelectedPosition(getInputPosition(currentInputId));
211         }
212         setFocusable(true);
213         requestFocus();
214     }
215 
getInputPosition(String inputId)216     private int getInputPosition(String inputId) {
217         if (inputId != null) {
218             for (int i = 0; i < mInputList.size(); ++i) {
219                 if (TextUtils.equals(mInputList.get(i).getId(), inputId)) {
220                     return i;
221                 }
222             }
223         }
224         return TUNER_INPUT_POSITION;
225     }
226 
227     @Override
onExitAction()228     public void onExitAction() {
229         mTracker.sendHideInputSelection(mViewDurationTimer.reset());
230         mTvInputManagerHelper.removeCallback(mTvInputCallback);
231         removeCallbacks(mHideRunnable);
232     }
233 
234     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)235     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
236         int height = mInputItemHeight * mInputList.size();
237         super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxItemWidth, MeasureSpec.EXACTLY),
238                 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
239     }
240 
scheduleHide()241     private void scheduleHide() {
242         removeCallbacks(mHideRunnable);
243         postDelayed(mHideRunnable, mShowDurationMillis);
244     }
245 
buildInputListAndNotify()246     private void buildInputListAndNotify() {
247         mInputList.clear();
248         Map<String, TvInputInfo> inputMap = new HashMap<>();
249         boolean foundTuner = false;
250         for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(false, false)) {
251             if (input.isPassthroughInput()) {
252                 if (!input.isHidden(getContext())) {
253                     mInputList.add(input);
254                     inputMap.put(input.getId(), input);
255                 }
256             } else if (!foundTuner) {
257                 foundTuner = true;
258                 mInputList.add(input);
259             }
260         }
261         // Do not show HDMI ports if a CEC device is directly connected to the port.
262         for (TvInputInfo input : inputMap.values()) {
263             if (input.getParentId() != null && !input.isConnectedToHdmiSwitch()) {
264                 mInputList.remove(inputMap.get(input.getParentId()));
265             }
266         }
267         Collections.sort(mInputList, mComparator);
268 
269         // Update the max item width.
270         mMaxItemWidth = 0;
271         for (TvInputInfo input : mInputList) {
272             setItemViewText(mItemViewForMeasure, input);
273             mItemViewForMeasure.measure(0, 0);
274             int width = mItemViewForMeasure.getMeasuredWidth();
275             if (width > mMaxItemWidth) {
276                 mMaxItemWidth = width;
277             }
278         }
279 
280         getAdapter().notifyDataSetChanged();
281     }
282 
setItemViewText(View v, TvInputInfo input)283     private void setItemViewText(View v, TvInputInfo input) {
284         TextView inputLabelView = (TextView) v.findViewById(R.id.input_label);
285         TextView secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
286         CharSequence customLabel = input.loadCustomLabel(getContext());
287         CharSequence label = input.loadLabel(getContext());
288         if (TextUtils.isEmpty(customLabel) || customLabel.equals(label)) {
289             inputLabelView.setText(label);
290             secondaryInputLabelView.setVisibility(View.GONE);
291         } else {
292             inputLabelView.setText(customLabel);
293             secondaryInputLabelView.setText(label);
294             secondaryInputLabelView.setVisibility(View.VISIBLE);
295         }
296     }
297 
isInputEnabled(TvInputInfo input)298     private boolean isInputEnabled(TvInputInfo input) {
299         return mTvInputManagerHelper.getInputState(input)
300                 != TvInputManager.INPUT_STATE_DISCONNECTED;
301     }
302 
303     /**
304      * Sets a callback which receives the notifications of input selection.
305      */
setOnInputSelectedCallback(OnInputSelectedCallback callback)306     public void setOnInputSelectedCallback(OnInputSelectedCallback callback) {
307         mCallback = callback;
308     }
309 
310     /**
311      * Sets the current channel. The initial selection will be the input which contains the
312      * {@code channel}.
313      */
setCurrentChannel(Channel channel)314     public void setCurrentChannel(Channel channel) {
315         mCurrentChannel = channel;
316     }
317 
318     class InputListAdapter extends RecyclerView.Adapter<InputListAdapter.ViewHolder> {
319         @Override
onCreateViewHolder(ViewGroup parent, int viewType)320         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
321             View v = LayoutInflater.from(parent.getContext()).inflate(
322                     R.layout.select_input_item, parent, false);
323             return new ViewHolder(v);
324         }
325 
326         @Override
onBindViewHolder(ViewHolder holder, final int position)327         public void onBindViewHolder(ViewHolder holder, final int position) {
328             TvInputInfo input = mInputList.get(position);
329             if (input.isPassthroughInput()) {
330                 if (isInputEnabled(input)) {
331                     holder.itemView.setFocusable(true);
332                     holder.inputLabelView.setTextColor(mTextColorPrimary);
333                     holder.secondaryInputLabelView.setTextColor(mTextColorSecondary);
334                 } else {
335                     holder.itemView.setFocusable(false);
336                     holder.inputLabelView.setTextColor(mTextColorDisabled);
337                     holder.secondaryInputLabelView.setTextColor(mTextColorDisabled);
338                 }
339                 setItemViewText(holder.itemView, input);
340             } else {
341                 holder.itemView.setFocusable(true);
342                 holder.inputLabelView.setTextColor(mTextColorPrimary);
343                 holder.inputLabelView.setText(R.string.input_long_label_for_tuner);
344                 holder.secondaryInputLabelView.setVisibility(View.GONE);
345             }
346 
347             holder.itemView.setOnClickListener(new View.OnClickListener() {
348                 @Override
349                 public void onClick(View v) {
350                     mSelectedInput = mInputList.get(position);
351                     // The user made a selection. Hide this view after the ripple animation. But
352                     // first, disable focus to avoid any further focus change during the animation.
353                     setFocusable(false);
354                     removeCallbacks(mHideRunnable);
355                     postDelayed(mHideRunnable, mRippleAnimDurationMillis);
356                 }
357             });
358             holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
359                 @Override
360                 public void onFocusChange(View view, boolean hasFocus) {
361                     if (hasFocus) {
362                         mSelectedInput = mInputList.get(position);
363                     }
364                 }
365             });
366 
367             if (mResetTransitionAlpha) {
368                 ViewUtils.setTransitionAlpha(holder.itemView, 1f);
369             }
370         }
371 
372         @Override
getItemCount()373         public int getItemCount() {
374             return mInputList.size();
375         }
376 
377         class ViewHolder extends RecyclerView.ViewHolder {
378             final TextView inputLabelView;
379             final TextView secondaryInputLabelView;
380 
ViewHolder(View v)381             ViewHolder(View v) {
382                 super(v);
383                 inputLabelView = (TextView) v.findViewById(R.id.input_label);
384                 secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
385             }
386         }
387     }
388 
389     private class InputsComparator implements Comparator<TvInputInfo> {
390         @Override
compare(TvInputInfo lhs, TvInputInfo rhs)391         public int compare(TvInputInfo lhs, TvInputInfo rhs) {
392             if (lhs == null) {
393                 return (rhs == null) ? 0 : 1;
394             }
395             if (rhs == null) {
396                 return -1;
397             }
398 
399             boolean enabledL = isInputEnabled(lhs);
400             boolean enabledR = isInputEnabled(rhs);
401             if (enabledL != enabledR) {
402                 return enabledL ? -1 : 1;
403             }
404 
405             int priorityL = getPriority(lhs);
406             int priorityR = getPriority(rhs);
407             if (priorityL != priorityR) {
408                 return priorityR - priorityL;
409             }
410 
411             String customLabelL = (String) lhs.loadCustomLabel(getContext());
412             String customLabelR = (String) rhs.loadCustomLabel(getContext());
413             if (!TextUtils.equals(customLabelL, customLabelR)) {
414                 customLabelL = customLabelL == null ? "" : customLabelL;
415                 customLabelR = customLabelR == null ? "" : customLabelR;
416                 return customLabelL.compareToIgnoreCase(customLabelR);
417             }
418 
419             String labelL = (String) lhs.loadLabel(getContext());
420             String labelR = (String) rhs.loadLabel(getContext());
421             labelL = labelL == null ? "" : labelL;
422             labelR = labelR == null ? "" : labelR;
423             return labelL.compareToIgnoreCase(labelR);
424         }
425 
getPriority(TvInputInfo info)426         private int getPriority(TvInputInfo info) {
427             switch (info.getType()) {
428                 case TvInputInfo.TYPE_TUNER:
429                     return 9;
430                 case TvInputInfo.TYPE_HDMI:
431                     HdmiDeviceInfo hdmiInfo = info.getHdmiDeviceInfo();
432                     if (hdmiInfo != null && hdmiInfo.isCecDevice()) {
433                         return 8;
434                     }
435                     return 7;
436                 case TvInputInfo.TYPE_DVI:
437                     return 6;
438                 case TvInputInfo.TYPE_COMPONENT:
439                     return 5;
440                 case TvInputInfo.TYPE_SVIDEO:
441                     return 4;
442                 case TvInputInfo.TYPE_COMPOSITE:
443                     return 3;
444                 case TvInputInfo.TYPE_DISPLAY_PORT:
445                     return 2;
446                 case TvInputInfo.TYPE_VGA:
447                     return 1;
448                 case TvInputInfo.TYPE_SCART:
449                 default:
450                     return 0;
451             }
452         }
453     }
454 
455     /**
456      * A callback interface for the input selection.
457      */
458     public interface OnInputSelectedCallback {
459         /**
460          * Called when the tuner input is selected.
461          */
onTunerInputSelected()462         void onTunerInputSelected();
463 
464         /**
465          * Called when the passthrough input is selected.
466          */
onPassthroughInputSelected(@onNull TvInputInfo input)467         void onPassthroughInputSelected(@NonNull TvInputInfo input);
468     }
469 }
470