SelectInputView.java revision 65fda1eaa94968bb55d5ded10dcb0b3f37fb05f2
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
17package com.android.tv.ui;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.hardware.hdmi.HdmiDeviceInfo;
22import android.media.tv.TvInputInfo;
23import android.media.tv.TvInputManager;
24import android.media.tv.TvInputManager.TvInputCallback;
25import android.support.annotation.NonNull;
26import android.support.v17.leanback.widget.VerticalGridView;
27import android.support.v7.widget.RecyclerView;
28import android.text.TextUtils;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.view.KeyEvent;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.ViewGroup;
35import android.widget.TextView;
36
37import com.android.tv.ApplicationSingletons;
38import com.android.tv.R;
39import com.android.tv.TvApplication;
40import com.android.tv.analytics.DurationTimer;
41import com.android.tv.analytics.Tracker;
42import com.android.tv.data.Channel;
43import com.android.tv.util.TvInputManagerHelper;
44
45import java.util.ArrayList;
46import java.util.Collections;
47import java.util.Comparator;
48import java.util.HashMap;
49import java.util.List;
50import java.util.Map;
51
52public 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
137    public SelectInputView(Context context) {
138        this(context, null, 0);
139    }
140
141    public SelectInputView(Context context, AttributeSet attrs) {
142        this(context, attrs, 0);
143    }
144
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
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
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
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
227    public void onExitAction() {
228        mTracker.sendHideInputSelection(mViewDurationTimer.reset());
229        mTvInputManagerHelper.removeCallback(mTvInputCallback);
230        removeCallbacks(mHideRunnable);
231    }
232
233    @Override
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
240    private void scheduleHide() {
241        removeCallbacks(mHideRunnable);
242        postDelayed(mHideRunnable, mShowDurationMillis);
243    }
244
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
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
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     */
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     */
313    public void setCurrentChannel(Channel channel) {
314        mCurrentChannel = channel;
315    }
316
317    class InputListAdapter extends RecyclerView.Adapter<InputListAdapter.ViewHolder> {
318        @Override
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
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
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
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
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
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         */
461        void onTunerInputSelected();
462
463        /**
464         * Called when the passthrough input is selected.
465         */
466        void onPassthroughInputSelected(@NonNull TvInputInfo input);
467    }
468}
469