Home | History | Annotate | Download | only in ui
      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.hardware.hdmi.HdmiDeviceInfo;
     22 import android.media.tv.TvInputInfo;
     23 import android.media.tv.TvInputManager;
     24 import android.media.tv.TvInputManager.TvInputCallback;
     25 import android.support.annotation.NonNull;
     26 import android.support.v17.leanback.widget.VerticalGridView;
     27 import android.support.v7.widget.RecyclerView;
     28 import android.text.TextUtils;
     29 import android.util.AttributeSet;
     30 import android.util.Log;
     31 import android.view.KeyEvent;
     32 import android.view.LayoutInflater;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 import android.widget.TextView;
     36 
     37 import com.android.tv.ApplicationSingletons;
     38 import com.android.tv.R;
     39 import com.android.tv.TvApplication;
     40 import com.android.tv.analytics.DurationTimer;
     41 import com.android.tv.analytics.Tracker;
     42 import com.android.tv.data.Channel;
     43 import com.android.tv.util.TvInputManagerHelper;
     44 
     45 import java.util.ArrayList;
     46 import java.util.Collections;
     47 import java.util.Comparator;
     48 import java.util.HashMap;
     49 import java.util.List;
     50 import java.util.Map;
     51 
     52 public 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