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