1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.ui;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorInflater;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.AnimatorSet;
23 import android.animation.ValueAnimator;
24 import android.content.Context;
25 import android.content.res.Resources;
26 import android.database.ContentObserver;
27 import android.graphics.Bitmap;
28 import android.media.tv.TvContentRating;
29 import android.media.tv.TvContract;
30 import android.media.tv.TvInputInfo;
31 import android.net.Uri;
32 import android.os.Handler;
33 import android.support.annotation.Nullable;
34 import android.text.Spannable;
35 import android.text.SpannableString;
36 import android.text.TextUtils;
37 import android.text.format.DateUtils;
38 import android.text.style.TextAppearanceSpan;
39 import android.util.AttributeSet;
40 import android.util.Log;
41 import android.util.TypedValue;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.view.animation.AnimationUtils;
45 import android.view.animation.Interpolator;
46 import android.widget.FrameLayout;
47 import android.widget.ImageView;
48 import android.widget.ProgressBar;
49 import android.widget.RelativeLayout;
50 import android.widget.TextView;
51 
52 import com.android.tv.MainActivity;
53 import com.android.tv.R;
54 import com.android.tv.TvApplication;
55 import com.android.tv.common.feature.CommonFeatures;
56 import com.android.tv.data.Channel;
57 import com.android.tv.data.Program;
58 import com.android.tv.data.StreamInfo;
59 import com.android.tv.dvr.DvrManager;
60 import com.android.tv.dvr.ScheduledRecording;
61 import com.android.tv.parental.ContentRatingsManager;
62 import com.android.tv.util.ImageCache;
63 import com.android.tv.util.ImageLoader;
64 import com.android.tv.util.ImageLoader.ImageLoaderCallback;
65 import com.android.tv.util.ImageLoader.LoadTvInputLogoTask;
66 import com.android.tv.util.Utils;
67 
68 import junit.framework.Assert;
69 
70 import java.util.Objects;
71 
72 /**
73  * A view to render channel banner.
74  */
75 public 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 
ChannelBannerView(Context context)199     public ChannelBannerView(Context context) {
200         this(context, null);
201     }
202 
ChannelBannerView(Context context, AttributeSet attrs)203     public ChannelBannerView(Context context, AttributeSet attrs) {
204         this(context, attrs, 0);
205     }
206 
ChannelBannerView(Context context, AttributeSet attrs, int defStyle)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
onAttachedToWindow()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
onDetachedFromWindow()272     protected void onDetachedFromWindow() {
273         if (DEBUG) Log.d(TAG, "onDetachedToWindow");
274         getContext().getContentResolver().unregisterContentObserver(mProgramUpdateObserver);
275         super.onDetachedFromWindow();
276     }
277 
278     @Override
onFinishInflate()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
onEnterAction(boolean fromEmptyScene)313     public void onEnterAction(boolean fromEmptyScene) {
314         resetAnimationEffects();
315         if (fromEmptyScene) {
316             ViewUtils.setTransitionAlpha(mChannelView, 1f);
317         }
318         scheduleHide();
319     }
320 
321     @Override
onExitAction()322     public void onExitAction() {
323         mCurrentHeight = 0;
324         cancelHide();
325     }
326 
scheduleHide()327     private void scheduleHide() {
328         cancelHide();
329         mHandler.postDelayed(mHideRunnable, mShowDurationMillis);
330     }
331 
cancelHide()332     private void cancelHide() {
333         mHandler.removeCallbacks(mHideRunnable);
334     }
335 
resetAnimationEffects()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      */
setLockType(int lockType)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      */
setBlockingContentRating(TvContentRating rating)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      */
updateViews(StreamInfo info)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 
updateStreamInfo(StreamInfo info)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 
updateChannelInfo()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 
getCurrentInputId()477     private String getCurrentInputId() {
478         Channel channel = mMainActivity.getCurrentChannel();
479         return channel != null ? channel.getInputId() : null;
480     }
481 
updateTvInputLogo(Bitmap bitmap)482     private void updateTvInputLogo(Bitmap bitmap) {
483         mTvInputLogoImageView.setVisibility(View.VISIBLE);
484         mTvInputLogoImageView.setImageBitmap(bitmap);
485     }
486 
createTvInputLogoLoaderCallback( final TvInputInfo info, ChannelBannerView channelBannerView)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 }