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