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