SelectInputView.java revision 4a5144ac8c51c4d89d1359e13e37fcd7f928ed9a
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.media.tv.TvInputInfo;
22import android.media.tv.TvInputManager;
23import android.media.tv.TvInputManager.TvInputCallback;
24import android.support.annotation.NonNull;
25import android.support.v17.leanback.widget.VerticalGridView;
26import android.support.v7.widget.RecyclerView;
27import android.text.TextUtils;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.view.KeyEvent;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.ViewGroup;
34import android.widget.TextView;
35import com.android.tv.R;
36import com.android.tv.TvSingletons;
37import com.android.tv.analytics.Tracker;
38import com.android.tv.common.util.DurationTimer;
39import com.android.tv.data.Channel;
40import com.android.tv.util.TvInputManagerHelper;
41import java.util.ArrayList;
42import java.util.Collections;
43import java.util.HashMap;
44import java.util.List;
45import java.util.Map;
46
47public class SelectInputView extends VerticalGridView
48        implements TvTransitionManager.TransitionLayout {
49    private static final String TAG = "SelectInputView";
50    private static final boolean DEBUG = false;
51    public static final String SCREEN_NAME = "Input selection";
52    private static final int TUNER_INPUT_POSITION = 0;
53
54    private final TvInputManagerHelper mTvInputManagerHelper;
55    private final List<TvInputInfo> mInputList = new ArrayList<>();
56    private final TvInputManagerHelper.HardwareInputComparator mComparator;
57    private final Tracker mTracker;
58    private final DurationTimer mViewDurationTimer = new DurationTimer();
59    private final TvInputCallback mTvInputCallback =
60            new TvInputCallback() {
61                @Override
62                public void onInputAdded(String inputId) {
63                    buildInputListAndNotify();
64                    updateSelectedPositionIfNeeded();
65                }
66
67                @Override
68                public void onInputRemoved(String inputId) {
69                    buildInputListAndNotify();
70                    updateSelectedPositionIfNeeded();
71                }
72
73                @Override
74                public void onInputUpdated(String inputId) {
75                    buildInputListAndNotify();
76                    updateSelectedPositionIfNeeded();
77                }
78
79                @Override
80                public void onInputStateChanged(String inputId, int state) {
81                    buildInputListAndNotify();
82                    updateSelectedPositionIfNeeded();
83                }
84
85                private void updateSelectedPositionIfNeeded() {
86                    if (!isFocusable() || mSelectedInput == null) {
87                        return;
88                    }
89                    if (!isInputEnabled(mSelectedInput)) {
90                        setSelectedPosition(TUNER_INPUT_POSITION);
91                        return;
92                    }
93                    if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) {
94                        setSelectedPosition(getInputPosition(mSelectedInput.getId()));
95                    }
96                }
97            };
98
99    private Channel mCurrentChannel;
100    private OnInputSelectedCallback mCallback;
101
102    private final Runnable mHideRunnable =
103            new Runnable() {
104                @Override
105                public void run() {
106                    if (mSelectedInput == null) {
107                        return;
108                    }
109                    // TODO: pass english label to tracker http://b/22355024
110                    final String label = mSelectedInput.loadLabel(getContext()).toString();
111                    mTracker.sendInputSelected(label);
112                    if (mCallback != null) {
113                        if (mSelectedInput.isPassthroughInput()) {
114                            mCallback.onPassthroughInputSelected(mSelectedInput);
115                        } else {
116                            mCallback.onTunerInputSelected();
117                        }
118                    }
119                }
120            };
121
122    private final int mInputItemHeight;
123    private final long mShowDurationMillis;
124    private final long mRippleAnimDurationMillis;
125    private final int mTextColorPrimary;
126    private final int mTextColorSecondary;
127    private final int mTextColorDisabled;
128    private final View mItemViewForMeasure;
129
130    private boolean mResetTransitionAlpha;
131    private TvInputInfo mSelectedInput;
132    private int mMaxItemWidth;
133
134    public SelectInputView(Context context) {
135        this(context, null, 0);
136    }
137
138    public SelectInputView(Context context, AttributeSet attrs) {
139        this(context, attrs, 0);
140    }
141
142    public SelectInputView(Context context, AttributeSet attrs, int defStyleAttr) {
143        super(context, attrs, defStyleAttr);
144        setAdapter(new InputListAdapter());
145
146        TvSingletons tvSingletons = TvSingletons.getSingletons(context);
147        mTracker = tvSingletons.getTracker();
148        mTvInputManagerHelper = tvSingletons.getTvInputManagerHelper();
149        mComparator =
150                new TvInputManagerHelper.HardwareInputComparator(context, mTvInputManagerHelper);
151
152        Resources resources = context.getResources();
153        mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height);
154        mShowDurationMillis = resources.getInteger(R.integer.select_input_show_duration);
155        mRippleAnimDurationMillis =
156                resources.getInteger(R.integer.select_input_ripple_anim_duration);
157        mTextColorPrimary = resources.getColor(R.color.select_input_text_color_primary, null);
158        mTextColorSecondary = resources.getColor(R.color.select_input_text_color_secondary, null);
159        mTextColorDisabled = resources.getColor(R.color.select_input_text_color_disabled, null);
160
161        mItemViewForMeasure =
162                LayoutInflater.from(context).inflate(R.layout.select_input_item, this, false);
163        buildInputListAndNotify();
164    }
165
166    @Override
167    public boolean onKeyUp(int keyCode, KeyEvent event) {
168        if (DEBUG) Log.d(TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
169        scheduleHide();
170
171        if (keyCode == KeyEvent.KEYCODE_TV_INPUT) {
172            // Go down to the next available input.
173            int currentPosition = mInputList.indexOf(mSelectedInput);
174            int nextPosition = currentPosition;
175            while (true) {
176                nextPosition = (nextPosition + 1) % mInputList.size();
177                if (isInputEnabled(mInputList.get(nextPosition))) {
178                    break;
179                }
180                if (nextPosition == currentPosition) {
181                    nextPosition = 0;
182                    break;
183                }
184            }
185            setSelectedPosition(nextPosition);
186            return true;
187        }
188        return super.onKeyUp(keyCode, event);
189    }
190
191    @Override
192    public void onEnterAction(boolean fromEmptyScene) {
193        mTracker.sendShowInputSelection();
194        mTracker.sendScreenView(SCREEN_NAME);
195        mViewDurationTimer.start();
196        scheduleHide();
197
198        mResetTransitionAlpha = fromEmptyScene;
199        buildInputListAndNotify();
200        mTvInputManagerHelper.addCallback(mTvInputCallback);
201        String currentInputId =
202                mCurrentChannel != null && mCurrentChannel.isPassthrough()
203                        ? mCurrentChannel.getInputId()
204                        : 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
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
228    public void onExitAction() {
229        mTracker.sendHideInputSelection(mViewDurationTimer.reset());
230        mTvInputManagerHelper.removeCallback(mTvInputCallback);
231        removeCallbacks(mHideRunnable);
232    }
233
234    @Override
235    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
236        int height = mInputItemHeight * mInputList.size();
237        super.onMeasure(
238                MeasureSpec.makeMeasureSpec(mMaxItemWidth, MeasureSpec.EXACTLY),
239                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
240    }
241
242    private void scheduleHide() {
243        removeCallbacks(mHideRunnable);
244        postDelayed(mHideRunnable, mShowDurationMillis);
245    }
246
247    private void buildInputListAndNotify() {
248        mInputList.clear();
249        Map<String, TvInputInfo> inputMap = new HashMap<>();
250        boolean foundTuner = false;
251        for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(false, false)) {
252            if (input.isPassthroughInput()) {
253                if (!input.isHidden(getContext())) {
254                    mInputList.add(input);
255                    inputMap.put(input.getId(), input);
256                }
257            } else if (!foundTuner) {
258                foundTuner = true;
259                mInputList.add(input);
260            }
261        }
262        // Do not show HDMI ports if a CEC device is directly connected to the port.
263        for (TvInputInfo input : inputMap.values()) {
264            if (input.getParentId() != null && !input.isConnectedToHdmiSwitch()) {
265                mInputList.remove(inputMap.get(input.getParentId()));
266            }
267        }
268        Collections.sort(mInputList, mComparator);
269
270        // Update the max item width.
271        mMaxItemWidth = 0;
272        for (TvInputInfo input : mInputList) {
273            setItemViewText(mItemViewForMeasure, input);
274            mItemViewForMeasure.measure(0, 0);
275            int width = mItemViewForMeasure.getMeasuredWidth();
276            if (width > mMaxItemWidth) {
277                mMaxItemWidth = width;
278            }
279        }
280
281        getAdapter().notifyDataSetChanged();
282    }
283
284    private void setItemViewText(View v, TvInputInfo input) {
285        TextView inputLabelView = (TextView) v.findViewById(R.id.input_label);
286        TextView secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
287        CharSequence customLabel = input.loadCustomLabel(getContext());
288        CharSequence label = input.loadLabel(getContext());
289        if (TextUtils.isEmpty(customLabel) || customLabel.equals(label)) {
290            inputLabelView.setText(label);
291            secondaryInputLabelView.setVisibility(View.GONE);
292        } else {
293            inputLabelView.setText(customLabel);
294            secondaryInputLabelView.setText(label);
295            secondaryInputLabelView.setVisibility(View.VISIBLE);
296        }
297    }
298
299    private boolean isInputEnabled(TvInputInfo input) {
300        return mTvInputManagerHelper.getInputState(input)
301                != TvInputManager.INPUT_STATE_DISCONNECTED;
302    }
303
304    /** Sets a callback which receives the notifications of input selection. */
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 {@code
311     * 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 =
321                    LayoutInflater.from(parent.getContext())
322                            .inflate(R.layout.select_input_item, parent, false);
323            return new ViewHolder(v);
324        }
325
326        @Override
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(
348                    new View.OnClickListener() {
349                        @Override
350                        public void onClick(View v) {
351                            mSelectedInput = mInputList.get(position);
352                            // The user made a selection. Hide this view after the ripple animation.
353                            // But
354                            // first, disable focus to avoid any further focus change during the
355                            // animation.
356                            setFocusable(false);
357                            removeCallbacks(mHideRunnable);
358                            postDelayed(mHideRunnable, mRippleAnimDurationMillis);
359                        }
360                    });
361            holder.itemView.setOnFocusChangeListener(
362                    new View.OnFocusChangeListener() {
363                        @Override
364                        public void onFocusChange(View view, boolean hasFocus) {
365                            if (hasFocus) {
366                                mSelectedInput = mInputList.get(position);
367                            }
368                        }
369                    });
370
371            if (mResetTransitionAlpha) {
372                ViewUtils.setTransitionAlpha(holder.itemView, 1f);
373            }
374        }
375
376        @Override
377        public int getItemCount() {
378            return mInputList.size();
379        }
380
381        class ViewHolder extends RecyclerView.ViewHolder {
382            final TextView inputLabelView;
383            final TextView secondaryInputLabelView;
384
385            ViewHolder(View v) {
386                super(v);
387                inputLabelView = (TextView) v.findViewById(R.id.input_label);
388                secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
389            }
390        }
391    }
392
393    /** A callback interface for the input selection. */
394    public interface OnInputSelectedCallback {
395        /** Called when the tuner input is selected. */
396        void onTunerInputSelected();
397
398        /** Called when the passthrough input is selected. */
399        void onPassthroughInputSelected(@NonNull TvInputInfo input);
400    }
401}
402