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