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.media.tv.TvInputInfo;
     22 import android.media.tv.TvInputManager;
     23 import android.media.tv.TvInputManager.TvInputCallback;
     24 import android.support.annotation.NonNull;
     25 import android.support.v17.leanback.widget.VerticalGridView;
     26 import android.support.v7.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 
    134     public SelectInputView(Context context) {
    135         this(context, null, 0);
    136     }
    137 
    138     public SelectInputView(Context context, AttributeSet attrs) {
    139         this(context, attrs, 0);
    140     }
    141 
    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
    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
    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 
    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(
    238                 MeasureSpec.makeMeasureSpec(mMaxItemWidth, MeasureSpec.EXACTLY),
    239                 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
    240     }
    241 
    242     private void scheduleHide() {
    243         removeCallbacks(mHideRunnable);
    244         postDelayed(mHideRunnable, mShowDurationMillis);
    245     }
    246 
    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 
    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 
    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. */
    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      */
    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 =
    321                     LayoutInflater.from(parent.getContext())
    322                             .inflate(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(
    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
    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 
    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. */
    396         void onTunerInputSelected();
    397 
    398         /** Called when the passthrough input is selected. */
    399         void onPassthroughInputSelected(@NonNull TvInputInfo input);
    400     }
    401 }
    402