• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ProgramTableAdapter(Context context, ProgramGuide programGuide)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 
update()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
getItemCount()205     public int getItemCount() {
206         return mProgramListAdapters.size();
207     }
208 
209     @Override
getItemViewType(int position)210     public int getItemViewType(int position) {
211         return R.layout.program_guide_table_row;
212     }
213 
214     @Override
onBindViewHolder(ProgramRowViewHolder holder, int position)215     public void onBindViewHolder(ProgramRowViewHolder holder, int position) {
216         holder.onBind(position);
217     }
218 
219     @Override
onBindViewHolder(ProgramRowViewHolder holder, int position, List<Object> payloads)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
onCreateViewHolder(ViewGroup parent, int viewType)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
onTableEntryChanged(ProgramManager.TableEntry tableEntry)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 
ProgramRowViewHolder(View itemView)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 
onBind(int position)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