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