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.media.tv.TvInputInfo;
22 import android.media.tv.TvInputManager;
23 import android.media.tv.TvInputManager.TvInputCallback;
24 import android.support.annotation.NonNull;
25 import androidx.leanback.widget.VerticalGridView;
26 import androidx.recyclerview.widget.RecyclerView;
27 import android.text.TextUtils;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.view.KeyEvent;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.TextView;
35 import com.android.tv.R;
36 import com.android.tv.TvSingletons;
37 import com.android.tv.analytics.Tracker;
38 import com.android.tv.common.util.DurationTimer;
39 import com.android.tv.data.api.Channel;
40 import com.android.tv.util.TvInputManagerHelper;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46 
47 public 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 
SelectInputView(Context context)134     public SelectInputView(Context context) {
135         this(context, null, 0);
136     }
137 
SelectInputView(Context context, AttributeSet attrs)138     public SelectInputView(Context context, AttributeSet attrs) {
139         this(context, attrs, 0);
140     }
141 
SelectInputView(Context context, AttributeSet attrs, int defStyleAttr)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
onKeyUp(int keyCode, KeyEvent event)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
onEnterAction(boolean fromEmptyScene)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 
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(
238                 MeasureSpec.makeMeasureSpec(mMaxItemWidth, MeasureSpec.EXACTLY),
239                 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
240     }
241 
scheduleHide()242     private void scheduleHide() {
243         removeCallbacks(mHideRunnable);
244         postDelayed(mHideRunnable, mShowDurationMillis);
245     }
246 
buildInputListAndNotify()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 
setItemViewText(View v, TvInputInfo input)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 
isInputEnabled(TvInputInfo input)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. */
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 {@code
311      * 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 =
321                     LayoutInflater.from(parent.getContext())
322                             .inflate(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(
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
getItemCount()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 
ViewHolder(View v)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. */
onTunerInputSelected()396         void onTunerInputSelected();
397 
398         /** Called when the passthrough input is selected. */
onPassthroughInputSelected(@onNull TvInputInfo input)399         void onPassthroughInputSelected(@NonNull TvInputInfo input);
400     }
401 }
402