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