Home | History | Annotate | Download | only in guide
      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 
     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