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