blob: 88872a0a9276b97cf60861b04df9b778350afb79 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.ui;
import android.content.Context;
import android.content.res.Resources;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
import android.support.annotation.NonNull;
import android.support.v17.leanback.widget.VerticalGridView;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.util.DurationTimer;
import com.android.tv.analytics.Tracker;
import com.android.tv.data.Channel;
import com.android.tv.util.TvInputManagerHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SelectInputView extends VerticalGridView implements
TvTransitionManager.TransitionLayout {
private static final String TAG = "SelectInputView";
private static final boolean DEBUG = false;
public static final String SCREEN_NAME = "Input selection";
private static final int TUNER_INPUT_POSITION = 0;
private final TvInputManagerHelper mTvInputManagerHelper;
private final List<TvInputInfo> mInputList = new ArrayList<>();
private final TvInputManagerHelper.InputComparator mComparator;
private final Tracker mTracker;
private final DurationTimer mViewDurationTimer = new DurationTimer();
private final TvInputCallback mTvInputCallback = new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
buildInputListAndNotify();
updateSelectedPositionIfNeeded();
}
@Override
public void onInputRemoved(String inputId) {
buildInputListAndNotify();
updateSelectedPositionIfNeeded();
}
@Override
public void onInputUpdated(String inputId) {
buildInputListAndNotify();
updateSelectedPositionIfNeeded();
}
@Override
public void onInputStateChanged(String inputId, int state) {
buildInputListAndNotify();
updateSelectedPositionIfNeeded();
}
private void updateSelectedPositionIfNeeded() {
if (!isFocusable() || mSelectedInput == null) {
return;
}
if (!isInputEnabled(mSelectedInput)) {
setSelectedPosition(TUNER_INPUT_POSITION);
return;
}
if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) {
setSelectedPosition(getInputPosition(mSelectedInput.getId()));
}
}
};
private Channel mCurrentChannel;
private OnInputSelectedCallback mCallback;
private final Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
if (mSelectedInput == null) {
return;
}
// TODO: pass english label to tracker http://b/22355024
final String label = mSelectedInput.loadLabel(getContext()).toString();
mTracker.sendInputSelected(label);
if (mCallback != null) {
if (mSelectedInput.isPassthroughInput()) {
mCallback.onPassthroughInputSelected(mSelectedInput);
} else {
mCallback.onTunerInputSelected();
}
}
}
};
private final int mInputItemHeight;
private final long mShowDurationMillis;
private final long mRippleAnimDurationMillis;
private final int mTextColorPrimary;
private final int mTextColorSecondary;
private final int mTextColorDisabled;
private final View mItemViewForMeasure;
private boolean mResetTransitionAlpha;
private TvInputInfo mSelectedInput;
private int mMaxItemWidth;
public SelectInputView(Context context) {
this(context, null, 0);
}
public SelectInputView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SelectInputView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setAdapter(new InputListAdapter());
ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
mTracker = appSingletons.getTracker();
mTvInputManagerHelper = appSingletons.getTvInputManagerHelper();
mComparator = new TvInputManagerHelper.InputComparator(context, mTvInputManagerHelper);
Resources resources = context.getResources();
mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height);
mShowDurationMillis = resources.getInteger(R.integer.select_input_show_duration);
mRippleAnimDurationMillis = resources.getInteger(
R.integer.select_input_ripple_anim_duration);
mTextColorPrimary = resources.getColor(R.color.select_input_text_color_primary, null);
mTextColorSecondary = resources.getColor(R.color.select_input_text_color_secondary, null);
mTextColorDisabled = resources.getColor(R.color.select_input_text_color_disabled, null);
mItemViewForMeasure = LayoutInflater.from(context).inflate(
R.layout.select_input_item, this, false);
buildInputListAndNotify();
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (DEBUG) Log.d(TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
scheduleHide();
if (keyCode == KeyEvent.KEYCODE_TV_INPUT) {
// Go down to the next available input.
int currentPosition = mInputList.indexOf(mSelectedInput);
int nextPosition = currentPosition;
while (true) {
nextPosition = (nextPosition + 1) % mInputList.size();
if (isInputEnabled(mInputList.get(nextPosition))) {
break;
}
if (nextPosition == currentPosition) {
nextPosition = 0;
break;
}
}
setSelectedPosition(nextPosition);
return true;
}
return super.onKeyUp(keyCode, event);
}
@Override
public void onEnterAction(boolean fromEmptyScene) {
mTracker.sendShowInputSelection();
mTracker.sendScreenView(SCREEN_NAME);
mViewDurationTimer.start();
scheduleHide();
mResetTransitionAlpha = fromEmptyScene;
buildInputListAndNotify();
mTvInputManagerHelper.addCallback(mTvInputCallback);
String currentInputId = mCurrentChannel != null && mCurrentChannel.isPassthrough() ?
mCurrentChannel.getInputId() : null;
if (currentInputId != null
&& !isInputEnabled(mTvInputManagerHelper.getTvInputInfo(currentInputId))) {
// If current input is disabled, the tuner input will be focused.
setSelectedPosition(TUNER_INPUT_POSITION);
} else {
setSelectedPosition(getInputPosition(currentInputId));
}
setFocusable(true);
requestFocus();
}
private int getInputPosition(String inputId) {
if (inputId != null) {
for (int i = 0; i < mInputList.size(); ++i) {
if (TextUtils.equals(mInputList.get(i).getId(), inputId)) {
return i;
}
}
}
return TUNER_INPUT_POSITION;
}
@Override
public void onExitAction() {
mTracker.sendHideInputSelection(mViewDurationTimer.reset());
mTvInputManagerHelper.removeCallback(mTvInputCallback);
removeCallbacks(mHideRunnable);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = mInputItemHeight * mInputList.size();
super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxItemWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
private void scheduleHide() {
removeCallbacks(mHideRunnable);
postDelayed(mHideRunnable, mShowDurationMillis);
}
private void buildInputListAndNotify() {
mInputList.clear();
Map<String, TvInputInfo> inputMap = new HashMap<>();
boolean foundTuner = false;
for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(false, false)) {
if (input.isPassthroughInput()) {
if (!input.isHidden(getContext())) {
mInputList.add(input);
inputMap.put(input.getId(), input);
}
} else if (!foundTuner) {
foundTuner = true;
mInputList.add(input);
}
}
// Do not show HDMI ports if a CEC device is directly connected to the port.
for (TvInputInfo input : inputMap.values()) {
if (input.getParentId() != null && !input.isConnectedToHdmiSwitch()) {
mInputList.remove(inputMap.get(input.getParentId()));
}
}
Collections.sort(mInputList, mComparator);
// Update the max item width.
mMaxItemWidth = 0;
for (TvInputInfo input : mInputList) {
setItemViewText(mItemViewForMeasure, input);
mItemViewForMeasure.measure(0, 0);
int width = mItemViewForMeasure.getMeasuredWidth();
if (width > mMaxItemWidth) {
mMaxItemWidth = width;
}
}
getAdapter().notifyDataSetChanged();
}
private void setItemViewText(View v, TvInputInfo input) {
TextView inputLabelView = (TextView) v.findViewById(R.id.input_label);
TextView secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
CharSequence customLabel = input.loadCustomLabel(getContext());
CharSequence label = input.loadLabel(getContext());
if (TextUtils.isEmpty(customLabel) || customLabel.equals(label)) {
inputLabelView.setText(label);
secondaryInputLabelView.setVisibility(View.GONE);
} else {
inputLabelView.setText(customLabel);
secondaryInputLabelView.setText(label);
secondaryInputLabelView.setVisibility(View.VISIBLE);
}
}
private boolean isInputEnabled(TvInputInfo input) {
return mTvInputManagerHelper.getInputState(input)
!= TvInputManager.INPUT_STATE_DISCONNECTED;
}
/**
* Sets a callback which receives the notifications of input selection.
*/
public void setOnInputSelectedCallback(OnInputSelectedCallback callback) {
mCallback = callback;
}
/**
* Sets the current channel. The initial selection will be the input which contains the
* {@code channel}.
*/
public void setCurrentChannel(Channel channel) {
mCurrentChannel = channel;
}
class InputListAdapter extends RecyclerView.Adapter<InputListAdapter.ViewHolder> {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.select_input_item, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
TvInputInfo input = mInputList.get(position);
if (input.isPassthroughInput()) {
if (isInputEnabled(input)) {
holder.itemView.setFocusable(true);
holder.inputLabelView.setTextColor(mTextColorPrimary);
holder.secondaryInputLabelView.setTextColor(mTextColorSecondary);
} else {
holder.itemView.setFocusable(false);
holder.inputLabelView.setTextColor(mTextColorDisabled);
holder.secondaryInputLabelView.setTextColor(mTextColorDisabled);
}
setItemViewText(holder.itemView, input);
} else {
holder.itemView.setFocusable(true);
holder.inputLabelView.setTextColor(mTextColorPrimary);
holder.inputLabelView.setText(R.string.input_long_label_for_tuner);
holder.secondaryInputLabelView.setVisibility(View.GONE);
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mSelectedInput = mInputList.get(position);
// The user made a selection. Hide this view after the ripple animation. But
// first, disable focus to avoid any further focus change during the animation.
setFocusable(false);
removeCallbacks(mHideRunnable);
postDelayed(mHideRunnable, mRippleAnimDurationMillis);
}
});
holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
if (hasFocus) {
mSelectedInput = mInputList.get(position);
}
}
});
if (mResetTransitionAlpha) {
ViewUtils.setTransitionAlpha(holder.itemView, 1f);
}
}
@Override
public int getItemCount() {
return mInputList.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
final TextView inputLabelView;
final TextView secondaryInputLabelView;
ViewHolder(View v) {
super(v);
inputLabelView = (TextView) v.findViewById(R.id.input_label);
secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
}
}
}
/**
* A callback interface for the input selection.
*/
public interface OnInputSelectedCallback {
/**
* Called when the tuner input is selected.
*/
void onTunerInputSelected();
/**
* Called when the passthrough input is selected.
*/
void onPassthroughInputSelected(@NonNull TvInputInfo input);
}
}