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.guide; 18 19import static com.android.tv.util.ImageLoader.ImageLoaderCallback; 20 21import android.animation.Animator; 22import android.animation.ObjectAnimator; 23import android.animation.PropertyValuesHolder; 24import android.content.Context; 25import android.content.res.ColorStateList; 26import android.content.res.Resources; 27import android.graphics.Bitmap; 28import android.media.tv.TvContentRating; 29import android.media.tv.TvInputInfo; 30import android.os.Handler; 31import android.support.annotation.NonNull; 32import android.support.annotation.Nullable; 33import android.support.v7.widget.RecyclerView; 34import android.support.v7.widget.RecyclerView.RecycledViewPool; 35import android.text.Spannable; 36import android.text.SpannableString; 37import android.text.TextUtils; 38import android.text.style.TextAppearanceSpan; 39import android.util.Log; 40import android.util.TypedValue; 41import android.view.LayoutInflater; 42import android.view.View; 43import android.view.ViewGroup; 44import android.widget.ImageView; 45import android.widget.TextView; 46 47import com.android.tv.R; 48import com.android.tv.TvApplication; 49import com.android.tv.data.Channel; 50import com.android.tv.data.Program; 51import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; 52import com.android.tv.parental.ParentalControlSettings; 53import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; 54import com.android.tv.util.ImageCache; 55import com.android.tv.util.ImageLoader; 56import com.android.tv.util.ImageLoader.LoadTvInputLogoTask; 57import com.android.tv.util.TvInputManagerHelper; 58import com.android.tv.util.Utils; 59 60import java.util.ArrayList; 61import java.util.List; 62 63/** 64 * Adapts the {@link ProgramListAdapter} list to the body of the program guide table. 65 */ 66public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowHolder> 67 implements ProgramManager.TableEntryChangedListener { 68 private static final String TAG = "ProgramTableAdapter"; 69 private static final boolean DEBUG = false; 70 71 private final Context mContext; 72 private final TvInputManagerHelper mTvInputManagerHelper; 73 private final ProgramManager mProgramManager; 74 private final ProgramGuide mProgramGuide; 75 private final Handler mHandler = new Handler(); 76 private final List<ProgramListAdapter> mProgramListAdapters = new ArrayList<>(); 77 private final RecycledViewPool mRecycledViewPool; 78 79 private final int mChannelLogoWidth; 80 private final int mChannelLogoHeight; 81 private final int mImageWidth; 82 private final int mImageHeight; 83 private final String mProgramTitleForNoInformation; 84 private final String mProgramTitleForBlockedChannel; 85 private final int mChannelTextColor; 86 private final int mChannelBlockedTextColor; 87 private final int mDetailTextColor; 88 private final int mDetailGrayedTextColor; 89 private final int mAnimationDuration; 90 private final int mDetailPadding; 91 private final TextAppearanceSpan mEpisodeTitleStyle; 92 93 public ProgramTableAdapter(Context context, ProgramManager programManager, 94 ProgramGuide programGuide) { 95 mContext = context; 96 mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper(); 97 mProgramManager = programManager; 98 mProgramGuide = programGuide; 99 100 Resources res = context.getResources(); 101 mChannelLogoWidth = res.getDimensionPixelSize( 102 R.dimen.program_guide_table_header_column_channel_logo_width); 103 mChannelLogoHeight = res.getDimensionPixelSize( 104 R.dimen.program_guide_table_header_column_channel_logo_height); 105 mImageWidth = res.getDimensionPixelSize( 106 R.dimen.program_guide_table_detail_image_width); 107 mImageHeight = res.getDimensionPixelSize( 108 R.dimen.program_guide_table_detail_image_height); 109 mProgramTitleForNoInformation = res.getString( 110 R.string.program_title_for_no_information); 111 mProgramTitleForBlockedChannel = res.getString( 112 R.string.program_title_for_blocked_channel); 113 mChannelTextColor = Utils.getColor(res, 114 R.color.program_guide_table_header_column_channel_number_text_color); 115 mChannelBlockedTextColor = Utils.getColor(res, 116 R.color.program_guide_table_header_column_channel_number_blocked_text_color); 117 mDetailTextColor = Utils.getColor(res, 118 R.color.program_guide_table_detail_title_text_color); 119 mDetailGrayedTextColor = Utils.getColor(res, 120 R.color.program_guide_table_detail_title_grayed_text_color); 121 mAnimationDuration = 122 res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration); 123 mDetailPadding = res.getDimensionPixelOffset( 124 R.dimen.program_guide_table_detail_padding); 125 126 int episodeTitleSize = res.getDimensionPixelSize( 127 R.dimen.program_guide_table_detail_episode_title_text_size); 128 ColorStateList episodeTitleColor = ColorStateList.valueOf( 129 Utils.getColor(res, R.color.program_guide_table_detail_episode_title_text_color)); 130 mEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize, 131 episodeTitleColor, null); 132 133 mRecycledViewPool = new RecycledViewPool(); 134 mRecycledViewPool.setMaxRecycledViews(R.layout.program_guide_table_item, 135 context.getResources().getInteger( 136 R.integer.max_recycled_view_pool_epg_table_item)); 137 mProgramManager.addListener(new ProgramManager.ListenerAdapter() { 138 @Override 139 public void onChannelsUpdated() { 140 mHandler.post(new Runnable() { 141 @Override 142 public void run() { 143 update(); 144 } 145 }); 146 } 147 }); 148 update(); 149 mProgramManager.addTableEntryChangedListener(this); 150 } 151 152 private void update() { 153 if (DEBUG) Log.d(TAG, "update " + mProgramManager.getChannelCount() + " channels"); 154 for (TableEntriesUpdatedListener listener : mProgramListAdapters) { 155 mProgramManager.removeTableEntriesUpdatedListener(listener); 156 } 157 mProgramListAdapters.clear(); 158 for (int i = 0; i < mProgramManager.getChannelCount(); i++) { 159 ProgramListAdapter listAdapter = new ProgramListAdapter(mContext.getResources(), 160 mProgramManager, i); 161 mProgramManager.addTableEntriesUpdatedListener(listAdapter); 162 mProgramListAdapters.add(listAdapter); 163 } 164 notifyDataSetChanged(); 165 } 166 167 @Override 168 public int getItemCount() { 169 return mProgramListAdapters.size(); 170 } 171 172 @Override 173 public int getItemViewType(int position) { 174 return R.layout.program_guide_table_row; 175 } 176 177 @Override 178 public void onBindViewHolder(ProgramRowHolder holder, int position) { 179 holder.onBind(position); 180 } 181 182 @Override 183 public ProgramRowHolder onCreateViewHolder(ViewGroup parent, int viewType) { 184 View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); 185 ProgramRow programRow = (ProgramRow) itemView.findViewById(R.id.row); 186 programRow.setRecycledViewPool(mRecycledViewPool); 187 return new ProgramRowHolder(itemView); 188 } 189 190 @Override 191 public void onTableEntryChanged(ProgramManager.TableEntry tableEntry) { 192 int channelIndex = mProgramManager.getChannelIndex(tableEntry.channelId); 193 int pos = mProgramManager.getProgramIdIndex(tableEntry.channelId, tableEntry.getId()); 194 if (DEBUG) Log.d(TAG, "update(" + channelIndex + ", " + pos + ")"); 195 mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry); 196 } 197 198 // TODO: make it static 199 public class ProgramRowHolder extends RecyclerView.ViewHolder 200 implements ProgramRow.ChildFocusListener { 201 202 private final ViewGroup mContainer; 203 private final ProgramRow mProgramRow; 204 private ProgramManager.TableEntry mSelectedEntry; 205 private Animator mDetailOutAnimator; 206 private Animator mDetailInAnimator; 207 private final Runnable mDetailInStarter = new Runnable() { 208 @Override 209 public void run() { 210 mProgramRow.removeOnScrollListener(mOnScrollListener); 211 if (mDetailInAnimator != null) { 212 mDetailInAnimator.start(); 213 } 214 } 215 }; 216 217 private final RecyclerView.OnScrollListener mOnScrollListener = 218 new RecyclerView.OnScrollListener() { 219 @Override 220 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 221 onHorizontalScrolled(); 222 } 223 }; 224 225 // Members of Program Details 226 private final ViewGroup mDetailView; 227 private final ImageView mImageView; 228 private final ImageView mBlockView; 229 private final TextView mTitleView; 230 private final TextView mTimeView; 231 private final TextView mDescriptionView; 232 private final TextView mAspectRatioView; 233 private final TextView mResolutionView; 234 235 // Members of Channel Header 236 private Channel mChannel; 237 private final View mChannelHeaderView; 238 private final TextView mChannelNumberView; 239 private final TextView mChannelNameView; 240 private final ImageView mChannelLogoView; 241 private final ImageView mChannelBlockView; 242 private final ImageView mInputLogoView; 243 244 private boolean mIsInputLogoVisible; 245 246 public ProgramRowHolder(View itemView) { 247 super(itemView); 248 249 mContainer = (ViewGroup) itemView; 250 mProgramRow = (ProgramRow) mContainer.findViewById(R.id.row); 251 252 mDetailView = (ViewGroup) mContainer.findViewById(R.id.detail); 253 mImageView = (ImageView) mDetailView.findViewById(R.id.image); 254 mBlockView = (ImageView) mDetailView.findViewById(R.id.block); 255 mTitleView = (TextView) mDetailView.findViewById(R.id.title); 256 mTimeView = (TextView) mDetailView.findViewById(R.id.time); 257 mDescriptionView = (TextView) mDetailView.findViewById(R.id.desc); 258 mAspectRatioView = (TextView) mDetailView.findViewById(R.id.aspect_ratio); 259 mResolutionView = (TextView) mDetailView.findViewById(R.id.resolution); 260 261 mChannelHeaderView = mContainer.findViewById(R.id.header_column); 262 mChannelNumberView = (TextView) mContainer.findViewById(R.id.channel_number); 263 mChannelNameView = (TextView) mContainer.findViewById(R.id.channel_name); 264 mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo); 265 mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block); 266 mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo); 267 } 268 269 public void onBind(int position) { 270 onBindChannel(mProgramManager.getChannel(position)); 271 272 mProgramRow.swapAdapter(mProgramListAdapters.get(position), true); 273 mProgramRow.setProgramManager(mProgramManager); 274 mProgramRow.setChannel(mProgramManager.getChannel(position)); 275 mProgramRow.setChildFocusListener(this); 276 mProgramRow.resetScroll(mProgramGuide.getTimelineRowScrollOffset()); 277 278 mDetailView.setVisibility(View.GONE); 279 280 // The bottom-left of the last channel header view will have a rounded corner. 281 mChannelHeaderView.setBackgroundResource((position < mProgramListAdapters.size() - 1) 282 ? R.drawable.program_guide_table_header_column_item_background 283 : R.drawable.program_guide_table_header_column_last_item_background); 284 } 285 286 private void onBindChannel(Channel channel) { 287 if (DEBUG) Log.d(TAG, "onBindChannel " + channel); 288 289 mChannel = channel; 290 mInputLogoView.setVisibility(View.GONE); 291 mIsInputLogoVisible = false; 292 if (channel == null) { 293 mChannelNumberView.setVisibility(View.GONE); 294 mChannelNameView.setVisibility(View.GONE); 295 mChannelLogoView.setVisibility(View.GONE); 296 mChannelBlockView.setVisibility(View.GONE); 297 return; 298 } 299 300 String displayNumber = channel.getDisplayNumber(); 301 if (displayNumber == null) { 302 mChannelNumberView.setVisibility(View.GONE); 303 } else { 304 int size; 305 if (displayNumber.length() <= 4) { 306 size = R.dimen.program_guide_table_header_column_channel_number_large_font_size; 307 } else { 308 size = R.dimen.program_guide_table_header_column_channel_number_small_font_size; 309 } 310 mChannelNumberView.setTextSize(TypedValue.COMPLEX_UNIT_PX, 311 mChannelNumberView.getContext().getResources().getDimension(size)); 312 mChannelNumberView.setText(displayNumber); 313 mChannelNumberView.setVisibility(View.VISIBLE); 314 } 315 mChannelNumberView.setTextColor( 316 isChannelLocked(channel) ? mChannelBlockedTextColor : mChannelTextColor); 317 318 mChannelLogoView.setImageBitmap(null); 319 mChannelLogoView.setVisibility(View.GONE); 320 if (isChannelLocked(channel)) { 321 mChannelNameView.setVisibility(View.GONE); 322 mChannelBlockView.setVisibility(View.VISIBLE); 323 } else { 324 mChannelNameView.setText(channel.getDisplayName()); 325 mChannelNameView.setVisibility(View.VISIBLE); 326 mChannelBlockView.setVisibility(View.GONE); 327 328 mChannel.loadBitmap(itemView.getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, 329 mChannelLogoWidth, mChannelLogoHeight, 330 createChannelLogoLoadedCallback(this, channel.getId())); 331 } 332 } 333 334 @Override 335 public void onChildFocus(View oldFocus, View newFocus) { 336 if (newFocus == null) { 337 return; 338 } 339 mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry(); 340 if (oldFocus == null) { 341 updateDetailView(); 342 return; 343 } 344 345 if (Program.isValid(mSelectedEntry.program)) { 346 Program program = mSelectedEntry.program; 347 if (getProgramBlock(program) == null) { 348 program.prefetchPosterArt(itemView.getContext(), mImageWidth, mImageHeight); 349 } 350 } 351 352 // -1 means the selection goes rightwards and 1 goes leftwards 353 int direction = oldFocus.getLeft() < newFocus.getLeft() ? -1 : 1; 354 View detailContentView = mDetailView.findViewById(R.id.detail_content); 355 356 if (mDetailInAnimator == null) { 357 mDetailOutAnimator = ObjectAnimator.ofPropertyValuesHolder(detailContentView, 358 PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), 359 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 360 0f, direction * mDetailPadding)); 361 mDetailOutAnimator.setDuration(mAnimationDuration); 362 mDetailOutAnimator.addListener( 363 new HardwareLayerAnimatorListenerAdapter(detailContentView) { 364 @Override 365 public void onAnimationEnd(Animator animator) { 366 super.onAnimationEnd(animator); 367 mDetailOutAnimator = null; 368 mHandler.removeCallbacks(mDetailInStarter); 369 mHandler.postDelayed(mDetailInStarter, mAnimationDuration); 370 } 371 }); 372 373 mProgramRow.addOnScrollListener(mOnScrollListener); 374 mDetailOutAnimator.start(); 375 } else { 376 if (mDetailInAnimator.isStarted()) { 377 mDetailInAnimator.cancel(); 378 detailContentView.setAlpha(0); 379 } 380 381 mHandler.removeCallbacks(mDetailInStarter); 382 mHandler.postDelayed(mDetailInStarter, mAnimationDuration); 383 } 384 385 mDetailInAnimator = ObjectAnimator.ofPropertyValuesHolder(detailContentView, 386 PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), 387 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 388 direction * -mDetailPadding, 0f)); 389 mDetailInAnimator.setDuration(mAnimationDuration); 390 mDetailInAnimator.addListener( 391 new HardwareLayerAnimatorListenerAdapter(detailContentView) { 392 @Override 393 public void onAnimationStart(Animator animator) { 394 super.onAnimationStart(animator); 395 updateDetailView(); 396 } 397 398 @Override 399 public void onAnimationEnd(Animator animator) { 400 super.onAnimationEnd(animator); 401 mDetailInAnimator = null; 402 } 403 }); 404 } 405 406 private void updateDetailView() { 407 if (Program.isValid(mSelectedEntry.program)) { 408 mTitleView.setTextColor(mDetailTextColor); 409 Context context = itemView.getContext(); 410 Program program = mSelectedEntry.program; 411 412 TvContentRating blockedRating = getProgramBlock(program); 413 414 updatePosterArt(null); 415 if (blockedRating == null) { 416 program.loadPosterArt(context, mImageWidth, mImageHeight, 417 createProgramPosterArtCallback(this, program)); 418 } 419 420 if (TextUtils.isEmpty(program.getEpisodeTitle())) { 421 mTitleView.setText(program.getTitle()); 422 } else { 423 String title = program.getTitle(); 424 String episodeTitle = program.getEpisodeDisplayTitle(mContext); 425 String fullTitle = title + " " + episodeTitle; 426 427 SpannableString text = new SpannableString(fullTitle); 428 text.setSpan(mEpisodeTitleStyle, 429 fullTitle.length() - episodeTitle.length(), fullTitle.length(), 430 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 431 mTitleView.setText(text); 432 } 433 434 updateTextView(mTimeView, Utils.getDurationString(context, 435 program.getStartTimeUtcMillis(), 436 program.getEndTimeUtcMillis(), false)); 437 438 if (blockedRating == null) { 439 mBlockView.setVisibility(View.GONE); 440 updateTextView(mDescriptionView, program.getDescription()); 441 } else { 442 mBlockView.setVisibility(View.VISIBLE); 443 updateTextView(mDescriptionView, getBlockedDescription(blockedRating)); 444 } 445 446 updateTextView(mAspectRatioView, Utils.getAspectRatioString( 447 program.getVideoWidth(), program.getVideoHeight())); 448 449 int videoDefinitionLevel = Utils.getVideoDefinitionLevelFromSize( 450 program.getVideoWidth(), program.getVideoHeight()); 451 updateTextView(mResolutionView, Utils.getVideoDefinitionLevelString( 452 context, videoDefinitionLevel)); 453 } else { 454 mTitleView.setTextColor(mDetailGrayedTextColor); 455 if (mSelectedEntry.isBlocked()) { 456 updateTextView(mTitleView, mProgramTitleForBlockedChannel); 457 } else { 458 updateTextView(mTitleView, mProgramTitleForNoInformation); 459 } 460 mImageView.setVisibility(View.GONE); 461 mBlockView.setVisibility(View.GONE); 462 mTimeView.setVisibility(View.GONE); 463 mDescriptionView.setVisibility(View.GONE); 464 mAspectRatioView.setVisibility(View.GONE); 465 mResolutionView.setVisibility(View.GONE); 466 } 467 } 468 469 private TvContentRating getProgramBlock(Program program) { 470 ParentalControlSettings parental = mTvInputManagerHelper.getParentalControlSettings(); 471 if (!parental.isParentalControlsEnabled()) { 472 return null; 473 } 474 return parental.getBlockedRating(program.getContentRatings()); 475 } 476 477 private boolean isChannelLocked(Channel channel) { 478 return mTvInputManagerHelper.getParentalControlSettings().isParentalControlsEnabled() 479 && channel.isLocked(); 480 } 481 482 private String getBlockedDescription(TvContentRating blockedRating) { 483 String name = mTvInputManagerHelper.getContentRatingsManager() 484 .getDisplayNameForRating(blockedRating); 485 if (TextUtils.isEmpty(name)) { 486 return mContext.getString(R.string.program_guide_content_locked); 487 } else { 488 return mContext.getString(R.string.program_guide_content_locked_format, name); 489 } 490 } 491 492 /** 493 * Update tv input logo. It should be called when the visible child item in ProgramGrid 494 * changed. 495 */ 496 public void updateInputLogo(int lastPosition, boolean forceShow) { 497 if (mChannel == null) { 498 mInputLogoView.setVisibility(View.GONE); 499 mIsInputLogoVisible = false; 500 return; 501 } 502 503 boolean showLogo = forceShow; 504 if (!showLogo) { 505 Channel lastChannel = mProgramManager.getChannel(lastPosition); 506 if (lastChannel == null 507 || !mChannel.getInputId().equals(lastChannel.getInputId())) { 508 showLogo = true; 509 } 510 } 511 512 if (showLogo) { 513 if (!mIsInputLogoVisible) { 514 mIsInputLogoVisible = true; 515 TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(mChannel.getInputId()); 516 if (info != null) { 517 LoadTvInputLogoTask task = new LoadTvInputLogoTask( 518 itemView.getContext(), ImageCache.getInstance(), info); 519 ImageLoader.loadBitmap(createTvInputLogoLoadedCallback(info, this), task); 520 } 521 } 522 } else { 523 mInputLogoView.setVisibility(View.GONE); 524 mInputLogoView.setImageDrawable(null); 525 mIsInputLogoVisible = false; 526 } 527 } 528 529 private void updateTextView(TextView textView, String text) { 530 if (!TextUtils.isEmpty(text)) { 531 textView.setVisibility(View.VISIBLE); 532 textView.setText(text); 533 } else { 534 textView.setVisibility(View.GONE); 535 } 536 } 537 538 private void updatePosterArt(@Nullable Bitmap posterArt) { 539 mImageView.setImageBitmap(posterArt); 540 mImageView.setVisibility(posterArt == null ? View.GONE : View.VISIBLE); 541 } 542 543 private void updateChannelLogo(@Nullable Bitmap logo) { 544 mChannelLogoView.setImageBitmap(logo); 545 mChannelNameView.setVisibility(View.GONE); 546 mChannelLogoView.setVisibility(View.VISIBLE); 547 } 548 549 private void updateInputLogoInternal(@NonNull Bitmap tvInputLogo) { 550 if (!mIsInputLogoVisible) { 551 return; 552 } 553 mInputLogoView.setImageBitmap(tvInputLogo); 554 mInputLogoView.setVisibility(View.VISIBLE); 555 } 556 557 private void onHorizontalScrolled() { 558 if (mDetailInAnimator != null) { 559 mHandler.removeCallbacks(mDetailInStarter); 560 mHandler.postDelayed(mDetailInStarter, mAnimationDuration); 561 } 562 } 563 } 564 565 private static ImageLoaderCallback<ProgramRowHolder> createProgramPosterArtCallback( 566 ProgramRowHolder holder, final Program program) { 567 return new ImageLoaderCallback<ProgramRowHolder>(holder) { 568 @Override 569 public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap posterArt) { 570 if (posterArt == null || holder.mSelectedEntry == null 571 || holder.mSelectedEntry.program == null) { 572 return; 573 } 574 String posterArtUri = holder.mSelectedEntry.program.getPosterArtUri(); 575 if (posterArtUri == null || !posterArtUri.equals(program.getPosterArtUri())) { 576 return; 577 } 578 holder.updatePosterArt(posterArt); 579 } 580 }; 581 } 582 583 private static ImageLoaderCallback<ProgramRowHolder> createChannelLogoLoadedCallback( 584 ProgramRowHolder holder, final long channelId) { 585 return new ImageLoaderCallback<ProgramRowHolder>(holder) { 586 @Override 587 public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) { 588 if (logo == null || holder.mChannel == null 589 || holder.mChannel.getId() != channelId) { 590 return; 591 } 592 holder.updateChannelLogo(logo); 593 } 594 }; 595 } 596 597 private static ImageLoaderCallback<ProgramRowHolder> createTvInputLogoLoadedCallback( 598 final TvInputInfo info, ProgramRowHolder holder) { 599 return new ImageLoaderCallback<ProgramRowHolder>(holder) { 600 @Override 601 public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) { 602 if (logo != null && info.getId() 603 .equals(holder.mChannel.getInputId())) { 604 holder.updateInputLogoInternal(logo); 605 } 606 } 607 }; 608 } 609} 610