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