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.TvContract;
29 import android.media.tv.TvInputInfo;
30 import android.net.Uri;
31 import android.os.Handler;
32 import android.support.annotation.Nullable;
33 import android.text.Spannable;
34 import android.text.SpannableString;
35 import android.text.TextUtils;
36 import android.text.format.DateUtils;
37 import android.text.style.TextAppearanceSpan;
38 import android.util.AttributeSet;
39 import android.util.Log;
40 import android.util.TypedValue;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.animation.AnimationUtils;
44 import android.view.animation.Interpolator;
45 import android.widget.FrameLayout;
46 import android.widget.ImageView;
47 import android.widget.ProgressBar;
48 import android.widget.RelativeLayout;
49 import android.widget.TextView;
50 
51 import com.android.tv.MainActivity;
52 import com.android.tv.R;
53 import com.android.tv.common.recording.RecordedProgram;
54 import com.android.tv.data.Channel;
55 import com.android.tv.data.Program;
56 import com.android.tv.data.StreamInfo;
57 import com.android.tv.util.ImageCache;
58 import com.android.tv.util.ImageLoader;
59 import com.android.tv.util.ImageLoader.ImageLoaderCallback;
60 import com.android.tv.util.ImageLoader.LoadTvInputLogoTask;
61 import com.android.tv.util.Utils;
62 
63 import junit.framework.Assert;
64 
65 import java.util.Objects;
66 
67 /**
68  * A view to render channel banner.
69  */
70 public 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 
ChannelBannerView(Context context)187     public ChannelBannerView(Context context) {
188         this(context, null);
189     }
190 
ChannelBannerView(Context context, AttributeSet attrs)191     public ChannelBannerView(Context context, AttributeSet attrs) {
192         this(context, attrs, 0);
193     }
194 
ChannelBannerView(Context context, AttributeSet attrs, int defStyle)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
onAttachedToWindow()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
onDetachedFromWindow()250     protected void onDetachedFromWindow() {
251         if (DEBUG) Log.d(TAG, "onDetachedToWindow");
252         getContext().getContentResolver().unregisterContentObserver(mProgramUpdateObserver);
253         super.onDetachedFromWindow();
254     }
255 
256     @Override
onFinishInflate()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
onEnterAction(boolean fromEmptyScene)287     public void onEnterAction(boolean fromEmptyScene) {
288         resetAnimationEffects();
289         if (fromEmptyScene) {
290             ViewUtils.setTransitionAlpha(mChannelView, 1f);
291         }
292         scheduleHide();
293     }
294 
295     @Override
onExitAction()296     public void onExitAction() {
297         mCurrentHeight = 0;
298         cancelHide();
299     }
300 
scheduleHide()301     private void scheduleHide() {
302         cancelHide();
303         mHandler.postDelayed(mHideRunnable, mShowDurationMillis);
304     }
305 
cancelHide()306     private void cancelHide() {
307         mHandler.removeCallbacks(mHideRunnable);
308     }
309 
resetAnimationEffects()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      */
setLockType(int lockType)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      */
updateViews(StreamInfo info)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 
updateStreamInfo(StreamInfo info)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 
updateChannelInfo()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 
getCurrentInputId()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 
updateTvInputLogo(Bitmap bitmap)453     private void updateTvInputLogo(Bitmap bitmap) {
454         mTvInputLogoImageView.setVisibility(View.VISIBLE);
455         mTvInputLogoImageView.setImageBitmap(bitmap);
456     }
457 
createTvInputLogoLoaderCallback( final TvInputInfo info, ChannelBannerView channelBannerView)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