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;
35
36import com.android.tv.ApplicationSingletons;
37import com.android.tv.R;
38import com.android.tv.TvApplication;
39import com.android.tv.util.DurationTimer;
40import com.android.tv.analytics.Tracker;
41import com.android.tv.data.Channel;
42import com.android.tv.util.TvInputManagerHelper;
43
44import java.util.ArrayList;
45import java.util.Collections;
46import java.util.HashMap;
47import java.util.List;
48import java.util.Map;
49
50public class SelectInputView extends VerticalGridView implements
51        TvTransitionManager.TransitionLayout {
52    private static final String TAG = "SelectInputView";
53    private static final boolean DEBUG = false;
54    public static final String SCREEN_NAME = "Input selection";
55    private static final int TUNER_INPUT_POSITION = 0;
56
57    private final TvInputManagerHelper mTvInputManagerHelper;
58    private final List<TvInputInfo> mInputList = new ArrayList<>();
59    private final TvInputManagerHelper.HardwareInputComparator mComparator;
60    private final Tracker mTracker;
61    private final DurationTimer mViewDurationTimer = new DurationTimer();
62    private final TvInputCallback mTvInputCallback = new TvInputCallback() {
63        @Override
64        public void onInputAdded(String inputId) {
65            buildInputListAndNotify();
66            updateSelectedPositionIfNeeded();
67        }
68
69        @Override
70        public void onInputRemoved(String inputId) {
71            buildInputListAndNotify();
72            updateSelectedPositionIfNeeded();
73        }
74
75        @Override
76        public void onInputUpdated(String inputId) {
77            buildInputListAndNotify();
78            updateSelectedPositionIfNeeded();
79        }
80
81        @Override
82        public void onInputStateChanged(String inputId, int state) {
83            buildInputListAndNotify();
84            updateSelectedPositionIfNeeded();
85        }
86
87        private void updateSelectedPositionIfNeeded() {
88            if (!isFocusable() || mSelectedInput == null) {
89                return;
90            }
91            if (!isInputEnabled(mSelectedInput)) {
92                setSelectedPosition(TUNER_INPUT_POSITION);
93                return;
94            }
95            if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) {
96                setSelectedPosition(getInputPosition(mSelectedInput.getId()));
97            }
98        }
99    };
100
101    private Channel mCurrentChannel;
102    private OnInputSelectedCallback mCallback;
103
104    private final Runnable mHideRunnable = new Runnable() {
105        @Override
106        public void run() {
107            if (mSelectedInput == null) {
108                return;
109            }
110            // TODO: pass english label to tracker http://b/22355024
111            final String label = mSelectedInput.loadLabel(getContext()).toString();
112            mTracker.sendInputSelected(label);
113            if (mCallback != null) {
114                if (mSelectedInput.isPassthroughInput()) {
115                    mCallback.onPassthroughInputSelected(mSelectedInput);
116                } else {
117                    mCallback.onTunerInputSelected();
118                }
119            }
120        }
121    };
122
123    private final int mInputItemHeight;
124    private final long mShowDurationMillis;
125    private final long mRippleAnimDurationMillis;
126    private final int mTextColorPrimary;
127    private final int mTextColorSecondary;
128    private final int mTextColorDisabled;
129    private final View mItemViewForMeasure;
130
131    private boolean mResetTransitionAlpha;
132    private TvInputInfo mSelectedInput;
133    private int mMaxItemWidth;
134
135    public SelectInputView(Context context) {
136        this(context, null, 0);
137    }
138
139    public SelectInputView(Context context, AttributeSet attrs) {
140        this(context, attrs, 0);
141    }
142
143    public SelectInputView(Context context, AttributeSet attrs, int defStyleAttr) {
144        super(context, attrs, defStyleAttr);
145        setAdapter(new InputListAdapter());
146
147        ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
148        mTracker = appSingletons.getTracker();
149        mTvInputManagerHelper = appSingletons.getTvInputManagerHelper();
150        mComparator =
151                new TvInputManagerHelper.HardwareInputComparator(context, mTvInputManagerHelper);
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    /**
389     * A callback interface for the input selection.
390     */
391    public interface OnInputSelectedCallback {
392        /**
393         * Called when the tuner input is selected.
394         */
395        void onTunerInputSelected();
396
397        /**
398         * Called when the passthrough input is selected.
399         */
400        void onPassthroughInputSelected(@NonNull TvInputInfo input);
401    }
402}
403