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