diff options
Diffstat (limited to 'src/com/android/tv/guide/ProgramTableAdapter.java')
-rw-r--r-- | src/com/android/tv/guide/ProgramTableAdapter.java | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java new file mode 100644 index 00000000..df764668 --- /dev/null +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -0,0 +1,509 @@ +/* + * 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.guide; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.media.tv.TvContentRating; +import android.os.Handler; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.RecycledViewPool; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.data.Channel; +import com.android.tv.data.Program; +import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; +import com.android.tv.parental.ParentalControlSettings; +import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; +import com.android.tv.util.TvInputManagerHelper; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adapts the {@link ProgramListAdapter} list to the body of the program guide table. + */ +public class ProgramTableAdapter extends + RecyclerView.Adapter<ProgramTableAdapter.ProgramRowHolder> { + private static final String TAG = "ProgramTableAdapter"; + private static final boolean DEBUG = false; + + private final Context mContext; + private final TvInputManagerHelper mTvInputManagerHelper; + private final ProgramManager mProgramManager; + private final ProgramGuide mProgramGuide; + private final Handler mHandler = new Handler(); + private final List<ProgramListAdapter> mProgramListAdapters = new ArrayList<>(); + private final RecycledViewPool mRecycledViewPool; + + private final int mChannelLogoWidth; + private final int mChannelLogoHeight; + private final int mImageWidth; + private final int mImageHeight; + private final String mProgramTitleForNoInformation; + private final String mProgramTitleForBlockedChannel; + private final int mChannelTextColor; + private final int mChannelBlockedTextColor; + private final int mDetailTextColor; + private final int mDetailGrayedTextColor; + private final int mAnimationDuration; + private final int mDetailPadding; + private final TextAppearanceSpan mEpisodeTitleStyle; + + public ProgramTableAdapter(Context context, TvInputManagerHelper tvInputManagerHelper, + ProgramManager programManager, ProgramGuide programGuide) { + mContext = context; + mTvInputManagerHelper = tvInputManagerHelper; + mProgramManager = programManager; + mProgramGuide = programGuide; + + Resources res = context.getResources(); + mChannelLogoWidth = res.getDimensionPixelSize( + R.dimen.program_guide_table_header_column_channel_logo_width); + mChannelLogoHeight = res.getDimensionPixelSize( + R.dimen.program_guide_table_header_column_channel_logo_height); + mImageWidth = res.getDimensionPixelSize( + R.dimen.program_guide_table_detail_image_width); + mImageHeight = res.getDimensionPixelSize( + R.dimen.program_guide_table_detail_image_height); + mProgramTitleForNoInformation = res.getString( + R.string.program_title_for_no_information); + mProgramTitleForBlockedChannel = res.getString( + R.string.program_title_for_blocked_channel); + mChannelTextColor = res.getColor( + R.color.program_guide_table_header_column_channel_number_text_color); + mChannelBlockedTextColor = res.getColor(R.color + .program_guide_table_header_column_channel_number_blocked_text_color); + mDetailTextColor = res.getColor( + R.color.program_guide_table_detail_title_text_color); + mDetailGrayedTextColor = res.getColor( + R.color.program_guide_table_detail_title_grayed_text_color); + mAnimationDuration = + res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration); + mDetailPadding = res.getDimensionPixelOffset( + R.dimen.program_guide_table_detail_padding); + + int episodeTitleSize = res.getDimensionPixelSize( + R.dimen.program_guide_table_detail_episode_title_text_size); + ColorStateList episodeTitleColor = ColorStateList.valueOf( + res.getColor(R.color.program_guide_table_detail_episode_title_text_color)); + mEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize, + episodeTitleColor, null); + + mRecycledViewPool = new RecycledViewPool(); + mRecycledViewPool.setMaxRecycledViews(R.layout.program_guide_table_item, + context.getResources().getInteger( + R.integer.max_recycled_view_pool_epg_table_item)); + mProgramManager.addListener(new ProgramManager.ListenerAdapter() { + @Override + public void onChannelsUpdated() { + mHandler.post(new Runnable() { + @Override + public void run() { + update(); + } + }); + } + }); + update(); + } + + private void update() { + if (DEBUG) Log.d(TAG, "update " + mProgramManager.getChannelCount() + " channels"); + for (TableEntriesUpdatedListener listener : mProgramListAdapters) { + mProgramManager.removeTableEntriesUpdatedListener(listener); + } + mProgramListAdapters.clear(); + for (int i = 0; i < mProgramManager.getChannelCount(); i++) { + ProgramListAdapter listAdapter = new ProgramListAdapter(mContext, mProgramManager, i); + mProgramManager.addTableEntriesUpdatedListener(listAdapter); + mProgramListAdapters.add(listAdapter); + } + notifyDataSetChanged(); + } + + @Override + public int getItemCount() { + return mProgramListAdapters.size(); + } + + @Override + public int getItemViewType(int position) { + return R.layout.program_guide_table_row; + } + + @Override + public void onBindViewHolder(ProgramRowHolder holder, int position) { + holder.onBind(position); + } + + @Override + public ProgramRowHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); + ProgramRow programRow = (ProgramRow) itemView.findViewById(R.id.row); + programRow.setRecycledViewPool(mRecycledViewPool); + return new ProgramRowHolder(itemView); + } + + // TODO: make it static + public class ProgramRowHolder extends RecyclerView.ViewHolder implements + ProgramRow.ChildFocusListener, Program.LoadPosterArtCallback, + Channel.LoadImageCallback { + + private final ViewGroup mContainer; + private final ProgramRow mProgramRow; + private ProgramManager.TableEntry mSelectedEntry; + private Animator mDetailOutAnimator; + private Animator mDetailInAnimator; + private final Runnable mDetailInStarter = new Runnable() { + @Override + public void run() { + mProgramRow.removeOnScrollListener(mOnScrollListener); + if (mDetailInAnimator != null) { + mDetailInAnimator.start(); + } + } + }; + + private final RecyclerView.OnScrollListener mOnScrollListener = + new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + onHorizontalScrolled(); + } + }; + + // Members of Program Details + private final ViewGroup mDetailView; + private final ImageView mImageView; + private final ImageView mBlockView; + private final TextView mTitleView; + private final TextView mTimeView; + private final TextView mDescriptionView; + private final TextView mAspectRatioView; + private final TextView mResolutionView; + + // Members of Channel Header + private Channel mChannel; + private final View mChannelHeaderView; + private final TextView mChannelNumberView; + private final TextView mChannelNameView; + private final ImageView mChannelLogoView; + private final ImageView mChannelBlockView; + + public ProgramRowHolder(View itemView) { + super(itemView); + + mContainer = (ViewGroup) itemView; + mProgramRow = (ProgramRow) mContainer.findViewById(R.id.row); + + mDetailView = (ViewGroup) mContainer.findViewById(R.id.detail); + mImageView = (ImageView) mDetailView.findViewById(R.id.image); + mBlockView = (ImageView) mDetailView.findViewById(R.id.block); + mTitleView = (TextView) mDetailView.findViewById(R.id.title); + mTimeView = (TextView) mDetailView.findViewById(R.id.time); + mDescriptionView = (TextView) mDetailView.findViewById(R.id.desc); + mAspectRatioView = (TextView) mDetailView.findViewById(R.id.aspect_ratio); + mResolutionView = (TextView) mDetailView.findViewById(R.id.resolution); + + mChannelHeaderView = mContainer.findViewById(R.id.header_column); + mChannelNumberView = (TextView) mContainer.findViewById(R.id.channel_number); + mChannelNameView = (TextView) mContainer.findViewById(R.id.channel_name); + mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo); + mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block); + } + + public void onBind(int position) { + onBindChannel(mProgramManager.getChannel(position)); + + mProgramRow.swapAdapter(mProgramListAdapters.get(position), true); + mProgramRow.setProgramManager(mProgramManager); + mProgramRow.setChannel(mProgramManager.getChannel(position)); + mProgramRow.setChildFocusListener(this); + mProgramRow.resetScroll(mProgramGuide.getTimelineRowScrollOffset()); + + mDetailView.setVisibility(View.GONE); + + // The bottom-left of the last channel header view will have a rounded corner. + mChannelHeaderView.setBackgroundResource((position < mProgramListAdapters.size() - 1) + ? R.drawable.program_guide_table_header_column_item_background + : R.drawable.program_guide_table_header_column_last_item_background); + } + + private void onBindChannel(Channel channel) { + if (DEBUG) Log.d(TAG, "onBindChannel " + channel); + + mChannel = channel; + if (channel == null) { + mChannelNumberView.setVisibility(View.GONE); + mChannelNameView.setVisibility(View.GONE); + mChannelLogoView.setVisibility(View.GONE); + mChannelBlockView.setVisibility(View.GONE); + return; + } + + String displayNumber = channel.getDisplayNumber(); + if (displayNumber == null) { + mChannelNumberView.setVisibility(View.GONE); + } else { + int size; + if (displayNumber.length() <= 4) { + size = R.dimen.program_guide_table_header_column_channel_number_large_font_size; + } else { + size = R.dimen.program_guide_table_header_column_channel_number_small_font_size; + } + mChannelNumberView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + mChannelNumberView.getContext().getResources().getDimension(size)); + mChannelNumberView.setText(displayNumber); + mChannelNumberView.setVisibility(View.VISIBLE); + } + mChannelNumberView.setTextColor(isChannelLocked(channel) + ? mChannelBlockedTextColor : mChannelTextColor); + + mChannelLogoView.setImageBitmap(null); + mChannelLogoView.setVisibility(View.GONE); + if (isChannelLocked(channel)) { + mChannelNameView.setVisibility(View.GONE); + mChannelBlockView.setVisibility(View.VISIBLE); + } else { + mChannelNameView.setText(channel.getDisplayName()); + mChannelNameView.setVisibility(View.VISIBLE); + mChannelBlockView.setVisibility(View.GONE); + + mChannel.loadBitmap(itemView.getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, + mChannelLogoWidth, mChannelLogoHeight, this); + } + } + + @Override + public void onChildFocus(View oldFocus, View newFocus) { + if (newFocus == null) { + return; + } + mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry(); + if (oldFocus == null) { + updateDetailView(); + return; + } + + if (Program.isValid(mSelectedEntry.program)) { + Program program = mSelectedEntry.program; + if (getProgramBlock(program) == null) { + program.prefetchPosterArt(itemView.getContext(), mImageWidth, mImageHeight); + } + } + + // -1 means the selection goes rightwards and 1 goes leftwards + int direction = oldFocus.getLeft() < newFocus.getLeft() ? -1 : 1; + View detailContentView = mDetailView.findViewById(R.id.detail_content); + + if (mDetailInAnimator == null) { + mDetailOutAnimator = ObjectAnimator.ofPropertyValuesHolder(detailContentView, + PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), + PropertyValuesHolder.ofFloat(View.TRANSLATION_X, + 0f, direction * mDetailPadding)); + mDetailOutAnimator.setDuration(mAnimationDuration); + mDetailOutAnimator.addListener( + new HardwareLayerAnimatorListenerAdapter(detailContentView) { + @Override + public void onAnimationEnd(Animator animator) { + super.onAnimationEnd(animator); + mDetailOutAnimator = null; + mHandler.removeCallbacks(mDetailInStarter); + mHandler.postDelayed(mDetailInStarter, mAnimationDuration); + } + }); + + mProgramRow.addOnScrollListener(mOnScrollListener); + mDetailOutAnimator.start(); + } else { + if (mDetailInAnimator.isStarted()) { + mDetailInAnimator.cancel(); + detailContentView.setAlpha(0); + } + + mHandler.removeCallbacks(mDetailInStarter); + mHandler.postDelayed(mDetailInStarter, mAnimationDuration); + } + + mDetailInAnimator = ObjectAnimator.ofPropertyValuesHolder(detailContentView, + PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), + PropertyValuesHolder.ofFloat(View.TRANSLATION_X, + direction * -mDetailPadding, 0f)); + mDetailInAnimator.setDuration(mAnimationDuration); + mDetailInAnimator.addListener( + new HardwareLayerAnimatorListenerAdapter(detailContentView) { + @Override + public void onAnimationStart(Animator animator) { + super.onAnimationStart(animator); + updateDetailView(); + } + + @Override + public void onAnimationEnd(Animator animator) { + super.onAnimationEnd(animator); + mDetailInAnimator = null; + } + }); + } + + private void updateDetailView() { + if (Program.isValid(mSelectedEntry.program)) { + mTitleView.setTextColor(mDetailTextColor); + Context context = itemView.getContext(); + Program program = mSelectedEntry.program; + + TvContentRating blockedRating = getProgramBlock(program); + + mImageView.setImageBitmap(null); + mImageView.setVisibility(View.GONE); + if (blockedRating == null) { + program.loadPosterArt(context, mImageWidth, mImageHeight, this); + } + + if (TextUtils.isEmpty(program.getEpisodeTitle())) { + mTitleView.setText(program.getTitle()); + } else { + String title = program.getTitle(); + String episodeTitle = program.getEpisodeDisplayTitle(mContext); + String fullTitle = title + " " + episodeTitle; + + SpannableString text = new SpannableString(fullTitle); + text.setSpan(mEpisodeTitleStyle, + fullTitle.length() - episodeTitle.length(), fullTitle.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + mTitleView.setText(text); + } + + updateTextView(mTimeView, Utils.getDurationString(context, + program.getStartTimeUtcMillis(), + program.getEndTimeUtcMillis(), false)); + + if (blockedRating == null) { + mBlockView.setVisibility(View.GONE); + updateTextView(mDescriptionView, program.getDescription()); + } else { + mBlockView.setVisibility(View.VISIBLE); + updateTextView(mDescriptionView, getBlockedDescription(blockedRating)); + } + + updateTextView(mAspectRatioView, Utils.getAspectRatioString( + program.getVideoWidth(), program.getVideoHeight())); + + int videoDefinitionLevel = Utils.getVideoDefinitionLevelFromSize( + program.getVideoWidth(), program.getVideoHeight()); + updateTextView(mResolutionView, Utils.getVideoDefinitionLevelString( + context, videoDefinitionLevel)); + } else { + mTitleView.setTextColor(mDetailGrayedTextColor); + if (mSelectedEntry.isBlocked()) { + updateTextView(mTitleView, mProgramTitleForBlockedChannel); + } else { + updateTextView(mTitleView, mProgramTitleForNoInformation); + } + mImageView.setVisibility(View.GONE); + mBlockView.setVisibility(View.GONE); + mTimeView.setVisibility(View.GONE); + mDescriptionView.setVisibility(View.GONE); + mAspectRatioView.setVisibility(View.GONE); + mResolutionView.setVisibility(View.GONE); + } + } + + private TvContentRating getProgramBlock(Program program) { + ParentalControlSettings parental = mTvInputManagerHelper.getParentalControlSettings(); + if (!parental.isParentalControlsEnabled()) { + return null; + } + return parental.getBlockedRating(program.getContentRatings()); + } + + private boolean isChannelLocked(Channel channel) { + return mTvInputManagerHelper.getParentalControlSettings().isParentalControlsEnabled() + && channel.isLocked(); + } + + private String getBlockedDescription(TvContentRating blockedRating) { + String name = mTvInputManagerHelper.getContentRatingsManager() + .getDisplayNameForRating(blockedRating); + if (TextUtils.isEmpty(name)) { + return mContext.getString(R.string.program_guide_content_locked); + } else { + return mContext.getString(R.string.program_guide_content_locked_format, name); + } + } + + private void updateTextView(TextView textView, String text) { + if (!TextUtils.isEmpty(text)) { + textView.setVisibility(View.VISIBLE); + textView.setText(text); + } else { + textView.setVisibility(View.GONE); + } + } + + @Override + public void onLoadPosterArtFinished(Program program, Bitmap posterArt) { + if (posterArt == null || mSelectedEntry == null || mSelectedEntry.program == null) { + return; + } + + String posterArtUri = mSelectedEntry.program.getPosterArtUri(); + if (posterArtUri == null || !posterArtUri.equals(program.getPosterArtUri())) { + return; + } + + mImageView.setImageBitmap(posterArt); + mImageView.setVisibility(View.VISIBLE); + } + + @Override + public void onLoadImageFinished(Channel channel, int type, Bitmap logo) { + if (logo == null || mChannel == null || mChannel.getId() != channel.getId()) { + return; + } + + mChannelLogoView.setImageBitmap(logo); + mChannelNameView.setVisibility(View.GONE); + mChannelLogoView.setVisibility(View.VISIBLE); + } + + private void onHorizontalScrolled() { + if (mDetailInAnimator != null) { + mHandler.removeCallbacks(mDetailInStarter); + mHandler.postDelayed(mDetailInStarter, mAnimationDuration); + } + } + } +} |