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.guide;
18 
19 import static com.android.tv.util.ImageLoader.ImageLoaderCallback;
20 
21 import android.animation.Animator;
22 import android.animation.ObjectAnimator;
23 import android.animation.PropertyValuesHolder;
24 import android.content.Context;
25 import android.content.res.ColorStateList;
26 import android.content.res.Resources;
27 import android.graphics.Bitmap;
28 import android.media.tv.TvContentRating;
29 import android.media.tv.TvInputInfo;
30 import android.os.Handler;
31 import android.support.annotation.NonNull;
32 import android.support.annotation.Nullable;
33 import android.support.v7.widget.RecyclerView;
34 import android.support.v7.widget.RecyclerView.RecycledViewPool;
35 import android.text.Spannable;
36 import android.text.SpannableString;
37 import android.text.TextUtils;
38 import android.text.style.TextAppearanceSpan;
39 import android.util.Log;
40 import android.util.TypedValue;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.ImageView;
45 import android.widget.TextView;
46 
47 import com.android.tv.R;
48 import com.android.tv.TvApplication;
49 import com.android.tv.data.Channel;
50 import com.android.tv.data.Program;
51 import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener;
52 import com.android.tv.parental.ParentalControlSettings;
53 import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
54 import com.android.tv.util.ImageCache;
55 import com.android.tv.util.ImageLoader;
56 import com.android.tv.util.ImageLoader.LoadTvInputLogoTask;
57 import com.android.tv.util.TvInputManagerHelper;
58 import com.android.tv.util.Utils;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 
63 /**
64  * Adapts the {@link ProgramListAdapter} list to the body of the program guide table.
65  */
66 public 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 
ProgramTableAdapter(Context context, ProgramManager programManager, ProgramGuide programGuide)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 
update()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
getItemCount()168     public int getItemCount() {
169         return mProgramListAdapters.size();
170     }
171 
172     @Override
getItemViewType(int position)173     public int getItemViewType(int position) {
174         return R.layout.program_guide_table_row;
175     }
176 
177     @Override
onBindViewHolder(ProgramRowHolder holder, int position)178     public void onBindViewHolder(ProgramRowHolder holder, int position) {
179         holder.onBind(position);
180     }
181 
182     @Override
onCreateViewHolder(ViewGroup parent, int viewType)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
onTableEntryChanged(ProgramManager.TableEntry tableEntry)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 
ProgramRowHolder(View itemView)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 
onBind(int position)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