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