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