ChannelBannerView.java revision d41f0075a7d2ea826204e81fcec57d0aa57171a9
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.ui;
18
19import android.animation.Animator;
20import android.animation.AnimatorInflater;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.AnimatorSet;
23import android.animation.ValueAnimator;
24import android.content.Context;
25import android.content.res.Resources;
26import android.database.ContentObserver;
27import android.graphics.Bitmap;
28import android.media.tv.TvContentRating;
29import android.media.tv.TvContract;
30import android.media.tv.TvInputInfo;
31import android.net.Uri;
32import android.os.Handler;
33import android.support.annotation.Nullable;
34import android.text.Spannable;
35import android.text.SpannableString;
36import android.text.TextUtils;
37import android.text.format.DateUtils;
38import android.text.style.TextAppearanceSpan;
39import android.util.AttributeSet;
40import android.util.Log;
41import android.util.TypedValue;
42import android.view.View;
43import android.view.ViewGroup;
44import android.view.animation.AnimationUtils;
45import android.view.animation.Interpolator;
46import android.widget.FrameLayout;
47import android.widget.ImageView;
48import android.widget.ProgressBar;
49import android.widget.RelativeLayout;
50import android.widget.TextView;
51
52import com.android.tv.MainActivity;
53import com.android.tv.R;
54import com.android.tv.TvApplication;
55import com.android.tv.common.feature.CommonFeatures;
56import com.android.tv.data.Channel;
57import com.android.tv.data.Program;
58import com.android.tv.data.StreamInfo;
59import com.android.tv.dvr.DvrManager;
60import com.android.tv.dvr.ScheduledRecording;
61import com.android.tv.parental.ContentRatingsManager;
62import com.android.tv.util.ImageCache;
63import com.android.tv.util.ImageLoader;
64import com.android.tv.util.ImageLoader.ImageLoaderCallback;
65import com.android.tv.util.ImageLoader.LoadTvInputLogoTask;
66import com.android.tv.util.Utils;
67
68import junit.framework.Assert;
69
70import java.util.Objects;
71
72/**
73 * A view to render channel banner.
74 */
75public class ChannelBannerView extends FrameLayout implements TvTransitionManager.TransitionLayout {
76    private static final String TAG = "ChannelBannerView";
77    private static final boolean DEBUG = false;
78
79    /**
80     * Show all information at the channel banner.
81     */
82    public static final int LOCK_NONE = 0;
83
84    /**
85     * Lock program details at the channel banner.
86     * This is used when a content is locked so we don't want to show program details
87     * including program description text and poster art.
88     */
89    public static final int LOCK_PROGRAM_DETAIL = 1;
90
91    /**
92     * Lock channel information at the channel banner.
93     * This is used when a channel is locked so we only want to show input information.
94     */
95    public static final int LOCK_CHANNEL_INFO = 2;
96
97    private static final int DISPLAYED_CONTENT_RATINGS_COUNT = 3;
98
99    private static final String EMPTY_STRING = "";
100
101    private static Program sNoProgram;
102    private static Program sLockedChannelProgram;
103    private static String sClosedCaptionMark;
104
105    private final MainActivity mMainActivity;
106    private final Resources mResources;
107    private View mChannelView;
108
109    private TextView mChannelNumberTextView;
110    private ImageView mChannelLogoImageView;
111    private TextView mProgramTextView;
112    private ImageView mTvInputLogoImageView;
113    private TextView mChannelNameTextView;
114    private TextView mProgramTimeTextView;
115    private ProgressBar mRemainingTimeView;
116    private TextView mRecordingIndicatorView;
117    private TextView mClosedCaptionTextView;
118    private TextView mAspectRatioTextView;
119    private TextView mResolutionTextView;
120    private TextView mAudioChannelTextView;
121    private TextView[] mContentRatingsTextViews = new TextView[DISPLAYED_CONTENT_RATINGS_COUNT];
122    private TextView mProgramDescriptionTextView;
123    private String mProgramDescriptionText;
124    private View mAnchorView;
125    private Channel mCurrentChannel;
126    private Program mLastUpdatedProgram;
127    private final Handler mHandler = new Handler();
128    private final DvrManager mDvrManager;
129    private ContentRatingsManager mContentRatingsManager;
130    private TvContentRating mBlockingContentRating;
131
132    private int mLockType;
133
134    private Animator mResizeAnimator;
135    private int mCurrentHeight;
136    private boolean mProgramInfoUpdatePendingByResizing;
137
138    private final Animator mProgramDescriptionFadeInAnimator;
139    private final Animator mProgramDescriptionFadeOutAnimator;
140
141    private final Runnable mHideRunnable = new Runnable() {
142        @Override
143        public void run() {
144            mCurrentHeight = 0;
145            mMainActivity.getOverlayManager().hideOverlays(
146                    TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
147                    | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
148                    | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
149                    | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
150                    | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
151        }
152    };
153    private final long mShowDurationMillis;
154    private final int mChannelLogoImageViewWidth;
155    private final int mChannelLogoImageViewHeight;
156    private final int mChannelLogoImageViewMarginStart;
157    private final int mProgramDescriptionTextViewWidth;
158    private final int mChannelBannerTextColor;
159    private final int mChannelBannerDimTextColor;
160    private final int mResizeAnimDuration;
161    private final int mRecordingIconPadding;
162    private final Interpolator mResizeInterpolator;
163
164    private final AnimatorListenerAdapter mResizeAnimatorListener = new AnimatorListenerAdapter() {
165        @Override
166        public void onAnimationStart(Animator animator) {
167            mProgramInfoUpdatePendingByResizing = false;
168        }
169
170        @Override
171        public void onAnimationEnd(Animator animator) {
172            mProgramDescriptionTextView.setAlpha(1f);
173            mResizeAnimator = null;
174            if (mProgramInfoUpdatePendingByResizing) {
175                mProgramInfoUpdatePendingByResizing = false;
176                updateProgramInfo(mLastUpdatedProgram);
177            }
178        }
179    };
180
181    private final ContentObserver mProgramUpdateObserver = new ContentObserver(mHandler) {
182        @Override
183        public void onChange(boolean selfChange, Uri uri) {
184            // TODO: This {@code uri} argument may be a program which is not related to this
185            // channel. Consider adding channel id as a parameter of program URI to avoid
186            // unnecessary update.
187            mHandler.post(mProgramUpdateRunnable);
188        }
189    };
190
191    private final Runnable mProgramUpdateRunnable = new Runnable() {
192        @Override
193        public void run() {
194            removeCallbacks(this);
195            updateViews(null);
196        }
197    };
198
199    public ChannelBannerView(Context context) {
200        this(context, null);
201    }
202
203    public ChannelBannerView(Context context, AttributeSet attrs) {
204        this(context, attrs, 0);
205    }
206
207    public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) {
208        super(context, attrs, defStyle);
209        mResources = getResources();
210
211        mMainActivity = (MainActivity) context;
212
213        mShowDurationMillis = mResources.getInteger(
214                R.integer.channel_banner_show_duration);
215        mChannelLogoImageViewWidth = mResources.getDimensionPixelSize(
216                R.dimen.channel_banner_channel_logo_width);
217        mChannelLogoImageViewHeight = mResources.getDimensionPixelSize(
218                R.dimen.channel_banner_channel_logo_height);
219        mChannelLogoImageViewMarginStart = mResources.getDimensionPixelSize(
220                R.dimen.channel_banner_channel_logo_margin_start);
221        mProgramDescriptionTextViewWidth = mResources.getDimensionPixelSize(
222                R.dimen.channel_banner_program_description_width);
223        mChannelBannerTextColor = mResources.getColor(R.color.channel_banner_text_color, null);
224        mChannelBannerDimTextColor = mResources.getColor(R.color.channel_banner_dim_text_color,
225                null);
226        mResizeAnimDuration = mResources.getInteger(R.integer.channel_banner_fast_anim_duration);
227        mRecordingIconPadding = mResources.getDimensionPixelOffset(
228                R.dimen.channel_banner_recording_icon_padding);
229
230        mResizeInterpolator = AnimationUtils.loadInterpolator(context,
231                android.R.interpolator.linear_out_slow_in);
232
233        mProgramDescriptionFadeInAnimator = AnimatorInflater.loadAnimator(mMainActivity,
234                R.animator.channel_banner_program_description_fade_in);
235        mProgramDescriptionFadeOutAnimator = AnimatorInflater.loadAnimator(mMainActivity,
236                R.animator.channel_banner_program_description_fade_out);
237
238        if (CommonFeatures.DVR.isEnabled(mMainActivity)) {
239            mDvrManager = TvApplication.getSingletons(mMainActivity).getDvrManager();
240        } else {
241            mDvrManager = null;
242        }
243        mContentRatingsManager = TvApplication.getSingletons(getContext())
244                .getTvInputManagerHelper().getContentRatingsManager();
245
246        if (sNoProgram == null) {
247            sNoProgram = new Program.Builder()
248                    .setTitle(context.getString(R.string.channel_banner_no_title))
249                    .setDescription(EMPTY_STRING)
250                    .build();
251        }
252        if (sLockedChannelProgram == null){
253            sLockedChannelProgram = new Program.Builder()
254                    .setTitle(context.getString(R.string.channel_banner_locked_channel_title))
255                    .setDescription(EMPTY_STRING)
256                    .build();
257        }
258        if (sClosedCaptionMark == null) {
259            sClosedCaptionMark = context.getString(R.string.closed_caption);
260        }
261    }
262
263    @Override
264    protected void onAttachedToWindow() {
265        if (DEBUG) Log.d(TAG, "onAttachedToWindow");
266        super.onAttachedToWindow();
267        getContext().getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI,
268                true, mProgramUpdateObserver);
269    }
270
271    @Override
272    protected void onDetachedFromWindow() {
273        if (DEBUG) Log.d(TAG, "onDetachedToWindow");
274        getContext().getContentResolver().unregisterContentObserver(mProgramUpdateObserver);
275        super.onDetachedFromWindow();
276    }
277
278    @Override
279    protected void onFinishInflate() {
280        super.onFinishInflate();
281
282        mChannelView = findViewById(R.id.channel_banner_view);
283
284        mChannelNumberTextView = (TextView) findViewById(R.id.channel_number);
285        mChannelLogoImageView = (ImageView) findViewById(R.id.channel_logo);
286        mProgramTextView = (TextView) findViewById(R.id.program_text);
287        mTvInputLogoImageView = (ImageView) findViewById(R.id.tvinput_logo);
288        mChannelNameTextView = (TextView) findViewById(R.id.channel_name);
289        mProgramTimeTextView = (TextView) findViewById(R.id.program_time_text);
290        mRemainingTimeView = (ProgressBar) findViewById(R.id.remaining_time);
291        mRecordingIndicatorView = (TextView) findViewById(R.id.recording_indicator);
292        mClosedCaptionTextView = (TextView) findViewById(R.id.closed_caption);
293        mAspectRatioTextView = (TextView) findViewById(R.id.aspect_ratio);
294        mResolutionTextView = (TextView) findViewById(R.id.resolution);
295        mAudioChannelTextView = (TextView) findViewById(R.id.audio_channel);
296        mContentRatingsTextViews[0] = (TextView) findViewById(R.id.content_ratings_0);
297        mContentRatingsTextViews[1] = (TextView) findViewById(R.id.content_ratings_1);
298        mContentRatingsTextViews[2] = (TextView) findViewById(R.id.content_ratings_2);
299        mProgramDescriptionTextView = (TextView) findViewById(R.id.program_description);
300        mAnchorView = findViewById(R.id.anchor);
301
302        mProgramDescriptionFadeInAnimator.setTarget(mProgramDescriptionTextView);
303        mProgramDescriptionFadeOutAnimator.setTarget(mProgramDescriptionTextView);
304        mProgramDescriptionFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
305            @Override
306            public void onAnimationEnd(Animator animator) {
307                mProgramDescriptionTextView.setText(mProgramDescriptionText);
308            }
309        });
310    }
311
312    @Override
313    public void onEnterAction(boolean fromEmptyScene) {
314        resetAnimationEffects();
315        if (fromEmptyScene) {
316            ViewUtils.setTransitionAlpha(mChannelView, 1f);
317        }
318        scheduleHide();
319    }
320
321    @Override
322    public void onExitAction() {
323        mCurrentHeight = 0;
324        cancelHide();
325    }
326
327    private void scheduleHide() {
328        cancelHide();
329        mHandler.postDelayed(mHideRunnable, mShowDurationMillis);
330    }
331
332    private void cancelHide() {
333        mHandler.removeCallbacks(mHideRunnable);
334    }
335
336    private void resetAnimationEffects() {
337        setAlpha(1f);
338        setScaleX(1f);
339        setScaleY(1f);
340        setTranslationX(0);
341        setTranslationY(0);
342    }
343
344    /**
345     * Set new lock type.
346     *
347     * @param lockType Any of LOCK_NONE, LOCK_PROGRAM_DETAIL, or LOCK_CHANNEL_INFO.
348     * @return {@code true} only if lock type is changed
349     * @throws IllegalArgumentException if lockType is invalid.
350     */
351    public boolean setLockType(int lockType) {
352        if (lockType != LOCK_NONE && lockType != LOCK_CHANNEL_INFO
353                && lockType != LOCK_PROGRAM_DETAIL) {
354            throw new IllegalArgumentException("No such lock type " + lockType);
355        }
356        if (mLockType != lockType) {
357            mLockType = lockType;
358            return true;
359        }
360        return false;
361    }
362
363    /**
364     * Sets the content rating that blocks the current watched channel for displaying it in the
365     * channel banner.
366     */
367    public void setBlockingContentRating(TvContentRating rating) {
368        mBlockingContentRating = rating;
369        updateProgramRatings(mMainActivity.getCurrentProgram());
370    }
371
372    /**
373     * Update channel banner view.
374     *
375     * @param info A StreamInfo that includes stream information.
376     * If it's {@code null}, only program information will be updated.
377     */
378    public void updateViews(StreamInfo info) {
379        resetAnimationEffects();
380        Channel channel = mMainActivity.getCurrentChannel();
381        if (!Objects.equals(mCurrentChannel, channel)) {
382            mBlockingContentRating = null;
383            if (isShown()) {
384                scheduleHide();
385            }
386        }
387        mCurrentChannel = channel;
388        mChannelView.setVisibility(VISIBLE);
389        if (info != null) {
390            // If the current channels between ChannelTuner and TvView are different,
391            // the stream information should not be seen.
392            updateStreamInfo(channel != null && channel.equals(info.getCurrentChannel()) ? info
393                    : null);
394            updateChannelInfo();
395        }
396        updateProgramInfo(mMainActivity.getCurrentProgram());
397    }
398
399    private void updateStreamInfo(StreamInfo info) {
400        // Update stream information in a channel.
401        if (mLockType != LOCK_CHANNEL_INFO && info != null) {
402            updateText(mClosedCaptionTextView, info.hasClosedCaption() ? sClosedCaptionMark
403                    : EMPTY_STRING);
404            updateText(mAspectRatioTextView,
405                    Utils.getAspectRatioString(info.getVideoDisplayAspectRatio()));
406            updateText(mResolutionTextView,
407                    Utils.getVideoDefinitionLevelString(
408                            mMainActivity, info.getVideoDefinitionLevel()));
409            updateText(mAudioChannelTextView,
410                    Utils.getAudioChannelString(mMainActivity, info.getAudioChannelCount()));
411        } else {
412            // Channel change has been requested. But, StreamInfo hasn't been updated yet.
413            mClosedCaptionTextView.setVisibility(View.GONE);
414            mAspectRatioTextView.setVisibility(View.GONE);
415            mResolutionTextView.setVisibility(View.GONE);
416            mAudioChannelTextView.setVisibility(View.GONE);
417            for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
418                mContentRatingsTextViews[i].setVisibility(View.GONE);
419            }
420        }
421    }
422
423    private void updateChannelInfo() {
424        // Update static information for a channel.
425        String displayNumber = EMPTY_STRING;
426        String displayName = EMPTY_STRING;
427        if (mCurrentChannel != null) {
428            displayNumber = mCurrentChannel.getDisplayNumber();
429            if (displayNumber == null) {
430                displayNumber = EMPTY_STRING;
431            }
432            displayName = mCurrentChannel.getDisplayName();
433            if (displayName == null) {
434                displayName = EMPTY_STRING;
435            }
436        }
437
438        if (displayNumber.isEmpty()) {
439            mChannelNumberTextView.setVisibility(GONE);
440        } else {
441            mChannelNumberTextView.setVisibility(VISIBLE);
442        }
443        if (displayNumber.length() <= 3) {
444            updateTextView(
445                    mChannelNumberTextView,
446                    R.dimen.channel_banner_channel_number_large_text_size,
447                    R.dimen.channel_banner_channel_number_large_margin_top);
448        } else if (displayNumber.length() <= 4) {
449            updateTextView(
450                    mChannelNumberTextView,
451                    R.dimen.channel_banner_channel_number_medium_text_size,
452                    R.dimen.channel_banner_channel_number_medium_margin_top);
453        } else {
454            updateTextView(
455                    mChannelNumberTextView,
456                    R.dimen.channel_banner_channel_number_small_text_size,
457                    R.dimen.channel_banner_channel_number_small_margin_top);
458        }
459        mChannelNumberTextView.setText(displayNumber);
460        mChannelNameTextView.setText(displayName);
461        TvInputInfo info = mMainActivity.getTvInputManagerHelper().getTvInputInfo(
462                getCurrentInputId());
463        if (info == null || !ImageLoader.loadBitmap(createTvInputLogoLoaderCallback(info, this),
464                        new LoadTvInputLogoTask(getContext(), ImageCache.getInstance(), info))) {
465            mTvInputLogoImageView.setVisibility(View.GONE);
466            mTvInputLogoImageView.setImageDrawable(null);
467        }
468        mChannelLogoImageView.setImageBitmap(null);
469        mChannelLogoImageView.setVisibility(View.GONE);
470        if (mCurrentChannel != null) {
471            mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO,
472                    mChannelLogoImageViewWidth, mChannelLogoImageViewHeight,
473                    createChannelLogoCallback(this, mCurrentChannel));
474        }
475    }
476
477    private String getCurrentInputId() {
478        Channel channel = mMainActivity.getCurrentChannel();
479        return channel != null ? channel.getInputId() : null;
480    }
481
482    private void updateTvInputLogo(Bitmap bitmap) {
483        mTvInputLogoImageView.setVisibility(View.VISIBLE);
484        mTvInputLogoImageView.setImageBitmap(bitmap);
485    }
486
487    private static ImageLoaderCallback<ChannelBannerView> createTvInputLogoLoaderCallback(
488            final TvInputInfo info, ChannelBannerView channelBannerView) {
489        return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) {
490            @Override
491            public void onBitmapLoaded(ChannelBannerView channelBannerView, Bitmap bitmap) {
492                if (bitmap != null && channelBannerView.mCurrentChannel != null
493                        && info.getId().equals(channelBannerView.mCurrentChannel.getInputId())) {
494                    channelBannerView.updateTvInputLogo(bitmap);
495                }
496            }
497        };
498    }
499
500    private void updateText(TextView view, String text) {
501        if (TextUtils.isEmpty(text)) {
502            view.setVisibility(View.GONE);
503        } else {
504            view.setVisibility(View.VISIBLE);
505            view.setText(text);
506        }
507    }
508
509    private void updateTextView(TextView textView, int sizeRes, int marginTopRes) {
510        float textSize = mResources.getDimension(sizeRes);
511        if (textView.getTextSize() != textSize) {
512            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
513        }
514        updateTopMargin(textView, marginTopRes);
515    }
516
517    private void updateTopMargin(View view, int marginTopRes) {
518        RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view.getLayoutParams();
519        int topMargin = (int) mResources.getDimension(marginTopRes);
520        if (lp.topMargin != topMargin) {
521            lp.topMargin = topMargin;
522            view.setLayoutParams(lp);
523        }
524    }
525
526    private static ImageLoaderCallback<ChannelBannerView> createChannelLogoCallback(
527            ChannelBannerView channelBannerView, final Channel channel) {
528        return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) {
529            @Override
530            public void onBitmapLoaded(ChannelBannerView view, @Nullable Bitmap logo) {
531                if (channel != view.mCurrentChannel) {
532                    // The logo is obsolete.
533                    return;
534                }
535                view.updateLogo(logo);
536            }
537        };
538    }
539
540    private void updateLogo(@Nullable Bitmap logo) {
541        if (logo == null) {
542            // Need to update the text size of the program text view depending on the channel logo.
543            updateProgramTextView(mLastUpdatedProgram);
544            return;
545        }
546
547        mChannelLogoImageView.setImageBitmap(logo);
548        mChannelLogoImageView.setVisibility(View.VISIBLE);
549        updateProgramTextView(mLastUpdatedProgram);
550
551        if (mResizeAnimator == null) {
552            String description = mProgramDescriptionTextView.getText().toString();
553            boolean needFadeAnimation = !description.equals(mProgramDescriptionText);
554            updateBannerHeight(needFadeAnimation);
555        } else {
556            mProgramInfoUpdatePendingByResizing = true;
557        }
558    }
559
560    private void updateProgramInfo(Program program) {
561        if (mLockType == LOCK_CHANNEL_INFO) {
562            program = sLockedChannelProgram;
563        } else if (program == null || !program.isValid() || TextUtils.isEmpty(program.getTitle())) {
564            program = sNoProgram;
565        }
566
567        if (mLastUpdatedProgram == null
568                || !TextUtils.equals(program.getTitle(), mLastUpdatedProgram.getTitle())
569                || !TextUtils.equals(program.getEpisodeDisplayTitle(getContext()),
570                mLastUpdatedProgram.getEpisodeDisplayTitle(getContext()))) {
571            updateProgramTextView(program);
572        }
573        updateProgramTimeInfo(program);
574        updateRecordingStatus(program);
575        updateProgramRatings(program);
576
577        // When the program is changed, but the previous resize animation has not ended yet,
578        // cancel the animation.
579        boolean isProgramChanged = !program.equals(mLastUpdatedProgram);
580        if (mResizeAnimator != null && isProgramChanged) {
581            setLastUpdatedProgram(program);
582            mProgramInfoUpdatePendingByResizing = true;
583            mResizeAnimator.cancel();
584        } else if (mResizeAnimator == null) {
585            if (mLockType != LOCK_NONE || TextUtils.isEmpty(program.getDescription())) {
586                mProgramDescriptionTextView.setVisibility(GONE);
587                mProgramDescriptionText = "";
588            } else {
589                mProgramDescriptionTextView.setVisibility(VISIBLE);
590                mProgramDescriptionText = program.getDescription();
591            }
592            String description = mProgramDescriptionTextView.getText().toString();
593            boolean needFadeAnimation = isProgramChanged
594                    || !description.equals(mProgramDescriptionText);
595            updateBannerHeight(needFadeAnimation);
596        } else {
597            mProgramInfoUpdatePendingByResizing = true;
598        }
599        setLastUpdatedProgram(program);
600    }
601
602    private void updateProgramTextView(Program program) {
603        if (program == null) {
604            return;
605        }
606        updateProgramTextView(program == sLockedChannelProgram, program.getTitle(),
607                program.getEpisodeDisplayTitle(getContext()));
608    }
609
610    private void updateProgramTextView(boolean dimText, String title,
611            String episodeDisplayTitle) {
612        mProgramTextView.setVisibility(View.VISIBLE);
613        if (dimText) {
614            mProgramTextView.setTextColor(mChannelBannerDimTextColor);
615        } else {
616            mProgramTextView.setTextColor(mChannelBannerTextColor);
617        }
618        updateTextView(mProgramTextView,
619                R.dimen.channel_banner_program_large_text_size,
620                R.dimen.channel_banner_program_large_margin_top);
621        if (TextUtils.isEmpty(episodeDisplayTitle)) {
622            mProgramTextView.setText(title);
623        } else {
624            String fullTitle = title + "  " + episodeDisplayTitle;
625
626            SpannableString text = new SpannableString(fullTitle);
627            text.setSpan(new TextAppearanceSpan(getContext(),
628                            R.style.text_appearance_channel_banner_episode_title),
629                    fullTitle.length() - episodeDisplayTitle.length(), fullTitle.length(),
630                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
631            mProgramTextView.setText(text);
632        }
633        int width = mProgramDescriptionTextViewWidth
634                - ((mChannelLogoImageView.getVisibility() != View.VISIBLE)
635                ? 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart);
636        ViewGroup.LayoutParams lp = mProgramTextView.getLayoutParams();
637        lp.width = width;
638        mProgramTextView.setLayoutParams(lp);
639        mProgramTextView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
640                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
641
642        boolean oneline = (mProgramTextView.getLineCount() == 1);
643        if (!oneline) {
644            updateTextView(
645                    mProgramTextView,
646                    R.dimen.channel_banner_program_medium_text_size,
647                    R.dimen.channel_banner_program_medium_margin_top);
648            mProgramTextView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
649                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
650            oneline = (mProgramTextView.getLineCount() == 1);
651        }
652        updateTopMargin(mAnchorView, oneline
653                ? R.dimen.channel_banner_anchor_one_line_y
654                : R.dimen.channel_banner_anchor_two_line_y);
655    }
656
657    private void updateProgramRatings(Program program) {
658        if (mBlockingContentRating != null) {
659            mContentRatingsTextViews[0].setText(
660                    mContentRatingsManager.getDisplayNameForRating(mBlockingContentRating));
661            mContentRatingsTextViews[0].setVisibility(View.VISIBLE);
662            for (int i = 1; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
663                mContentRatingsTextViews[i].setVisibility(View.GONE);
664            }
665            return;
666        }
667        TvContentRating[] ratings = (program == null) ? null : program.getContentRatings();
668        for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
669            if (ratings == null || ratings.length <= i) {
670                mContentRatingsTextViews[i].setVisibility(View.GONE);
671            } else {
672                mContentRatingsTextViews[i].setText(
673                        mContentRatingsManager.getDisplayNameForRating(ratings[i]));
674                mContentRatingsTextViews[i].setVisibility(View.VISIBLE);
675            }
676        }
677    }
678
679    private void updateProgramTimeInfo(Program program) {
680        long durationMs = program.getDurationMillis();
681        long startTimeMs = program.getStartTimeUtcMillis();
682        long endTimeMs = program.getEndTimeUtcMillis();
683
684        if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0 && startTimeMs > 0) {
685            mProgramTimeTextView.setVisibility(View.VISIBLE);
686            mRemainingTimeView.setVisibility(View.VISIBLE);
687            mProgramTimeTextView.setText(Utils.getDurationString(
688                    getContext(), startTimeMs, endTimeMs, true));
689        } else {
690            mProgramTimeTextView.setVisibility(View.GONE);
691            mRemainingTimeView.setVisibility(View.GONE);
692        }
693    }
694
695    private int getProgressPercent(long currTime, long startTime, long endTime) {
696        if (currTime <= startTime) {
697            return 0;
698        } else if (currTime >= endTime) {
699            return 100;
700        } else {
701            return (int) (100 * (currTime - startTime) / (endTime - startTime));
702        }
703    }
704
705    private void updateRecordingStatus(Program program) {
706        if (mDvrManager == null) {
707            updateProgressBarAndRecIcon(program, null);
708            return;
709        }
710        ScheduledRecording currentRecording = (mCurrentChannel == null) ? null
711                : mDvrManager.getCurrentRecording(mCurrentChannel.getId());
712        if (DEBUG) {
713            Log.d(TAG, currentRecording == null ? "No Recording" : "Recording:" + currentRecording);
714        }
715        if (currentRecording != null && isCurrentProgram(currentRecording, program)) {
716            updateProgressBarAndRecIcon(program, currentRecording);
717        } else {
718            updateProgressBarAndRecIcon(program, null);
719        }
720    }
721
722    private void updateProgressBarAndRecIcon(Program program,
723            @Nullable ScheduledRecording recording) {
724        long programStartTime = program.getStartTimeUtcMillis();
725        long programEndTime = program.getEndTimeUtcMillis();
726        long currentPosition = mMainActivity.getCurrentPlayingPosition();
727        updateRecordingIndicator(recording);
728        if (recording != null) {
729            // Recording now. Use recording-style progress bar.
730            mRemainingTimeView.setProgress(getProgressPercent(recording.getStartTimeMs(),
731                    programStartTime, programEndTime));
732            mRemainingTimeView.setSecondaryProgress(getProgressPercent(currentPosition,
733                    programStartTime, programEndTime));
734        } else {
735            // No recording is going now. Recover progress bar.
736            mRemainingTimeView.setProgress(getProgressPercent(currentPosition,
737                    programStartTime, programEndTime));
738            mRemainingTimeView.setSecondaryProgress(0);
739        }
740    }
741
742    private void updateRecordingIndicator(@Nullable ScheduledRecording recording) {
743        if (recording != null) {
744            if (mRemainingTimeView.getVisibility() == View.GONE) {
745                mRecordingIndicatorView.setText(mMainActivity.getResources().getString(
746                        R.string.dvr_recording_till_format, DateUtils.formatDateTime(mMainActivity,
747                                recording.getEndTimeMs(), DateUtils.FORMAT_SHOW_TIME)));
748                mRecordingIndicatorView.setCompoundDrawablePadding(mRecordingIconPadding);
749            } else {
750                mRecordingIndicatorView.setText("");
751                mRecordingIndicatorView.setCompoundDrawablePadding(0);
752            }
753            mRecordingIndicatorView.setVisibility(View.VISIBLE);
754        } else {
755            mRecordingIndicatorView.setVisibility(View.GONE);
756        }
757    }
758
759    private boolean isCurrentProgram(ScheduledRecording recording, Program program) {
760        long currentPosition = mMainActivity.getCurrentPlayingPosition();
761        return (recording.getType() == ScheduledRecording.TYPE_PROGRAM
762                && recording.getProgramId() == program.getId())
763                || (recording.getType() == ScheduledRecording.TYPE_TIMED
764                && currentPosition >= recording.getStartTimeMs()
765                && currentPosition <= recording.getEndTimeMs());
766    }
767
768    private void setLastUpdatedProgram(Program program) {
769        mLastUpdatedProgram = program;
770    }
771
772    private void updateBannerHeight(boolean needFadeAnimation) {
773        Assert.assertNull(mResizeAnimator);
774        // Need to measure the layout height with the new description text.
775        CharSequence oldDescription = mProgramDescriptionTextView.getText();
776        mProgramDescriptionTextView.setText(mProgramDescriptionText);
777        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
778        int targetHeight = getMeasuredHeight();
779
780        if (mCurrentHeight == 0 || !isShown()) {
781            // Do not add the resize animation when the banner has not been shown before.
782            mCurrentHeight = targetHeight;
783            LayoutParams layoutParams = (LayoutParams) getLayoutParams();
784            if (targetHeight != layoutParams.height) {
785                layoutParams.height = targetHeight;
786                setLayoutParams(layoutParams);
787            }
788        } else if (mCurrentHeight != targetHeight || needFadeAnimation) {
789            // Restore description text for fade in/out animation.
790            if (needFadeAnimation) {
791                mProgramDescriptionTextView.setText(oldDescription);
792            }
793            mResizeAnimator = createResizeAnimator(targetHeight, needFadeAnimation);
794            mResizeAnimator.start();
795        }
796    }
797
798    private Animator createResizeAnimator(int targetHeight, boolean addFadeAnimation) {
799        final ValueAnimator heightAnimator = ValueAnimator.ofInt(mCurrentHeight, targetHeight);
800        heightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
801            @Override
802            public void onAnimationUpdate(ValueAnimator animation) {
803                int value = (Integer) animation.getAnimatedValue();
804                LayoutParams layoutParams = (LayoutParams) ChannelBannerView.this.getLayoutParams();
805                if (value != layoutParams.height) {
806                    layoutParams.height = value;
807                    ChannelBannerView.this.setLayoutParams(layoutParams);
808                }
809                mCurrentHeight = value;
810            }
811        });
812
813        heightAnimator.setDuration(mResizeAnimDuration);
814        heightAnimator.setInterpolator(mResizeInterpolator);
815
816        if (!addFadeAnimation) {
817            heightAnimator.addListener(mResizeAnimatorListener);
818            return heightAnimator;
819        }
820
821        AnimatorSet fadeOutAndHeightAnimator = new AnimatorSet();
822        fadeOutAndHeightAnimator.playTogether(mProgramDescriptionFadeOutAnimator, heightAnimator);
823        AnimatorSet animator = new AnimatorSet();
824        animator.playSequentially(fadeOutAndHeightAnimator, mProgramDescriptionFadeInAnimator);
825        animator.addListener(mResizeAnimatorListener);
826        return animator;
827    }
828}