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