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