/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.ui; import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; import android.support.annotation.Nullable; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.TextAppearanceSpan; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import com.android.tv.R; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.singletons.HasSingletons; import com.android.tv.data.ProgramImpl; import com.android.tv.data.StreamInfo; import com.android.tv.data.api.Channel; import com.android.tv.data.api.Program; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.ui.TvTransitionManager.TransitionLayout; import com.android.tv.ui.hideable.AutoHideScheduler; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import com.android.tv.util.images.ImageCache; import com.android.tv.util.images.ImageLoader; import com.android.tv.util.images.ImageLoader.ImageLoaderCallback; import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask; import com.google.common.collect.ImmutableList; import javax.inject.Provider; /** A view to render channel banner. */ public class ChannelBannerView extends FrameLayout implements TransitionLayout, AccessibilityStateChangeListener { private static final String TAG = "ChannelBannerView"; private static final boolean DEBUG = false; /** Show all information at the channel banner. */ public static final int LOCK_NONE = 0; /** Singletons needed for this class. */ public interface MySingletons { Provider getCurrentChannelProvider(); Provider getCurrentProgramProvider(); Provider getOverlayManagerProvider(); TvInputManagerHelper getTvInputManagerHelperSingleton(); Provider getCurrentPlayingPositionProvider(); DvrManager getDvrManagerSingleton(); } /** * Lock program details at the channel banner. This is used when a content is locked so we don't * want to show program details including program description text and poster art. */ public static final int LOCK_PROGRAM_DETAIL = 1; /** * Lock channel information at the channel banner. This is used when a channel is locked so we * only want to show input information. */ public static final int LOCK_CHANNEL_INFO = 2; private static final int DISPLAYED_CONTENT_RATINGS_COUNT = 3; private static final String EMPTY_STRING = ""; private Program mNoProgram; private Program mLockedChannelProgram; private static String sClosedCaptionMark; private final Resources mResources; private final Provider mCurrentChannelProvider; private final Provider mCurrentProgramProvider; private final Provider mCurrentPlayingPositionProvider; private final TvInputManagerHelper mTvInputManagerHelper; // TvOverlayManager is always created after ChannelBannerView private final Provider mTvOverlayManager; private final AccessibilityManager mAccessibilityManager; private View mChannelView; private TextView mChannelNumberTextView; private ImageView mChannelLogoImageView; private TextView mProgramTextView; private ImageView mTvInputLogoImageView; private ImageView mChannelSignalStrengthView; private TextView mChannelNameTextView; private TextView mProgramTimeTextView; private ProgressBar mRemainingTimeView; private TextView mRecordingIndicatorView; private TextView mClosedCaptionTextView; private TextView mAspectRatioTextView; private TextView mResolutionTextView; private TextView mAudioChannelTextView; private TextView[] mContentRatingsTextViews = new TextView[DISPLAYED_CONTENT_RATINGS_COUNT]; private TextView mProgramDescriptionTextView; private String mProgramDescriptionText; private View mAnchorView; private Channel mCurrentChannel; private boolean mCurrentChannelLogoExists; private Program mLastUpdatedProgram; private final AutoHideScheduler mAutoHideScheduler; private final DvrManager mDvrManager; private ContentRatingsManager mContentRatingsManager; private TvContentRating mBlockingContentRating; private int mLockType; private boolean mUpdateOnTune; private Animator mResizeAnimator; private int mCurrentHeight; private boolean mProgramInfoUpdatePendingByResizing; private final Animator mProgramDescriptionFadeInAnimator; private final Animator mProgramDescriptionFadeOutAnimator; private final long mShowDurationMillis; private final int mChannelLogoImageViewWidth; private final int mChannelLogoImageViewHeight; private final int mChannelLogoImageViewMarginStart; private final int mProgramDescriptionTextViewWidth; private final int mChannelBannerTextColor; private final int mChannelBannerDimTextColor; private final int mResizeAnimDuration; private final int mRecordingIconPadding; private final Interpolator mResizeInterpolator; /** * 0 - 100 represent signal strength percentage. Strength is divided into 5 levels (0 - 4). * *

This is the upper boundary of level 0 [0%, 20%], and the lower boundary of level 1 (20%, * 40%]. */ private static final int SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND = 20; /** * This is the upper boundary of level 1 (20%, 40%], and the lower boundary of level 2 (40%, * 60%]. */ private static final int SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND = 40; /** * This is the upper boundary of level of level 2. (40%, 60%], and the lower boundary of level 3 * (60%, 80%]. */ private static final int SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND = 60; /** * This is the upper boundary of level of level 3 (60%, 80%], and the lower boundary of level 4 * (80%, 100%]. */ private static final int SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND = 80; private final AnimatorListenerAdapter mResizeAnimatorListener = new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animator) { mProgramInfoUpdatePendingByResizing = false; } @Override public void onAnimationEnd(Animator animator) { mProgramDescriptionTextView.setAlpha(1f); mResizeAnimator = null; if (mProgramInfoUpdatePendingByResizing) { mProgramInfoUpdatePendingByResizing = false; updateProgramInfo(mLastUpdatedProgram); } } }; public ChannelBannerView(Context context) { this(context, null); } public ChannelBannerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mResources = getResources(); @SuppressWarnings("unchecked") // injection MySingletons singletons = HasSingletons.get(MySingletons.class, context); mCurrentChannelProvider = singletons.getCurrentChannelProvider(); mCurrentProgramProvider = singletons.getCurrentProgramProvider(); mCurrentPlayingPositionProvider = singletons.getCurrentPlayingPositionProvider(); mTvInputManagerHelper = singletons.getTvInputManagerHelperSingleton(); mTvOverlayManager = singletons.getOverlayManagerProvider(); mShowDurationMillis = mResources.getInteger(R.integer.channel_banner_show_duration); mChannelLogoImageViewWidth = mResources.getDimensionPixelSize(R.dimen.channel_banner_channel_logo_width); mChannelLogoImageViewHeight = mResources.getDimensionPixelSize(R.dimen.channel_banner_channel_logo_height); mChannelLogoImageViewMarginStart = mResources.getDimensionPixelSize(R.dimen.channel_banner_channel_logo_margin_start); mProgramDescriptionTextViewWidth = mResources.getDimensionPixelSize(R.dimen.channel_banner_program_description_width); mChannelBannerTextColor = mResources.getColor(R.color.channel_banner_text_color, null); mChannelBannerDimTextColor = mResources.getColor(R.color.channel_banner_dim_text_color, null); mResizeAnimDuration = mResources.getInteger(R.integer.channel_banner_fast_anim_duration); mRecordingIconPadding = mResources.getDimensionPixelOffset(R.dimen.channel_banner_recording_icon_padding); mResizeInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); mProgramDescriptionFadeInAnimator = AnimatorInflater.loadAnimator( context, R.animator.channel_banner_program_description_fade_in); mProgramDescriptionFadeOutAnimator = AnimatorInflater.loadAnimator( context, R.animator.channel_banner_program_description_fade_out); if (CommonFeatures.DVR.isEnabled(context)) { mDvrManager = singletons.getDvrManagerSingleton(); } else { mDvrManager = null; } mContentRatingsManager = mTvInputManagerHelper.getContentRatingsManager(); mNoProgram = new ProgramImpl.Builder() .setTitle(context.getString(R.string.channel_banner_no_title)) .setDescription(EMPTY_STRING) .build(); mLockedChannelProgram = new ProgramImpl.Builder() .setTitle(context.getString(R.string.channel_banner_locked_channel_title)) .setDescription(EMPTY_STRING) .build(); if (sClosedCaptionMark == null) { sClosedCaptionMark = context.getString(R.string.closed_caption); } mAutoHideScheduler = new AutoHideScheduler(context, this::hide); mAccessibilityManager = context.getSystemService(AccessibilityManager.class); } @Override protected void onFinishInflate() { super.onFinishInflate(); mChannelView = findViewById(R.id.channel_banner_view); mChannelNumberTextView = findViewById(R.id.channel_number); mChannelLogoImageView = findViewById(R.id.channel_logo); mProgramTextView = findViewById(R.id.program_text); mTvInputLogoImageView = findViewById(R.id.tvinput_logo); mChannelSignalStrengthView = findViewById(R.id.channel_signal_strength); mChannelNameTextView = findViewById(R.id.channel_name); mProgramTimeTextView = findViewById(R.id.program_time_text); mRemainingTimeView = findViewById(R.id.remaining_time); mRecordingIndicatorView = findViewById(R.id.recording_indicator); mClosedCaptionTextView = findViewById(R.id.closed_caption); mAspectRatioTextView = findViewById(R.id.aspect_ratio); mResolutionTextView = findViewById(R.id.resolution); mAudioChannelTextView = findViewById(R.id.audio_channel); mContentRatingsTextViews[0] = findViewById(R.id.content_ratings_0); mContentRatingsTextViews[1] = findViewById(R.id.content_ratings_1); mContentRatingsTextViews[2] = findViewById(R.id.content_ratings_2); mProgramDescriptionTextView = findViewById(R.id.program_description); mAnchorView = findViewById(R.id.anchor); mProgramDescriptionFadeInAnimator.setTarget(mProgramDescriptionTextView); mProgramDescriptionFadeOutAnimator.setTarget(mProgramDescriptionTextView); mProgramDescriptionFadeOutAnimator.addListener( new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { mProgramDescriptionTextView.setText(mProgramDescriptionText); } }); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mAccessibilityManager.addAccessibilityStateChangeListener(mAutoHideScheduler); } @Override protected void onDetachedFromWindow() { mAccessibilityManager.removeAccessibilityStateChangeListener(mAutoHideScheduler); super.onDetachedFromWindow(); } @Override public void onEnterAction(boolean fromEmptyScene) { resetAnimationEffects(); if (fromEmptyScene) { ViewUtils.setTransitionAlpha(mChannelView, 1f); } mAutoHideScheduler.schedule(mShowDurationMillis); } @Override public void onExitAction() { mCurrentHeight = 0; mAutoHideScheduler.cancel(); } private void resetAnimationEffects() { setAlpha(1f); setScaleX(1f); setScaleY(1f); setTranslationX(0); setTranslationY(0); } /** * Set new lock type. * * @param lockType Any of LOCK_NONE, LOCK_PROGRAM_DETAIL, or LOCK_CHANNEL_INFO. * @return the previous lock type of the channel banner. * @throws IllegalArgumentException if lockType is invalid. */ public int setLockType(int lockType) { if (lockType != LOCK_NONE && lockType != LOCK_CHANNEL_INFO && lockType != LOCK_PROGRAM_DETAIL) { throw new IllegalArgumentException("No such lock type " + lockType); } int previousLockType = mLockType; mLockType = lockType; return previousLockType; } /** * Sets the content rating that blocks the current watched channel for displaying it in the * channel banner. */ public void setBlockingContentRating(TvContentRating rating) { mBlockingContentRating = rating; updateProgramRatings(mCurrentProgramProvider.get()); } /** * Update channel banner view. * * @param updateOnTune {@false} denotes the channel banner is updated due to other reasons than * tuning. The channel info will not be updated in this case. */ public void updateViews(boolean updateOnTune) { resetAnimationEffects(); mChannelView.setVisibility(VISIBLE); mUpdateOnTune = updateOnTune; if (mUpdateOnTune) { if (isShown()) { mAutoHideScheduler.schedule(mShowDurationMillis); } mBlockingContentRating = null; mCurrentChannel = mCurrentChannelProvider.get(); mCurrentChannelLogoExists = mCurrentChannel != null && mCurrentChannel.channelLogoExists(); updateStreamInfo(null); updateChannelInfo(); } updateProgramInfo(mCurrentProgramProvider.get()); mUpdateOnTune = false; } private void hide() { mCurrentHeight = 0; mTvOverlayManager .get() .hideOverlays( TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); } /** * Update channel banner view with stream info. * * @param info A StreamInfo that includes stream information. */ public void updateStreamInfo(StreamInfo info) { // Update stream information in a channel. if (mLockType != LOCK_CHANNEL_INFO && info != null) { updateText( mClosedCaptionTextView, info.hasClosedCaption() ? sClosedCaptionMark : EMPTY_STRING); updateText( mAspectRatioTextView, Utils.getAspectRatioString(info.getVideoDisplayAspectRatio())); updateText( mResolutionTextView, Utils.getVideoDefinitionLevelString( getContext(), info.getVideoDefinitionLevel())); updateText( mAudioChannelTextView, Utils.getAudioChannelString(getContext(), info.getAudioChannelCount())); } else { // Channel change has been requested. But, StreamInfo hasn't been updated yet. mClosedCaptionTextView.setVisibility(View.GONE); mAspectRatioTextView.setVisibility(View.GONE); mResolutionTextView.setVisibility(View.GONE); mAudioChannelTextView.setVisibility(View.GONE); } } private void updateChannelInfo() { // Update static information for a channel. String displayNumber = EMPTY_STRING; String displayName = EMPTY_STRING; if (mCurrentChannel != null) { displayNumber = mCurrentChannel.getDisplayNumber(); if (displayNumber == null) { displayNumber = EMPTY_STRING; } displayName = mCurrentChannel.getDisplayName(); if (displayName == null) { displayName = EMPTY_STRING; } } if (displayNumber.isEmpty()) { mChannelNumberTextView.setVisibility(GONE); } else { mChannelNumberTextView.setVisibility(VISIBLE); } if (displayNumber.length() <= 3) { updateTextView( mChannelNumberTextView, R.dimen.channel_banner_channel_number_large_text_size, R.dimen.channel_banner_channel_number_large_margin_top); } else if (displayNumber.length() <= 4) { updateTextView( mChannelNumberTextView, R.dimen.channel_banner_channel_number_medium_text_size, R.dimen.channel_banner_channel_number_medium_margin_top); } else { updateTextView( mChannelNumberTextView, R.dimen.channel_banner_channel_number_small_text_size, R.dimen.channel_banner_channel_number_small_margin_top); } mChannelNumberTextView.setText(displayNumber); mChannelNameTextView.setText(displayName); TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(getCurrentInputId()); if (info == null || !ImageLoader.loadBitmap( createTvInputLogoLoaderCallback(info, this), new LoadTvInputLogoTask(getContext(), ImageCache.getInstance(), info))) { mTvInputLogoImageView.setVisibility(View.GONE); mTvInputLogoImageView.setImageDrawable(null); } mChannelLogoImageView.setImageBitmap(null); mChannelLogoImageView.setVisibility(View.GONE); if (mCurrentChannel != null && mCurrentChannelLogoExists) { mCurrentChannel.loadBitmap( getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, mChannelLogoImageViewWidth, mChannelLogoImageViewHeight, createChannelLogoCallback(this, mCurrentChannel)); } } private String getCurrentInputId() { Channel channel = mCurrentChannelProvider.get(); return channel != null ? channel.getInputId() : null; } private void updateTvInputLogo(Bitmap bitmap) { mTvInputLogoImageView.setVisibility(View.VISIBLE); mTvInputLogoImageView.setImageBitmap(bitmap); } private static ImageLoaderCallback createTvInputLogoLoaderCallback( final TvInputInfo info, ChannelBannerView channelBannerView) { return new ImageLoaderCallback(channelBannerView) { @Override public void onBitmapLoaded(ChannelBannerView channelBannerView, Bitmap bitmap) { if (bitmap != null && channelBannerView.mCurrentChannel != null && info.getId().equals(channelBannerView.mCurrentChannel.getInputId())) { channelBannerView.updateTvInputLogo(bitmap); } } }; } private void updateText(TextView view, String text) { if (TextUtils.isEmpty(text)) { view.setVisibility(View.GONE); } else { view.setVisibility(View.VISIBLE); view.setText(text); } } private void updateTextView(TextView textView, int sizeRes, int marginTopRes) { float textSize = mResources.getDimension(sizeRes); if (textView.getTextSize() != textSize) { textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); } updateTopMargin(textView, marginTopRes); } private void updateTopMargin(View view, int marginTopRes) { RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view.getLayoutParams(); int topMargin = (int) mResources.getDimension(marginTopRes); if (lp.topMargin != topMargin) { lp.topMargin = topMargin; view.setLayoutParams(lp); } } private static ImageLoaderCallback createChannelLogoCallback( ChannelBannerView channelBannerView, final Channel channel) { return new ImageLoaderCallback(channelBannerView) { @Override public void onBitmapLoaded(ChannelBannerView view, @Nullable Bitmap logo) { if (!channel.equals(view.mCurrentChannel)) { // The logo is obsolete. return; } view.updateLogo(logo); } }; } public void updateChannelSignalStrengthView(int value) { int resId = signalStrenghtToResId(value); if (resId != 0) { mChannelSignalStrengthView.setVisibility(View.VISIBLE); mChannelSignalStrengthView.setImageResource(resId); } else { mChannelSignalStrengthView.setVisibility(View.GONE); } } private int signalStrenghtToResId(int value) { int signal = 0; if (value >= 0 && value <= 100) { if (value <= SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND) { signal = R.drawable.quantum_ic_signal_cellular_0_bar_white_24; } else if (value <= SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND) { signal = R.drawable.quantum_ic_signal_cellular_1_bar_white_24; } else if (value <= SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND) { signal = R.drawable.quantum_ic_signal_cellular_2_bar_white_24; } else if (value <= SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND) { signal = R.drawable.quantum_ic_signal_cellular_3_bar_white_24; } else { signal = R.drawable.quantum_ic_signal_cellular_4_bar_white_24; } } return signal; } private void updateLogo(@Nullable Bitmap logo) { if (logo == null) { // Need to update the text size of the program text view depending on the channel logo. updateProgramTextView(mLastUpdatedProgram); return; } mChannelLogoImageView.setImageBitmap(logo); mChannelLogoImageView.setVisibility(View.VISIBLE); updateProgramTextView(mLastUpdatedProgram); if (mResizeAnimator == null) { String description = mProgramDescriptionTextView.getText().toString(); boolean programDescriptionNeedFadeAnimation = !description.equals(mProgramDescriptionText) && !mUpdateOnTune; updateBannerHeight(programDescriptionNeedFadeAnimation); } else { mProgramInfoUpdatePendingByResizing = true; } } private void updateProgramInfo(Program program) { if (mLockType == LOCK_CHANNEL_INFO) { program = mLockedChannelProgram; } else if (program == null || !program.isValid() || TextUtils.isEmpty(program.getTitle())) { program = mNoProgram; } if (mLastUpdatedProgram == null || !TextUtils.equals(program.getTitle(), mLastUpdatedProgram.getTitle()) || !TextUtils.equals( program.getEpisodeDisplayTitle(getContext()), mLastUpdatedProgram.getEpisodeDisplayTitle(getContext()))) { updateProgramTextView(program); } updateProgramTimeInfo(program); updateRecordingStatus(program); updateProgramRatings(program); // When the program is changed, but the previous resize animation has not ended yet, // cancel the animation. boolean isProgramChanged = !program.equals(mLastUpdatedProgram); if (mResizeAnimator != null && isProgramChanged) { setLastUpdatedProgram(program); mProgramInfoUpdatePendingByResizing = true; mResizeAnimator.cancel(); } else if (mResizeAnimator == null) { if (mLockType != LOCK_NONE || TextUtils.isEmpty(program.getDescription())) { mProgramDescriptionTextView.setVisibility(GONE); mProgramDescriptionText = ""; } else { mProgramDescriptionTextView.setVisibility(VISIBLE); mProgramDescriptionText = program.getDescription(); } String description = mProgramDescriptionTextView.getText().toString(); boolean programDescriptionNeedFadeAnimation = (isProgramChanged || !description.equals(mProgramDescriptionText)) && !mUpdateOnTune; updateBannerHeight(programDescriptionNeedFadeAnimation); } else { mProgramInfoUpdatePendingByResizing = true; } setLastUpdatedProgram(program); } private void updateProgramTextView(Program program) { if (program == null) { return; } updateProgramTextView( program.equals(mLockedChannelProgram), program.getTitle(), program.getEpisodeDisplayTitle(getContext())); } private void updateProgramTextView(boolean dimText, String title, String episodeDisplayTitle) { mProgramTextView.setVisibility(View.VISIBLE); if (dimText) { mProgramTextView.setTextColor(mChannelBannerDimTextColor); } else { mProgramTextView.setTextColor(mChannelBannerTextColor); } updateTextView( mProgramTextView, R.dimen.channel_banner_program_large_text_size, R.dimen.channel_banner_program_large_margin_top); if (TextUtils.isEmpty(episodeDisplayTitle)) { mProgramTextView.setText(title); } else { String fullTitle = title + " " + episodeDisplayTitle; SpannableString text = new SpannableString(fullTitle); text.setSpan( new TextAppearanceSpan( getContext(), R.style.text_appearance_channel_banner_episode_title), fullTitle.length() - episodeDisplayTitle.length(), fullTitle.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); mProgramTextView.setText(text); } int width = mProgramDescriptionTextViewWidth + (mCurrentChannelLogoExists ? 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart); ViewGroup.LayoutParams lp = mProgramTextView.getLayoutParams(); lp.width = width; mProgramTextView.setLayoutParams(lp); mProgramTextView.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); boolean oneline = (mProgramTextView.getLineCount() == 1); if (!oneline) { updateTextView( mProgramTextView, R.dimen.channel_banner_program_medium_text_size, R.dimen.channel_banner_program_medium_margin_top); mProgramTextView.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); oneline = (mProgramTextView.getLineCount() == 1); } updateTopMargin( mAnchorView, oneline ? R.dimen.channel_banner_anchor_one_line_y : R.dimen.channel_banner_anchor_two_line_y); } private void updateProgramRatings(Program program) { if (mLockType == LOCK_CHANNEL_INFO) { for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { mContentRatingsTextViews[i].setVisibility(View.GONE); } } else if (mBlockingContentRating != null) { String displayNameForRating = mContentRatingsManager.getDisplayNameForRating(mBlockingContentRating); if (!TextUtils.isEmpty(displayNameForRating)) { mContentRatingsTextViews[0].setText(displayNameForRating); mContentRatingsTextViews[0].setVisibility(View.VISIBLE); } else { mContentRatingsTextViews[0].setVisibility(View.GONE); } for (int i = 1; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { mContentRatingsTextViews[i].setVisibility(View.GONE); } } else { ImmutableList ratings = (program == null) ? null : program.getContentRatings(); int ratingsViewIndex = 0; if (ratings != null) { for (int i = 0; i < ratings.size(); i++) { if (ratingsViewIndex < DISPLAYED_CONTENT_RATINGS_COUNT && !TextUtils.isEmpty( mContentRatingsManager.getDisplayNameForRating( ratings.get(i)))) { mContentRatingsTextViews[ratingsViewIndex].setText( mContentRatingsManager.getDisplayNameForRating(ratings.get(i))); mContentRatingsTextViews[ratingsViewIndex].setVisibility(View.VISIBLE); ratingsViewIndex++; } } } while (ratingsViewIndex < DISPLAYED_CONTENT_RATINGS_COUNT) { mContentRatingsTextViews[ratingsViewIndex++].setVisibility(View.GONE); } } } private void updateProgramTimeInfo(Program program) { long durationMs = program.getDurationMillis(); long startTimeMs = program.getStartTimeUtcMillis(); if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0 && startTimeMs > 0) { mProgramTimeTextView.setVisibility(View.VISIBLE); mRemainingTimeView.setVisibility(View.VISIBLE); mProgramTimeTextView.setText(program.getDurationString(getContext())); } else { mProgramTimeTextView.setVisibility(View.GONE); mRemainingTimeView.setVisibility(View.GONE); } } private int getProgressPercent(long currTime, long startTime, long endTime) { if (currTime <= startTime) { return 0; } else if (currTime >= endTime) { return 100; } else { return (int) (100 * (currTime - startTime) / (endTime - startTime)); } } private void updateRecordingStatus(Program program) { if (mDvrManager == null) { updateProgressBarAndRecIcon(program, null); return; } ScheduledRecording currentRecording = (mCurrentChannel == null) ? null : mDvrManager.getCurrentRecording(mCurrentChannel.getId()); if (DEBUG) { Log.d(TAG, currentRecording == null ? "No Recording" : "Recording:" + currentRecording); } if (currentRecording != null && isCurrentProgram(currentRecording, program)) { updateProgressBarAndRecIcon(program, currentRecording); } else { updateProgressBarAndRecIcon(program, null); } } private void updateProgressBarAndRecIcon( Program program, @Nullable ScheduledRecording recording) { long programStartTime = program.getStartTimeUtcMillis(); long programEndTime = program.getEndTimeUtcMillis(); long currentPosition = mCurrentPlayingPositionProvider.get(); updateRecordingIndicator(recording); if (recording != null) { // Recording now. Use recording-style progress bar. mRemainingTimeView.setProgress( getProgressPercent( recording.getStartTimeMs(), programStartTime, programEndTime)); mRemainingTimeView.setSecondaryProgress( getProgressPercent(currentPosition, programStartTime, programEndTime)); } else { // No recording is going now. Recover progress bar. mRemainingTimeView.setProgress( getProgressPercent(currentPosition, programStartTime, programEndTime)); mRemainingTimeView.setSecondaryProgress(0); } } private void updateRecordingIndicator(@Nullable ScheduledRecording recording) { if (recording != null) { if (mRemainingTimeView.getVisibility() == View.GONE) { mRecordingIndicatorView.setText( getContext() .getResources() .getString( R.string.dvr_recording_till_format, DateUtils.formatDateTime( getContext(), recording.getEndTimeMs(), DateUtils.FORMAT_SHOW_TIME))); mRecordingIndicatorView.setCompoundDrawablePadding(mRecordingIconPadding); } else { mRecordingIndicatorView.setText(""); mRecordingIndicatorView.setCompoundDrawablePadding(0); } mRecordingIndicatorView.setVisibility(View.VISIBLE); } else { mRecordingIndicatorView.setVisibility(View.GONE); } } private boolean isCurrentProgram(ScheduledRecording recording, Program program) { long currentPosition = mCurrentPlayingPositionProvider.get(); return (recording.getType() == ScheduledRecording.TYPE_PROGRAM && recording.getProgramId() == program.getId()) || (recording.getType() == ScheduledRecording.TYPE_TIMED && currentPosition >= recording.getStartTimeMs() && currentPosition <= recording.getEndTimeMs()); } private void setLastUpdatedProgram(Program program) { mLastUpdatedProgram = program; } private void updateBannerHeight(boolean needProgramDescriptionFadeAnimation) { SoftPreconditions.checkState(mResizeAnimator == null); // Need to measure the layout height with the new description text. CharSequence oldDescription = mProgramDescriptionTextView.getText(); mProgramDescriptionTextView.setText(mProgramDescriptionText); measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); int targetHeight = getMeasuredHeight(); if (mCurrentHeight == 0 || !isShown()) { // Do not add the resize animation when the banner has not been shown before. mCurrentHeight = targetHeight; LayoutParams layoutParams = (LayoutParams) getLayoutParams(); if (targetHeight != layoutParams.height) { layoutParams.height = targetHeight; setLayoutParams(layoutParams); } } else if (mCurrentHeight != targetHeight || needProgramDescriptionFadeAnimation) { // Restore description text for fade in/out animation. if (needProgramDescriptionFadeAnimation) { mProgramDescriptionTextView.setText(oldDescription); } mResizeAnimator = createResizeAnimator(targetHeight, needProgramDescriptionFadeAnimation); mResizeAnimator.start(); } } private Animator createResizeAnimator(int targetHeight, boolean addFadeAnimation) { final ValueAnimator heightAnimator = ValueAnimator.ofInt(mCurrentHeight, targetHeight); heightAnimator.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int value = (Integer) animation.getAnimatedValue(); LayoutParams layoutParams = (LayoutParams) ChannelBannerView.this.getLayoutParams(); if (value != layoutParams.height) { layoutParams.height = value; ChannelBannerView.this.setLayoutParams(layoutParams); } mCurrentHeight = value; } }); heightAnimator.setDuration(mResizeAnimDuration); heightAnimator.setInterpolator(mResizeInterpolator); if (!addFadeAnimation) { heightAnimator.addListener(mResizeAnimatorListener); return heightAnimator; } AnimatorSet fadeOutAndHeightAnimator = new AnimatorSet(); fadeOutAndHeightAnimator.playTogether(mProgramDescriptionFadeOutAnimator, heightAnimator); AnimatorSet animator = new AnimatorSet(); animator.playSequentially(fadeOutAndHeightAnimator, mProgramDescriptionFadeInAnimator); animator.addListener(mResizeAnimatorListener); return animator; } @Override public void onAccessibilityStateChanged(boolean enabled) { mAutoHideScheduler.onAccessibilityStateChanged(enabled); } }