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 android.animation.Animator;
     20 import android.animation.ObjectAnimator;
     21 import android.animation.PropertyValuesHolder;
     22 import android.content.Context;
     23 import android.content.res.ColorStateList;
     24 import android.content.res.Resources;
     25 import android.graphics.Bitmap;
     26 import android.media.tv.TvContentRating;
     27 import android.media.tv.TvInputInfo;
     28 import android.os.Handler;
     29 import android.support.annotation.NonNull;
     30 import android.support.annotation.Nullable;
     31 import android.support.v7.widget.RecyclerView;
     32 import android.support.v7.widget.RecyclerView.RecycledViewPool;
     33 import android.text.Html;
     34 import android.text.Spannable;
     35 import android.text.SpannableString;
     36 import android.text.TextUtils;
     37 import android.text.style.TextAppearanceSpan;
     38 import android.util.Log;
     39 import android.util.TypedValue;
     40 import android.view.LayoutInflater;
     41 import android.view.View;
     42 import android.view.ViewGroup;
     43 import android.view.ViewParent;
     44 import android.view.ViewTreeObserver;
     45 import android.view.accessibility.AccessibilityManager;
     46 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
     47 import android.widget.ImageView;
     48 import android.widget.LinearLayout;
     49 import android.widget.TextView;
     50 import com.android.tv.R;
     51 import com.android.tv.TvSingletons;
     52 import com.android.tv.common.feature.CommonFeatures;
     53 import com.android.tv.common.util.CommonUtils;
     54 import com.android.tv.data.Program;
     55 import com.android.tv.data.Program.CriticScore;
     56 import com.android.tv.data.api.Channel;
     57 import com.android.tv.dvr.DvrDataManager;
     58 import com.android.tv.dvr.DvrManager;
     59 import com.android.tv.dvr.data.ScheduledRecording;
     60 import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener;
     61 import com.android.tv.parental.ParentalControlSettings;
     62 import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
     63 import com.android.tv.util.TvInputManagerHelper;
     64 import com.android.tv.util.Utils;
     65 import com.android.tv.util.images.ImageCache;
     66 import com.android.tv.util.images.ImageLoader;
     67 import com.android.tv.util.images.ImageLoader.ImageLoaderCallback;
     68 import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask;
     69 
     70 import java.util.ArrayList;
     71 import java.util.List;
     72 
     73 /** Adapts the {@link ProgramListAdapter} list to the body of the program guide table. */
     74 class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowViewHolder>
     75         implements ProgramManager.TableEntryChangedListener {
     76     private static final String TAG = "ProgramTableAdapter";
     77     private static final boolean DEBUG = false;
     78 
     79     private final Context mContext;
     80     private final TvInputManagerHelper mTvInputManagerHelper;
     81     private final DvrManager mDvrManager;
     82     private final DvrDataManager mDvrDataManager;
     83     private final ProgramManager mProgramManager;
     84     private final AccessibilityManager mAccessibilityManager;
     85     private final ProgramGuide mProgramGuide;
     86     private final Handler mHandler = new Handler();
     87     private final List<ProgramListAdapter> mProgramListAdapters = new ArrayList<>();
     88     private final RecycledViewPool mRecycledViewPool;
     89     // views to be be reused when displaying critic scores
     90     private final List<LinearLayout> mCriticScoreViews;
     91 
     92     private final int mChannelLogoWidth;
     93     private final int mChannelLogoHeight;
     94     private final int mImageWidth;
     95     private final int mImageHeight;
     96     private final String mProgramTitleForNoInformation;
     97     private final String mProgramTitleForBlockedChannel;
     98     private final int mChannelTextColor;
     99     private final int mChannelBlockedTextColor;
    100     private final int mDetailTextColor;
    101     private final int mDetailGrayedTextColor;
    102     private final int mAnimationDuration;
    103     private final int mDetailPadding;
    104     private final TextAppearanceSpan mEpisodeTitleStyle;
    105     private final String mProgramRecordableText;
    106     private final String mRecordingScheduledText;
    107     private final String mRecordingConflictText;
    108     private final String mRecordingFailedText;
    109     private final String mRecordingInProgressText;
    110     private final int mDvrPaddingStartWithTrack;
    111     private final int mDvrPaddingStartWithOutTrack;
    112 
    113     ProgramTableAdapter(Context context, ProgramGuide programGuide) {
    114         mContext = context;
    115         mAccessibilityManager =
    116                 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
    117         mTvInputManagerHelper = TvSingletons.getSingletons(context).getTvInputManagerHelper();
    118         if (CommonFeatures.DVR.isEnabled(context)) {
    119             mDvrManager = TvSingletons.getSingletons(context).getDvrManager();
    120             mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
    121         } else {
    122             mDvrManager = null;
    123             mDvrDataManager = null;
    124         }
    125         mProgramGuide = programGuide;
    126         mProgramManager = programGuide.getProgramManager();
    127 
    128         Resources res = context.getResources();
    129         mChannelLogoWidth =
    130                 res.getDimensionPixelSize(
    131                         R.dimen.program_guide_table_header_column_channel_logo_width);
    132         mChannelLogoHeight =
    133                 res.getDimensionPixelSize(
    134                         R.dimen.program_guide_table_header_column_channel_logo_height);
    135         mImageWidth = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_image_width);
    136         mImageHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_image_height);
    137         mProgramTitleForNoInformation = res.getString(R.string.program_title_for_no_information);
    138         mProgramTitleForBlockedChannel = res.getString(R.string.program_title_for_blocked_channel);
    139         mChannelTextColor =
    140                 res.getColor(
    141                         R.color.program_guide_table_header_column_channel_number_text_color, null);
    142         mChannelBlockedTextColor =
    143                 res.getColor(
    144                         R.color.program_guide_table_header_column_channel_number_blocked_text_color,
    145                         null);
    146         mDetailTextColor = res.getColor(R.color.program_guide_table_detail_title_text_color, null);
    147         mDetailGrayedTextColor =
    148                 res.getColor(R.color.program_guide_table_detail_title_grayed_text_color, null);
    149         mAnimationDuration =
    150                 res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration);
    151         mDetailPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_detail_padding);
    152         mProgramRecordableText = res.getString(R.string.dvr_epg_program_recordable);
    153         mRecordingScheduledText = res.getString(R.string.dvr_epg_program_recording_scheduled);
    154         mRecordingConflictText = res.getString(R.string.dvr_epg_program_recording_conflict);
    155         mRecordingFailedText = res.getString(R.string.dvr_epg_program_recording_failed);
    156         mRecordingInProgressText = res.getString(R.string.dvr_epg_program_recording_in_progress);
    157         mDvrPaddingStartWithTrack =
    158                 res.getDimensionPixelOffset(R.dimen.program_guide_table_detail_dvr_margin_start);
    159         mDvrPaddingStartWithOutTrack =
    160                 res.getDimensionPixelOffset(
    161                         R.dimen.program_guide_table_detail_dvr_margin_start_without_track);
    162 
    163         int episodeTitleSize =
    164                 res.getDimensionPixelSize(
    165                         R.dimen.program_guide_table_detail_episode_title_text_size);
    166         ColorStateList episodeTitleColor =
    167                 ColorStateList.valueOf(
    168                         res.getColor(
    169                                 R.color.program_guide_table_detail_episode_title_text_color, null));
    170         mEpisodeTitleStyle =
    171                 new TextAppearanceSpan(null, 0, episodeTitleSize, episodeTitleColor, null);
    172 
    173         mCriticScoreViews = new ArrayList<>();
    174         mRecycledViewPool = new RecycledViewPool();
    175         mRecycledViewPool.setMaxRecycledViews(
    176                 R.layout.program_guide_table_item,
    177                 context.getResources().getInteger(R.integer.max_recycled_view_pool_epg_table_item));
    178         mProgramManager.addListener(
    179                 new ProgramManager.ListenerAdapter() {
    180                     @Override
    181                     public void onChannelsUpdated() {
    182                         update();
    183                     }
    184                 });
    185         update();
    186         mProgramManager.addTableEntryChangedListener(this);
    187     }
    188 
    189     private void update() {
    190         if (DEBUG) Log.d(TAG, "update " + mProgramManager.getChannelCount() + " channels");
    191         for (TableEntriesUpdatedListener listener : mProgramListAdapters) {
    192             mProgramManager.removeTableEntriesUpdatedListener(listener);
    193         }
    194         mProgramListAdapters.clear();
    195         for (int i = 0; i < mProgramManager.getChannelCount(); i++) {
    196             ProgramListAdapter listAdapter =
    197                     new ProgramListAdapter(mContext.getResources(), mProgramGuide, i);
    198             mProgramManager.addTableEntriesUpdatedListener(listAdapter);
    199             mProgramListAdapters.add(listAdapter);
    200         }
    201         notifyDataSetChanged();
    202     }
    203 
    204     @Override
    205     public int getItemCount() {
    206         return mProgramListAdapters.size();
    207     }
    208 
    209     @Override
    210     public int getItemViewType(int position) {
    211         return R.layout.program_guide_table_row;
    212     }
    213 
    214     @Override
    215     public void onBindViewHolder(ProgramRowViewHolder holder, int position) {
    216         holder.onBind(position);
    217     }
    218 
    219     @Override
    220     public void onBindViewHolder(ProgramRowViewHolder holder, int position, List<Object> payloads) {
    221         if (!payloads.isEmpty()) {
    222             holder.updateDetailView();
    223         } else {
    224             super.onBindViewHolder(holder, position, payloads);
    225         }
    226     }
    227 
    228     @Override
    229     public ProgramRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    230         View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
    231         ProgramRow programRow = (ProgramRow) itemView.findViewById(R.id.row);
    232         programRow.setRecycledViewPool(mRecycledViewPool);
    233         return new ProgramRowViewHolder(itemView);
    234     }
    235 
    236     @Override
    237     public void onTableEntryChanged(ProgramManager.TableEntry tableEntry) {
    238         int channelIndex = mProgramManager.getChannelIndex(tableEntry.channelId);
    239         int pos = mProgramManager.getProgramIdIndex(tableEntry.channelId, tableEntry.getId());
    240         if (DEBUG) Log.d(TAG, "update(" + channelIndex + ", " + pos + ")");
    241         mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry);
    242         notifyItemChanged(channelIndex, true);
    243     }
    244 
    245     class ProgramRowViewHolder extends RecyclerView.ViewHolder
    246             implements ProgramRow.ChildFocusListener {
    247 
    248         private final ViewGroup mContainer;
    249         private final ProgramRow mProgramRow;
    250         private ProgramManager.TableEntry mSelectedEntry;
    251         private Animator mDetailOutAnimator;
    252         private Animator mDetailInAnimator;
    253         private final Runnable mDetailInStarter =
    254                 new Runnable() {
    255                     @Override
    256                     public void run() {
    257                         mProgramRow.removeOnScrollListener(mOnScrollListener);
    258                         if (mDetailInAnimator != null) {
    259                             mDetailInAnimator.start();
    260                         }
    261                     }
    262                 };
    263         private final Runnable mUpdateDetailViewRunnable =
    264                 new Runnable() {
    265                     @Override
    266                     public void run() {
    267                         updateDetailView();
    268                     }
    269                 };
    270 
    271         private final RecyclerView.OnScrollListener mOnScrollListener =
    272                 new RecyclerView.OnScrollListener() {
    273                     @Override
    274                     public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    275                         onHorizontalScrolled();
    276                     }
    277                 };
    278 
    279         private final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener =
    280                 new ViewTreeObserver.OnGlobalFocusChangeListener() {
    281                     @Override
    282                     public void onGlobalFocusChanged(View oldFocus, View newFocus) {
    283                         onChildFocus(
    284                                 GuideUtils.isDescendant(mContainer, oldFocus) ? oldFocus : null,
    285                                 GuideUtils.isDescendant(mContainer, newFocus) ? newFocus : null);
    286                     }
    287                 };
    288 
    289         // Members of Program Details
    290         private final ViewGroup mDetailView;
    291         private final ImageView mImageView;
    292         private final ImageView mBlockView;
    293         private final TextView mTitleView;
    294         private final TextView mTimeView;
    295         private final LinearLayout mCriticScoresLayout;
    296         private final TextView mDescriptionView;
    297         private final TextView mAspectRatioView;
    298         private final TextView mResolutionView;
    299         private final ImageView mDvrIconView;
    300         private final TextView mDvrTextIconView;
    301         private final TextView mDvrStatusView;
    302         private final ViewGroup mDvrIndicator;
    303 
    304         // Members of Channel Header
    305         private Channel mChannel;
    306         private final View mChannelHeaderView;
    307         private final TextView mChannelNumberView;
    308         private final TextView mChannelNameView;
    309         private final ImageView mChannelLogoView;
    310         private final ImageView mChannelBlockView;
    311         private final ImageView mInputLogoView;
    312 
    313         private boolean mIsInputLogoVisible;
    314         private AccessibilityStateChangeListener mAccessibilityStateChangeListener =
    315                 new AccessibilityManager.AccessibilityStateChangeListener() {
    316                     @Override
    317                     public void onAccessibilityStateChanged(boolean enable) {
    318                         enable &= !CommonUtils.isRunningInTest();
    319                         mChannelHeaderView.setFocusable(enable);
    320                     }
    321                 };
    322 
    323         ProgramRowViewHolder(View itemView) {
    324             super(itemView);
    325 
    326             mContainer = (ViewGroup) itemView;
    327             mContainer.addOnAttachStateChangeListener(
    328                     new View.OnAttachStateChangeListener() {
    329                         @Override
    330                         public void onViewAttachedToWindow(View v) {
    331                             mContainer
    332                                     .getViewTreeObserver()
    333                                     .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
    334                             mAccessibilityManager.addAccessibilityStateChangeListener(
    335                                     mAccessibilityStateChangeListener);
    336                         }
    337 
    338                         @Override
    339                         public void onViewDetachedFromWindow(View v) {
    340                             mContainer
    341                                     .getViewTreeObserver()
    342                                     .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
    343                             mAccessibilityManager.removeAccessibilityStateChangeListener(
    344                                     mAccessibilityStateChangeListener);
    345                         }
    346                     });
    347             mProgramRow = (ProgramRow) mContainer.findViewById(R.id.row);
    348 
    349             mDetailView = (ViewGroup) mContainer.findViewById(R.id.detail);
    350             mImageView = (ImageView) mDetailView.findViewById(R.id.image);
    351             mBlockView = (ImageView) mDetailView.findViewById(R.id.block);
    352             mTitleView = (TextView) mDetailView.findViewById(R.id.title);
    353             mTimeView = (TextView) mDetailView.findViewById(R.id.time);
    354             mDescriptionView = (TextView) mDetailView.findViewById(R.id.desc);
    355             mAspectRatioView = (TextView) mDetailView.findViewById(R.id.aspect_ratio);
    356             mResolutionView = (TextView) mDetailView.findViewById(R.id.resolution);
    357             mDvrIconView = (ImageView) mDetailView.findViewById(R.id.dvr_icon);
    358             mDvrTextIconView = (TextView) mDetailView.findViewById(R.id.dvr_text_icon);
    359             mDvrStatusView = (TextView) mDetailView.findViewById(R.id.dvr_status);
    360             mDvrIndicator = (ViewGroup) mContainer.findViewById(R.id.dvr_indicator);
    361             mCriticScoresLayout = (LinearLayout) mDetailView.findViewById(R.id.critic_scores);
    362 
    363             mChannelHeaderView = mContainer.findViewById(R.id.header_column);
    364             mChannelNumberView = (TextView) mContainer.findViewById(R.id.channel_number);
    365             mChannelNameView = (TextView) mContainer.findViewById(R.id.channel_name);
    366             mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo);
    367             mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block);
    368             mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo);
    369 
    370             boolean accessibilityEnabled =
    371                     mAccessibilityManager.isEnabled() && !CommonUtils.isRunningInTest();
    372             mChannelHeaderView.setFocusable(accessibilityEnabled);
    373         }
    374 
    375         public void onBind(int position) {
    376             onBindChannel(mProgramManager.getChannel(position));
    377 
    378             mProgramRow.swapAdapter(mProgramListAdapters.get(position), true);
    379             mProgramRow.setProgramGuide(mProgramGuide);
    380             mProgramRow.setChannel(mProgramManager.getChannel(position));
    381             mProgramRow.setChildFocusListener(this);
    382             mProgramRow.resetScroll(mProgramGuide.getTimelineRowScrollOffset());
    383 
    384             mDetailView.setVisibility(View.GONE);
    385 
    386             // The bottom-left of the last channel header view will have a rounded corner.
    387             mChannelHeaderView.setBackgroundResource(
    388                     (position < mProgramListAdapters.size() - 1)
    389                             ? R.drawable.program_guide_table_header_column_item_background
    390                             : R.drawable.program_guide_table_header_column_last_item_background);
    391         }
    392 
    393         private void onBindChannel(Channel channel) {
    394             if (DEBUG) Log.d(TAG, "onBindChannel " + channel);
    395 
    396             mChannel = channel;
    397             mInputLogoView.setVisibility(View.GONE);
    398             mIsInputLogoVisible = false;
    399             if (channel == null) {
    400                 mChannelNumberView.setVisibility(View.GONE);
    401                 mChannelNameView.setVisibility(View.GONE);
    402                 mChannelLogoView.setVisibility(View.GONE);
    403                 mChannelBlockView.setVisibility(View.GONE);
    404                 return;
    405             }
    406 
    407             String displayNumber = channel.getDisplayNumber();
    408             if (displayNumber == null) {
    409                 mChannelNumberView.setVisibility(View.GONE);
    410             } else {
    411                 int size;
    412                 if (displayNumber.length() <= 4) {
    413                     size = R.dimen.program_guide_table_header_column_channel_number_large_font_size;
    414                 } else {
    415                     size = R.dimen.program_guide_table_header_column_channel_number_small_font_size;
    416                 }
    417                 mChannelNumberView.setTextSize(
    418                         TypedValue.COMPLEX_UNIT_PX,
    419                         mChannelNumberView.getContext().getResources().getDimension(size));
    420                 mChannelNumberView.setText(displayNumber);
    421                 mChannelNumberView.setVisibility(View.VISIBLE);
    422             }
    423             mChannelNumberView.setTextColor(
    424                     isChannelLocked(channel) ? mChannelBlockedTextColor : mChannelTextColor);
    425 
    426             mChannelLogoView.setImageBitmap(null);
    427             mChannelLogoView.setVisibility(View.GONE);
    428             if (isChannelLocked(channel)) {
    429                 mChannelNameView.setVisibility(View.GONE);
    430                 mChannelBlockView.setVisibility(View.VISIBLE);
    431             } else {
    432                 mChannelNameView.setText(channel.getDisplayName());
    433                 mChannelNameView.setVisibility(View.VISIBLE);
    434                 mChannelBlockView.setVisibility(View.GONE);
    435 
    436                 mChannel.loadBitmap(
    437                         itemView.getContext(),
    438                         Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO,
    439                         mChannelLogoWidth,
    440                         mChannelLogoHeight,
    441                         createChannelLogoLoadedCallback(this, channel.getId()));
    442             }
    443         }
    444 
    445         @Override
    446         public void onChildFocus(View oldFocus, View newFocus) {
    447             if (newFocus == null) {
    448                 return;
    449             } // When the accessibility service is enabled, focus might be put on channel's header
    450             // or
    451             // detail view, besides program items.
    452             if (newFocus == mChannelHeaderView) {
    453                 mSelectedEntry = ((ProgramItemView) mProgramRow.getChildAt(0)).getTableEntry();
    454             } else if (newFocus == mDetailView) {
    455                 return;
    456             } else {
    457                 mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry();
    458             }
    459             if (oldFocus == null) {
    460                 // Focus moved from other row.
    461                 if (mProgramGuide.getProgramGrid().isInLayout()) {
    462                     // We need to post runnable to avoid updating detail view when
    463                     // the recycler view is in layout, which may cause detail view not
    464                     // laid out according to the updated contents.
    465                     mHandler.post(mUpdateDetailViewRunnable);
    466                 } else {
    467                     updateDetailView();
    468                 }
    469                 return;
    470             }
    471 
    472             if (Program.isProgramValid(mSelectedEntry.program)) {
    473                 Program program = mSelectedEntry.program;
    474                 if (getProgramBlock(program) == null) {
    475                     program.prefetchPosterArt(itemView.getContext(), mImageWidth, mImageHeight);
    476                 }
    477             }
    478 
    479             // -1 means the selection goes rightwards and 1 goes leftwards
    480             int direction = oldFocus.getLeft() < newFocus.getLeft() ? -1 : 1;
    481             View detailContentView = mDetailView.findViewById(R.id.detail_content);
    482 
    483             if (mDetailInAnimator == null) {
    484                 mDetailOutAnimator =
    485                         ObjectAnimator.ofPropertyValuesHolder(
    486                                 detailContentView,
    487                                 PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f),
    488                                 PropertyValuesHolder.ofFloat(
    489                                         View.TRANSLATION_X, 0f, direction * mDetailPadding));
    490                 mDetailOutAnimator.setDuration(mAnimationDuration);
    491                 mDetailOutAnimator.addListener(
    492                         new HardwareLayerAnimatorListenerAdapter(detailContentView) {
    493                             @Override
    494                             public void onAnimationEnd(Animator animator) {
    495                                 super.onAnimationEnd(animator);
    496                                 mDetailOutAnimator = null;
    497                                 mHandler.removeCallbacks(mDetailInStarter);
    498                                 mHandler.postDelayed(mDetailInStarter, mAnimationDuration);
    499                             }
    500                         });
    501 
    502                 mProgramRow.addOnScrollListener(mOnScrollListener);
    503                 mDetailOutAnimator.start();
    504             } else {
    505                 if (mDetailInAnimator.isStarted()) {
    506                     mDetailInAnimator.cancel();
    507                     detailContentView.setAlpha(0);
    508                 }
    509 
    510                 mHandler.removeCallbacks(mDetailInStarter);
    511                 mHandler.postDelayed(mDetailInStarter, mAnimationDuration);
    512             }
    513 
    514             mDetailInAnimator =
    515                     ObjectAnimator.ofPropertyValuesHolder(
    516                             detailContentView,
    517                             PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f),
    518                             PropertyValuesHolder.ofFloat(
    519                                     View.TRANSLATION_X, direction * -mDetailPadding, 0f));
    520             mDetailInAnimator.setDuration(mAnimationDuration);
    521             mDetailInAnimator.addListener(
    522                     new HardwareLayerAnimatorListenerAdapter(detailContentView) {
    523                         @Override
    524                         public void onAnimationStart(Animator animator) {
    525                             super.onAnimationStart(animator);
    526                             updateDetailView();
    527                         }
    528 
    529                         @Override
    530                         public void onAnimationEnd(Animator animator) {
    531                             super.onAnimationEnd(animator);
    532                             mDetailInAnimator = null;
    533                         }
    534                     });
    535         }
    536 
    537         private void updateDetailView() {
    538             if (mSelectedEntry == null) {
    539                 // The view holder is never on focus before.
    540                 return;
    541             }
    542             if (DEBUG) Log.d(TAG, "updateDetailView");
    543             mCriticScoresLayout.removeAllViews();
    544             if (Program.isProgramValid(mSelectedEntry.program)) {
    545                 mTitleView.setTextColor(mDetailTextColor);
    546                 Context context = itemView.getContext();
    547                 Program program = mSelectedEntry.program;
    548 
    549                 TvContentRating blockedRating = getProgramBlock(program);
    550 
    551                 updatePosterArt(null);
    552                 if (blockedRating == null) {
    553                     program.loadPosterArt(
    554                             context,
    555                             mImageWidth,
    556                             mImageHeight,
    557                             createProgramPosterArtCallback(this, program));
    558                 }
    559 
    560                 String episodeTitle = program.getEpisodeDisplayTitle(mContext);
    561                 if (TextUtils.isEmpty(episodeTitle)) {
    562                     mTitleView.setText(program.getTitle());
    563                 } else {
    564                     String title = program.getTitle();
    565                     String fullTitle = title + "  " + episodeTitle;
    566 
    567                     SpannableString text = new SpannableString(fullTitle);
    568                     text.setSpan(
    569                             mEpisodeTitleStyle,
    570                             fullTitle.length() - episodeTitle.length(),
    571                             fullTitle.length(),
    572                             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    573                     mTitleView.setText(text);
    574                 }
    575 
    576                 updateTextView(
    577                         mTimeView,
    578                         Utils.getDurationString(
    579                                 context,
    580                                 program.getStartTimeUtcMillis(),
    581                                 program.getEndTimeUtcMillis(),
    582                                 false));
    583 
    584                 boolean trackMetaDataVisible =
    585                         updateTextView(
    586                                 mAspectRatioView,
    587                                 Utils.getAspectRatioString(
    588                                         program.getVideoWidth(), program.getVideoHeight()));
    589 
    590                 int videoDefinitionLevel =
    591                         Utils.getVideoDefinitionLevelFromSize(
    592                                 program.getVideoWidth(), program.getVideoHeight());
    593                 trackMetaDataVisible |=
    594                         updateTextView(
    595                                 mResolutionView,
    596                                 Utils.getVideoDefinitionLevelString(context, videoDefinitionLevel));
    597 
    598                 if (mDvrManager != null && mDvrManager.isProgramRecordable(program)) {
    599                     ScheduledRecording scheduledRecording =
    600                             mDvrDataManager.getScheduledRecordingForProgramId(program.getId());
    601                     String statusText = mProgramRecordableText;
    602                     int iconResId = 0;
    603                     if (scheduledRecording != null) {
    604                         if (mDvrManager.isConflicting(scheduledRecording)) {
    605                             iconResId = R.drawable.ic_warning_white_12dp;
    606                             statusText = mRecordingConflictText;
    607                         } else {
    608                             switch (scheduledRecording.getState()) {
    609                                 case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
    610                                     iconResId = R.drawable.ic_recording_program;
    611                                     statusText = mRecordingInProgressText;
    612                                     break;
    613                                 case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
    614                                     iconResId = R.drawable.ic_scheduled_white;
    615                                     statusText = mRecordingScheduledText;
    616                                     break;
    617                                 case ScheduledRecording.STATE_RECORDING_FAILED:
    618                                     iconResId = R.drawable.ic_warning_white_12dp;
    619                                     statusText = mRecordingFailedText;
    620                                     break;
    621                                 default:
    622                                     iconResId = 0;
    623                             }
    624                         }
    625                     }
    626                     if (iconResId == 0) {
    627                         mDvrIconView.setVisibility(View.GONE);
    628                         mDvrTextIconView.setVisibility(View.VISIBLE);
    629                     } else {
    630                         mDvrTextIconView.setVisibility(View.GONE);
    631                         mDvrIconView.setImageResource(iconResId);
    632                         mDvrIconView.setVisibility(View.VISIBLE);
    633                     }
    634                     if (!trackMetaDataVisible) {
    635                         mDvrIndicator.setPaddingRelative(mDvrPaddingStartWithOutTrack, 0, 0, 0);
    636                     } else {
    637                         mDvrIndicator.setPaddingRelative(mDvrPaddingStartWithTrack, 0, 0, 0);
    638                     }
    639                     mDvrIndicator.setVisibility(View.VISIBLE);
    640                     mDvrStatusView.setText(statusText);
    641                 } else {
    642                     mDvrIndicator.setVisibility(View.GONE);
    643                 }
    644 
    645                 if (blockedRating == null) {
    646                     mBlockView.setVisibility(View.GONE);
    647                     updateTextView(mDescriptionView, program.getDescription());
    648                 } else {
    649                     mBlockView.setVisibility(View.VISIBLE);
    650                     updateTextView(mDescriptionView, getBlockedDescription(blockedRating));
    651                 }
    652             } else {
    653                 mTitleView.setTextColor(mDetailGrayedTextColor);
    654                 if (mSelectedEntry.isBlocked()) {
    655                     updateTextView(mTitleView, mProgramTitleForBlockedChannel);
    656                 } else {
    657                     updateTextView(mTitleView, mProgramTitleForNoInformation);
    658                 }
    659                 mImageView.setVisibility(View.GONE);
    660                 mBlockView.setVisibility(View.GONE);
    661                 mTimeView.setVisibility(View.GONE);
    662                 mDvrIndicator.setVisibility(View.GONE);
    663                 mDescriptionView.setVisibility(View.GONE);
    664                 mAspectRatioView.setVisibility(View.GONE);
    665                 mResolutionView.setVisibility(View.GONE);
    666             }
    667         }
    668 
    669         private TvContentRating getProgramBlock(Program program) {
    670             ParentalControlSettings parental = mTvInputManagerHelper.getParentalControlSettings();
    671             if (!parental.isParentalControlsEnabled()) {
    672                 return null;
    673             }
    674             return parental.getBlockedRating(program.getContentRatings());
    675         }
    676 
    677         private boolean isChannelLocked(Channel channel) {
    678             return mTvInputManagerHelper.getParentalControlSettings().isParentalControlsEnabled()
    679                     && channel.isLocked();
    680         }
    681 
    682         private String getBlockedDescription(TvContentRating blockedRating) {
    683             String name =
    684                     mTvInputManagerHelper
    685                             .getContentRatingsManager()
    686                             .getDisplayNameForRating(blockedRating);
    687             if (TextUtils.isEmpty(name)) {
    688                 return mContext.getString(R.string.program_guide_content_locked);
    689             } else {
    690                 return TvContentRating.UNRATED.equals(blockedRating)
    691                         ? mContext.getString(R.string.program_guide_content_locked_unrated)
    692                         : mContext.getString(R.string.program_guide_content_locked_format, name);
    693             }
    694         }
    695 
    696         /**
    697          * Update tv input logo. It should be called when the visible child item in ProgramGrid
    698          * changed.
    699          */
    700         void updateInputLogo(int lastPosition, boolean forceShow) {
    701             if (mChannel == null) {
    702                 mInputLogoView.setVisibility(View.GONE);
    703                 mIsInputLogoVisible = false;
    704                 return;
    705             }
    706 
    707             boolean showLogo = forceShow;
    708             if (!showLogo) {
    709                 Channel lastChannel = mProgramManager.getChannel(lastPosition);
    710                 if (lastChannel == null
    711                         || !mChannel.getInputId().equals(lastChannel.getInputId())) {
    712                     showLogo = true;
    713                 }
    714             }
    715 
    716             if (showLogo) {
    717                 if (!mIsInputLogoVisible) {
    718                     mIsInputLogoVisible = true;
    719                     TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(mChannel.getInputId());
    720                     if (info != null) {
    721                         LoadTvInputLogoTask task =
    722                                 new LoadTvInputLogoTask(
    723                                         itemView.getContext(), ImageCache.getInstance(), info);
    724                         ImageLoader.loadBitmap(createTvInputLogoLoadedCallback(info, this), task);
    725                     }
    726                 }
    727             } else {
    728                 mInputLogoView.setVisibility(View.GONE);
    729                 mInputLogoView.setImageDrawable(null);
    730                 mIsInputLogoVisible = false;
    731             }
    732         }
    733 
    734         // The return value of this method will indicate the target view is visible (true)
    735         // or gone (false).
    736         private boolean updateTextView(TextView textView, String text) {
    737             if (!TextUtils.isEmpty(text)) {
    738                 textView.setVisibility(View.VISIBLE);
    739                 textView.setText(text);
    740                 return true;
    741             } else {
    742                 textView.setVisibility(View.GONE);
    743                 return false;
    744             }
    745         }
    746 
    747         private void updatePosterArt(@Nullable Bitmap posterArt) {
    748             mImageView.setImageBitmap(posterArt);
    749             mImageView.setVisibility(posterArt == null ? View.GONE : View.VISIBLE);
    750         }
    751 
    752         private void updateChannelLogo(@Nullable Bitmap logo) {
    753             mChannelLogoView.setImageBitmap(logo);
    754             mChannelNameView.setVisibility(View.GONE);
    755             mChannelLogoView.setVisibility(View.VISIBLE);
    756         }
    757 
    758         private void updateInputLogoInternal(@NonNull Bitmap tvInputLogo) {
    759             if (!mIsInputLogoVisible) {
    760                 return;
    761             }
    762             mInputLogoView.setImageBitmap(tvInputLogo);
    763             mInputLogoView.setVisibility(View.VISIBLE);
    764         }
    765 
    766         private void updateCriticScoreView(
    767                 ProgramRowViewHolder holder,
    768                 final long programId,
    769                 CriticScore criticScore,
    770                 View view) {
    771             TextView criticScoreSource = (TextView) view.findViewById(R.id.critic_score_source);
    772             TextView criticScoreText = (TextView) view.findViewById(R.id.critic_score_score);
    773             ImageView criticScoreLogo = (ImageView) view.findViewById(R.id.critic_score_logo);
    774 
    775             // set the appropriate information in the views
    776             if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
    777                 criticScoreSource.setText(
    778                         Html.fromHtml(criticScore.source, Html.FROM_HTML_MODE_LEGACY));
    779             } else {
    780                 criticScoreSource.setText(Html.fromHtml(criticScore.source));
    781             }
    782             criticScoreText.setText(criticScore.score);
    783             criticScoreSource.setVisibility(View.VISIBLE);
    784             criticScoreText.setVisibility(View.VISIBLE);
    785             ImageLoader.loadBitmap(
    786                     mContext,
    787                     criticScore.logoUrl,
    788                     createCriticScoreLogoCallback(holder, programId, criticScoreLogo));
    789         }
    790 
    791         private void onHorizontalScrolled() {
    792             if (mDetailInAnimator != null) {
    793                 mHandler.removeCallbacks(mDetailInStarter);
    794                 mHandler.postDelayed(mDetailInStarter, mAnimationDuration);
    795             }
    796         }
    797     }
    798 
    799     private static ImageLoaderCallback<ProgramRowViewHolder> createCriticScoreLogoCallback(
    800             ProgramRowViewHolder holder, final long programId, ImageView logoView) {
    801         return new ImageLoaderCallback<ProgramRowViewHolder>(holder) {
    802             @Override
    803             public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logoImage) {
    804                 if (logoImage == null
    805                         || holder.mSelectedEntry == null
    806                         || holder.mSelectedEntry.program == null
    807                         || holder.mSelectedEntry.program.getId() != programId) {
    808                     logoView.setVisibility(View.GONE);
    809                 } else {
    810                     logoView.setImageBitmap(logoImage);
    811                     logoView.setVisibility(View.VISIBLE);
    812                 }
    813             }
    814         };
    815     }
    816 
    817     private static ImageLoaderCallback<ProgramRowViewHolder> createProgramPosterArtCallback(
    818             ProgramRowViewHolder holder, final Program program) {
    819         return new ImageLoaderCallback<ProgramRowViewHolder>(holder) {
    820             @Override
    821             public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap posterArt) {
    822                 if (posterArt == null
    823                         || holder.mSelectedEntry == null
    824                         || holder.mSelectedEntry.program == null) {
    825                     return;
    826                 }
    827                 String posterArtUri = holder.mSelectedEntry.program.getPosterArtUri();
    828                 if (posterArtUri == null || !posterArtUri.equals(program.getPosterArtUri())) {
    829                     return;
    830                 }
    831                 holder.updatePosterArt(posterArt);
    832             }
    833         };
    834     }
    835 
    836     private static ImageLoaderCallback<ProgramRowViewHolder> createChannelLogoLoadedCallback(
    837             ProgramRowViewHolder holder, final long channelId) {
    838         return new ImageLoaderCallback<ProgramRowViewHolder>(holder) {
    839             @Override
    840             public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) {
    841                 if (logo == null
    842                         || holder.mChannel == null
    843                         || holder.mChannel.getId() != channelId) {
    844                     return;
    845                 }
    846                 holder.updateChannelLogo(logo);
    847             }
    848         };
    849     }
    850 
    851     private static ImageLoaderCallback<ProgramRowViewHolder> createTvInputLogoLoadedCallback(
    852             final TvInputInfo info, ProgramRowViewHolder holder) {
    853         return new ImageLoaderCallback<ProgramRowViewHolder>(holder) {
    854             @Override
    855             public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) {
    856                 if (logo != null
    857                         && holder.mChannel != null
    858                         && info.getId().equals(holder.mChannel.getInputId())) {
    859                     holder.updateInputLogoInternal(logo);
    860                 }
    861             }
    862         };
    863     }
    864 }
    865