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