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