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.R;
     38 import com.android.tv.ApplicationSingletons;
     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 import com.android.tv.util.Utils;
     45 
     46 import java.util.ArrayList;
     47 import java.util.Collections;
     48 import java.util.Comparator;
     49 import java.util.HashMap;
     50 import java.util.List;
     51 import java.util.Map;
     52 
     53 public class SelectInputView extends VerticalGridView implements
     54         TvTransitionManager.TransitionLayout {
     55     private static final String TAG = "SelectInputView";
     56     private static final boolean DEBUG = false;
     57     public static final String SCREEN_NAME = "Input selection";
     58     private static final int TUNER_INPUT_POSITION = 0;
     59 
     60     private final TvInputManagerHelper mTvInputManagerHelper;
     61     private final List<TvInputInfo> mInputList = new ArrayList<>();
     62     private final InputsComparator mComparator = new InputsComparator();
     63     private final Tracker mTracker;
     64     private final DurationTimer mViewDurationTimer = new DurationTimer();
     65     private final TvInputCallback mTvInputCallback = new TvInputCallback() {
     66         @Override
     67         public void onInputAdded(String inputId) {
     68             buildInputListAndNotify();
     69             updateSelectedPositionIfNeeded();
     70         }
     71 
     72         @Override
     73         public void onInputRemoved(String inputId) {
     74             buildInputListAndNotify();
     75             updateSelectedPositionIfNeeded();
     76         }
     77 
     78         @Override
     79         public void onInputUpdated(String inputId) {
     80             buildInputListAndNotify();
     81             updateSelectedPositionIfNeeded();
     82         }
     83 
     84         @Override
     85         public void onInputStateChanged(String inputId, int state) {
     86             buildInputListAndNotify();
     87             updateSelectedPositionIfNeeded();
     88         }
     89 
     90         private void updateSelectedPositionIfNeeded() {
     91             if (!isFocusable() || mSelectedInput == null) {
     92                 return;
     93             }
     94             if (!isInputEnabled(mSelectedInput)) {
     95                 setSelectedPosition(TUNER_INPUT_POSITION);
     96                 return;
     97             }
     98             if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) {
     99                 setSelectedPosition(getInputPosition(mSelectedInput.getId()));
    100             }
    101         }
    102     };
    103 
    104     private Channel mCurrentChannel;
    105     private OnInputSelectedCallback mCallback;
    106 
    107     private final Runnable mHideRunnable = new Runnable() {
    108         @Override
    109         public void run() {
    110             if (mSelectedInput == null) {
    111                 return;
    112             }
    113             // TODO: pass english label to tracker http://b/22355024
    114             final String label = mSelectedInput.loadLabel(getContext()).toString();
    115             mTracker.sendInputSelected(label);
    116             if (mCallback != null) {
    117                 if (mSelectedInput.isPassthroughInput()) {
    118                     mCallback.onPassthroughInputSelected(mSelectedInput);
    119                 } else {
    120                     mCallback.onTunerInputSelected();
    121                 }
    122             }
    123         }
    124     };
    125 
    126     private final int mInputItemHeight;
    127     private final long mShowDurationMillis;
    128     private final long mRippleAnimDurationMillis;
    129     private final int mTextColorPrimary;
    130     private final int mTextColorSecondary;
    131     private final int mTextColorDisabled;
    132     private final View mItemViewForMeasure;
    133 
    134     private boolean mResetTransitionAlpha;
    135     private TvInputInfo mSelectedInput;
    136     private int mMaxItemWidth;
    137 
    138     public SelectInputView(Context context) {
    139         this(context, null, 0);
    140     }
    141 
    142     public SelectInputView(Context context, AttributeSet attrs) {
    143         this(context, attrs, 0);
    144     }
    145 
    146     public SelectInputView(Context context, AttributeSet attrs, int defStyleAttr) {
    147         super(context, attrs, defStyleAttr);
    148         setAdapter(new InputListAdapter());
    149 
    150         ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
    151         mTracker = appSingletons.getTracker();
    152         mTvInputManagerHelper = appSingletons.getTvInputManagerHelper();
    153 
    154         Resources resources = context.getResources();
    155         mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height);
    156         mShowDurationMillis = resources.getInteger(R.integer.select_input_show_duration);
    157         mRippleAnimDurationMillis = resources.getInteger(
    158                 R.integer.select_input_ripple_anim_duration);
    159         mTextColorPrimary = Utils.getColor(resources, R.color.select_input_text_color_primary);
    160         mTextColorSecondary = Utils.getColor(resources, R.color.select_input_text_color_secondary);
    161         mTextColorDisabled = Utils.getColor(resources, R.color.select_input_text_color_disabled);
    162 
    163         mItemViewForMeasure = LayoutInflater.from(context).inflate(
    164                 R.layout.select_input_item, this, false);
    165         buildInputListAndNotify();
    166     }
    167 
    168     @Override
    169     public boolean onKeyUp(int keyCode, KeyEvent event) {
    170         if (DEBUG) Log.d(TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
    171         scheduleHide();
    172 
    173         if (keyCode == KeyEvent.KEYCODE_TV_INPUT) {
    174             // Go down to the next available input.
    175             int currentPosition = mInputList.indexOf(mSelectedInput);
    176             int nextPosition = currentPosition;
    177             while (true) {
    178                 nextPosition = (nextPosition + 1) % mInputList.size();
    179                 if (isInputEnabled(mInputList.get(nextPosition))) {
    180                     break;
    181                 }
    182                 if (nextPosition == currentPosition) {
    183                     nextPosition = 0;
    184                     break;
    185                 }
    186             }
    187             setSelectedPosition(nextPosition);
    188             return true;
    189         }
    190         return super.onKeyUp(keyCode, event);
    191     }
    192 
    193     @Override
    194     public void onEnterAction(boolean fromEmptyScene) {
    195         mTracker.sendShowInputSelection();
    196         mTracker.sendScreenView(SCREEN_NAME);
    197         mViewDurationTimer.start();
    198         scheduleHide();
    199 
    200         mResetTransitionAlpha = fromEmptyScene;
    201         buildInputListAndNotify();
    202         mTvInputManagerHelper.addCallback(mTvInputCallback);
    203         String currentInputId = mCurrentChannel != null && mCurrentChannel.isPassthrough() ?
    204                 mCurrentChannel.getInputId() : 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(MeasureSpec.makeMeasureSpec(mMaxItemWidth, MeasureSpec.EXACTLY),
    238                 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
    239     }
    240 
    241     private void scheduleHide() {
    242         removeCallbacks(mHideRunnable);
    243         postDelayed(mHideRunnable, mShowDurationMillis);
    244     }
    245 
    246     private void buildInputListAndNotify() {
    247         mInputList.clear();
    248         Map<String, TvInputInfo> inputMap = new HashMap<>();
    249         boolean foundTuner = false;
    250         for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(false, false)) {
    251             if (input.isPassthroughInput()) {
    252                 if (!input.isHidden(getContext())) {
    253                     mInputList.add(input);
    254                     inputMap.put(input.getId(), input);
    255                 }
    256             } else if (!foundTuner) {
    257                 foundTuner = true;
    258                 mInputList.add(input);
    259             }
    260         }
    261         // Do not show HDMI ports if a CEC device is directly connected to the port.
    262         for (TvInputInfo input : inputMap.values()) {
    263             if (input.getParentId() != null && !input.isConnectedToHdmiSwitch()) {
    264                 mInputList.remove(inputMap.get(input.getParentId()));
    265             }
    266         }
    267         Collections.sort(mInputList, mComparator);
    268 
    269         // Update the max item width.
    270         mMaxItemWidth = 0;
    271         for (TvInputInfo input : mInputList) {
    272             setItemViewText(mItemViewForMeasure, input);
    273             mItemViewForMeasure.measure(0, 0);
    274             int width = mItemViewForMeasure.getMeasuredWidth();
    275             if (width > mMaxItemWidth) {
    276                 mMaxItemWidth = width;
    277             }
    278         }
    279 
    280         getAdapter().notifyDataSetChanged();
    281     }
    282 
    283     private void setItemViewText(View v, TvInputInfo input) {
    284         TextView inputLabelView = (TextView) v.findViewById(R.id.input_label);
    285         TextView secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
    286         CharSequence customLabel = input.loadCustomLabel(getContext());
    287         CharSequence label = input.loadLabel(getContext());
    288         if (TextUtils.isEmpty(customLabel) || customLabel.equals(label)) {
    289             inputLabelView.setText(label);
    290             secondaryInputLabelView.setVisibility(View.GONE);
    291         } else {
    292             inputLabelView.setText(customLabel);
    293             secondaryInputLabelView.setText(label);
    294             secondaryInputLabelView.setVisibility(View.VISIBLE);
    295         }
    296     }
    297 
    298     private boolean isInputEnabled(TvInputInfo input) {
    299         return mTvInputManagerHelper.getInputState(input)
    300                 != TvInputManager.INPUT_STATE_DISCONNECTED;
    301     }
    302 
    303     /**
    304      * Sets a callback which receives the notifications of input selection.
    305      */
    306     public void setOnInputSelectedCallback(OnInputSelectedCallback callback) {
    307         mCallback = callback;
    308     }
    309 
    310     /**
    311      * Sets the current channel. The initial selection will be the input which contains the
    312      * {@code channel}.
    313      */
    314     public void setCurrentChannel(Channel channel) {
    315         mCurrentChannel = channel;
    316     }
    317 
    318     class InputListAdapter extends RecyclerView.Adapter<InputListAdapter.ViewHolder> {
    319         @Override
    320         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    321             View v = LayoutInflater.from(parent.getContext()).inflate(
    322                     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(new View.OnClickListener() {
    348                 @Override
    349                 public void onClick(View v) {
    350                     mSelectedInput = mInputList.get(position);
    351                     // The user made a selection. Hide this view after the ripple animation. But
    352                     // first, disable focus to avoid any further focus change during the animation.
    353                     setFocusable(false);
    354                     removeCallbacks(mHideRunnable);
    355                     postDelayed(mHideRunnable, mRippleAnimDurationMillis);
    356                 }
    357             });
    358             holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
    359                 @Override
    360                 public void onFocusChange(View view, boolean hasFocus) {
    361                     if (hasFocus) {
    362                         mSelectedInput = mInputList.get(position);
    363                     }
    364                 }
    365             });
    366 
    367             if (mResetTransitionAlpha) {
    368                 ViewUtils.setTransitionAlpha(holder.itemView, 1f);
    369             }
    370         }
    371 
    372         @Override
    373         public int getItemCount() {
    374             return mInputList.size();
    375         }
    376 
    377         class ViewHolder extends RecyclerView.ViewHolder {
    378             final TextView inputLabelView;
    379             final TextView secondaryInputLabelView;
    380 
    381             ViewHolder(View v) {
    382                 super(v);
    383                 inputLabelView = (TextView) v.findViewById(R.id.input_label);
    384                 secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label);
    385             }
    386         }
    387     }
    388 
    389     private class InputsComparator implements Comparator<TvInputInfo> {
    390         @Override
    391         public int compare(TvInputInfo lhs, TvInputInfo rhs) {
    392             if (lhs == null) {
    393                 return (rhs == null) ? 0 : 1;
    394             }
    395             if (rhs == null) {
    396                 return -1;
    397             }
    398 
    399             boolean enabledL = isInputEnabled(lhs);
    400             boolean enabledR = isInputEnabled(rhs);
    401             if (enabledL != enabledR) {
    402                 return enabledL ? -1 : 1;
    403             }
    404 
    405             int priorityL = getPriority(lhs);
    406             int priorityR = getPriority(rhs);
    407             if (priorityL != priorityR) {
    408                 return priorityR - priorityL;
    409             }
    410 
    411             String customLabelL = (String) lhs.loadCustomLabel(getContext());
    412             String customLabelR = (String) rhs.loadCustomLabel(getContext());
    413             if (!TextUtils.equals(customLabelL, customLabelR)) {
    414                 customLabelL = customLabelL == null ? "" : customLabelL;
    415                 customLabelR = customLabelR == null ? "" : customLabelR;
    416                 return customLabelL.compareToIgnoreCase(customLabelR);
    417             }
    418 
    419             String labelL = (String) lhs.loadLabel(getContext());
    420             String labelR = (String) rhs.loadLabel(getContext());
    421             labelL = labelL == null ? "" : labelL;
    422             labelR = labelR == null ? "" : labelR;
    423             return labelL.compareToIgnoreCase(labelR);
    424         }
    425 
    426         private int getPriority(TvInputInfo info) {
    427             switch (info.getType()) {
    428                 case TvInputInfo.TYPE_TUNER:
    429                     return 9;
    430                 case TvInputInfo.TYPE_HDMI:
    431                     HdmiDeviceInfo hdmiInfo = info.getHdmiDeviceInfo();
    432                     if (hdmiInfo != null && hdmiInfo.isCecDevice()) {
    433                         return 8;
    434                     }
    435                     return 7;
    436                 case TvInputInfo.TYPE_DVI:
    437                     return 6;
    438                 case TvInputInfo.TYPE_COMPONENT:
    439                     return 5;
    440                 case TvInputInfo.TYPE_SVIDEO:
    441                     return 4;
    442                 case TvInputInfo.TYPE_COMPOSITE:
    443                     return 3;
    444                 case TvInputInfo.TYPE_DISPLAY_PORT:
    445                     return 2;
    446                 case TvInputInfo.TYPE_VGA:
    447                     return 1;
    448                 case TvInputInfo.TYPE_SCART:
    449                 default:
    450                     return 0;
    451             }
    452         }
    453     }
    454 
    455     /**
    456      * A callback interface for the input selection.
    457      */
    458     public interface OnInputSelectedCallback {
    459         /**
    460          * Called when the tuner input is selected.
    461          */
    462         void onTunerInputSelected();
    463 
    464         /**
    465          * Called when the passthrough input is selected.
    466          */
    467         void onPassthroughInputSelected(@NonNull TvInputInfo input);
    468     }
    469 }
    470