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