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 36 import com.android.tv.ApplicationSingletons; 37 import com.android.tv.R; 38 import com.android.tv.TvApplication; 39 import com.android.tv.util.DurationTimer; 40 import com.android.tv.analytics.Tracker; 41 import com.android.tv.data.Channel; 42 import com.android.tv.util.TvInputManagerHelper; 43 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Map; 49 50 public class SelectInputView extends VerticalGridView implements 51 TvTransitionManager.TransitionLayout { 52 private static final String TAG = "SelectInputView"; 53 private static final boolean DEBUG = false; 54 public static final String SCREEN_NAME = "Input selection"; 55 private static final int TUNER_INPUT_POSITION = 0; 56 57 private final TvInputManagerHelper mTvInputManagerHelper; 58 private final List<TvInputInfo> mInputList = new ArrayList<>(); 59 private final TvInputManagerHelper.HardwareInputComparator mComparator; 60 private final Tracker mTracker; 61 private final DurationTimer mViewDurationTimer = new DurationTimer(); 62 private final TvInputCallback mTvInputCallback = new TvInputCallback() { 63 @Override 64 public void onInputAdded(String inputId) { 65 buildInputListAndNotify(); 66 updateSelectedPositionIfNeeded(); 67 } 68 69 @Override 70 public void onInputRemoved(String inputId) { 71 buildInputListAndNotify(); 72 updateSelectedPositionIfNeeded(); 73 } 74 75 @Override 76 public void onInputUpdated(String inputId) { 77 buildInputListAndNotify(); 78 updateSelectedPositionIfNeeded(); 79 } 80 81 @Override 82 public void onInputStateChanged(String inputId, int state) { 83 buildInputListAndNotify(); 84 updateSelectedPositionIfNeeded(); 85 } 86 87 private void updateSelectedPositionIfNeeded() { 88 if (!isFocusable() || mSelectedInput == null) { 89 return; 90 } 91 if (!isInputEnabled(mSelectedInput)) { 92 setSelectedPosition(TUNER_INPUT_POSITION); 93 return; 94 } 95 if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) { 96 setSelectedPosition(getInputPosition(mSelectedInput.getId())); 97 } 98 } 99 }; 100 101 private Channel mCurrentChannel; 102 private OnInputSelectedCallback mCallback; 103 104 private final Runnable mHideRunnable = new Runnable() { 105 @Override 106 public void run() { 107 if (mSelectedInput == null) { 108 return; 109 } 110 // TODO: pass english label to tracker http://b/22355024 111 final String label = mSelectedInput.loadLabel(getContext()).toString(); 112 mTracker.sendInputSelected(label); 113 if (mCallback != null) { 114 if (mSelectedInput.isPassthroughInput()) { 115 mCallback.onPassthroughInputSelected(mSelectedInput); 116 } else { 117 mCallback.onTunerInputSelected(); 118 } 119 } 120 } 121 }; 122 123 private final int mInputItemHeight; 124 private final long mShowDurationMillis; 125 private final long mRippleAnimDurationMillis; 126 private final int mTextColorPrimary; 127 private final int mTextColorSecondary; 128 private final int mTextColorDisabled; 129 private final View mItemViewForMeasure; 130 131 private boolean mResetTransitionAlpha; 132 private TvInputInfo mSelectedInput; 133 private int mMaxItemWidth; 134 SelectInputView(Context context)135 public SelectInputView(Context context) { 136 this(context, null, 0); 137 } 138 SelectInputView(Context context, AttributeSet attrs)139 public SelectInputView(Context context, AttributeSet attrs) { 140 this(context, attrs, 0); 141 } 142 SelectInputView(Context context, AttributeSet attrs, int defStyleAttr)143 public SelectInputView(Context context, AttributeSet attrs, int defStyleAttr) { 144 super(context, attrs, defStyleAttr); 145 setAdapter(new InputListAdapter()); 146 147 ApplicationSingletons appSingletons = TvApplication.getSingletons(context); 148 mTracker = appSingletons.getTracker(); 149 mTvInputManagerHelper = appSingletons.getTvInputManagerHelper(); 150 mComparator = 151 new TvInputManagerHelper.HardwareInputComparator(context, mTvInputManagerHelper); 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 onKeyUp(int keyCode, KeyEvent event)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 onEnterAction(boolean fromEmptyScene)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 getInputPosition(String inputId)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 onExitAction()227 public void onExitAction() { 228 mTracker.sendHideInputSelection(mViewDurationTimer.reset()); 229 mTvInputManagerHelper.removeCallback(mTvInputCallback); 230 removeCallbacks(mHideRunnable); 231 } 232 233 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 scheduleHide()240 private void scheduleHide() { 241 removeCallbacks(mHideRunnable); 242 postDelayed(mHideRunnable, mShowDurationMillis); 243 } 244 buildInputListAndNotify()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 setItemViewText(View v, TvInputInfo input)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 isInputEnabled(TvInputInfo input)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 */ 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 311 * {@code 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 = LayoutInflater.from(parent.getContext()).inflate( 321 R.layout.select_input_item, parent, false); 322 return new ViewHolder(v); 323 } 324 325 @Override onBindViewHolder(ViewHolder holder, final int position)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 getItemCount()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 ViewHolder(View v)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 /** 389 * A callback interface for the input selection. 390 */ 391 public interface OnInputSelectedCallback { 392 /** 393 * Called when the tuner input is selected. 394 */ onTunerInputSelected()395 void onTunerInputSelected(); 396 397 /** 398 * Called when the passthrough input is selected. 399 */ onPassthroughInputSelected(@onNull TvInputInfo input)400 void onPassthroughInputSelected(@NonNull TvInputInfo input); 401 } 402 } 403