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 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 isInputEnabled(TvInputInfo input)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. */ 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 {@code 311 * 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 = 321 LayoutInflater.from(parent.getContext()) 322 .inflate(R.layout.select_input_item, parent, false); 323 return new ViewHolder(v); 324 } 325 326 @Override onBindViewHolder(ViewHolder holder, final int position)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 getItemCount()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 ViewHolder(View v)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. */ onTunerInputSelected()396 void onTunerInputSelected(); 397 398 /** Called when the passthrough input is selected. */ onPassthroughInputSelected(@onNull TvInputInfo input)399 void onPassthroughInputSelected(@NonNull TvInputInfo input); 400 } 401 } 402