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 androidx.leanback.widget.VerticalGridView; 26 import androidx.recyclerview.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 SelectInputView(Context context)134 public SelectInputView(Context context) { 135 this(context, null, 0); 136 } 137 SelectInputView(Context context, AttributeSet attrs)138 public SelectInputView(Context context, AttributeSet attrs) { 139 this(context, attrs, 0); 140 } 141 SelectInputView(Context context, AttributeSet attrs, int defStyleAttr)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 onKeyUp(int keyCode, KeyEvent event)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 onEnterAction(boolean fromEmptyScene)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 getInputPosition(String inputId)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 onExitAction()228 public void onExitAction() { 229 mTracker.sendHideInputSelection(mViewDurationTimer.reset()); 230 mTvInputManagerHelper.removeCallback(mTvInputCallback); 231 removeCallbacks(mHideRunnable); 232 } 233 234 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 scheduleHide()242 private void scheduleHide() { 243 removeCallbacks(mHideRunnable); 244 postDelayed(mHideRunnable, mShowDurationMillis); 245 } 246 buildInputListAndNotify()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 setItemViewText(View v, TvInputInfo input)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 if (input.isPassthroughInput()) { 291 inputLabelView.setText(label); 292 } else { 293 inputLabelView.setText(R.string.input_long_label_for_tuner); 294 } 295 secondaryInputLabelView.setVisibility(View.GONE); 296 } else { 297 if (input.isPassthroughInput()) { 298 inputLabelView.setText(customLabel); 299 } else { 300 inputLabelView.setText(R.string.input_long_label_for_tuner); 301 } 302 secondaryInputLabelView.setText(label); 303 secondaryInputLabelView.setVisibility(View.VISIBLE); 304 } 305 } 306 isInputEnabled(TvInputInfo input)307 private boolean isInputEnabled(TvInputInfo input) { 308 return mTvInputManagerHelper.getInputState(input) 309 != TvInputManager.INPUT_STATE_DISCONNECTED; 310 } 311 312 /** Sets a callback which receives the notifications of input selection. */ setOnInputSelectedCallback(OnInputSelectedCallback callback)313 public void setOnInputSelectedCallback(OnInputSelectedCallback callback) { 314 mCallback = callback; 315 } 316 317 /** 318 * Sets the current channel. The initial selection will be the input which contains the {@code 319 * channel}. 320 */ setCurrentChannel(Channel channel)321 public void setCurrentChannel(Channel channel) { 322 mCurrentChannel = channel; 323 } 324 325 class InputListAdapter extends RecyclerView.Adapter<InputListAdapter.ViewHolder> { 326 @Override onCreateViewHolder(ViewGroup parent, int viewType)327 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 328 View v = 329 LayoutInflater.from(parent.getContext()) 330 .inflate(R.layout.select_input_item, parent, false); 331 return new ViewHolder(v); 332 } 333 334 @Override onBindViewHolder(ViewHolder holder, final int position)335 public void onBindViewHolder(ViewHolder holder, final int position) { 336 TvInputInfo input = mInputList.get(position); 337 if (input.isPassthroughInput()) { 338 if (isInputEnabled(input)) { 339 holder.itemView.setFocusable(true); 340 holder.inputLabelView.setTextColor(mTextColorPrimary); 341 holder.secondaryInputLabelView.setTextColor(mTextColorSecondary); 342 } else { 343 holder.itemView.setFocusable(false); 344 holder.inputLabelView.setTextColor(mTextColorDisabled); 345 holder.secondaryInputLabelView.setTextColor(mTextColorDisabled); 346 } 347 setItemViewText(holder.itemView, input); 348 } else { 349 holder.itemView.setFocusable(true); 350 holder.inputLabelView.setTextColor(mTextColorPrimary); 351 holder.inputLabelView.setText(R.string.input_long_label_for_tuner); 352 holder.secondaryInputLabelView.setVisibility(View.GONE); 353 } 354 355 holder.itemView.setOnClickListener( 356 new View.OnClickListener() { 357 @Override 358 public void onClick(View v) { 359 mSelectedInput = mInputList.get(position); 360 // The user made a selection. Hide this view after the ripple animation. 361 // But 362 // first, disable focus to avoid any further focus change during the 363 // animation. 364 setFocusable(false); 365 removeCallbacks(mHideRunnable); 366 postDelayed(mHideRunnable, mRippleAnimDurationMillis); 367 } 368 }); 369 holder.itemView.setOnFocusChangeListener( 370 new View.OnFocusChangeListener() { 371 @Override 372 public void onFocusChange(View view, boolean hasFocus) { 373 if (hasFocus) { 374 mSelectedInput = mInputList.get(position); 375 } 376 } 377 }); 378 379 if (mResetTransitionAlpha) { 380 ViewUtils.setTransitionAlpha(holder.itemView, 1f); 381 } 382 } 383 384 @Override getItemCount()385 public int getItemCount() { 386 return mInputList.size(); 387 } 388 389 class ViewHolder extends RecyclerView.ViewHolder { 390 final TextView inputLabelView; 391 final TextView secondaryInputLabelView; 392 ViewHolder(View v)393 ViewHolder(View v) { 394 super(v); 395 inputLabelView = (TextView) v.findViewById(R.id.input_label); 396 secondaryInputLabelView = (TextView) v.findViewById(R.id.secondary_input_label); 397 } 398 } 399 } 400 401 /** A callback interface for the input selection. */ 402 public interface OnInputSelectedCallback { 403 /** Called when the tuner input is selected. */ onTunerInputSelected()404 void onTunerInputSelected(); 405 406 /** Called when the passthrough input is selected. */ onPassthroughInputSelected(@onNull TvInputInfo input)407 void onPassthroughInputSelected(@NonNull TvInputInfo input); 408 } 409 } 410