Home | History | Annotate | Download | only in ui
      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 
    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