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 17package com.android.tv.guide; 18 19import static com.android.tv.util.ImageLoader.ImageLoaderCallback; 20 21import android.animation.Animator; 22import android.animation.ObjectAnimator; 23import android.animation.PropertyValuesHolder; 24import android.content.Context; 25import android.content.res.ColorStateList; 26import android.content.res.Resources; 27import android.graphics.Bitmap; 28import android.media.tv.TvContentRating; 29import android.media.tv.TvInputInfo; 30import android.os.Handler; 31import android.support.annotation.NonNull; 32import android.support.annotation.Nullable; 33import android.support.v7.widget.RecyclerView; 34import android.support.v7.widget.RecyclerView.RecycledViewPool; 35import android.text.Html; 36import android.text.Spannable; 37import android.text.SpannableString; 38import android.text.TextUtils; 39import android.text.style.TextAppearanceSpan; 40import android.util.Log; 41import android.util.TypedValue; 42import android.view.LayoutInflater; 43import android.view.View; 44import android.view.ViewGroup; 45import android.view.ViewParent; 46import android.view.ViewTreeObserver; 47import android.view.accessibility.AccessibilityManager; 48import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; 49import android.widget.ImageView; 50import android.widget.LinearLayout; 51import android.widget.TextView; 52 53import com.android.tv.R; 54import com.android.tv.TvApplication; 55import com.android.tv.common.TvCommonUtils; 56import com.android.tv.common.feature.CommonFeatures; 57import com.android.tv.data.Channel; 58import com.android.tv.data.Program; 59import com.android.tv.data.Program.CriticScore; 60import com.android.tv.dvr.DvrDataManager; 61import com.android.tv.dvr.DvrManager; 62import com.android.tv.dvr.data.ScheduledRecording; 63import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; 64import com.android.tv.parental.ParentalControlSettings; 65import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; 66import com.android.tv.util.ImageCache; 67import com.android.tv.util.ImageLoader; 68import com.android.tv.util.ImageLoader.LoadTvInputLogoTask; 69import com.android.tv.util.TvInputManagerHelper; 70import com.android.tv.util.Utils; 71 72import java.util.ArrayList; 73import java.util.List; 74 75/** 76 * Adapts the {@link ProgramListAdapter} list to the body of the program guide table. 77 */ 78class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowViewHolder> 79 implements ProgramManager.TableEntryChangedListener { 80 private static final String TAG = "ProgramTableAdapter"; 81 private static final boolean DEBUG = false; 82 83 private final Context mContext; 84 private final TvInputManagerHelper mTvInputManagerHelper; 85 private final DvrManager mDvrManager; 86 private final DvrDataManager mDvrDataManager; 87 private final ProgramManager mProgramManager; 88 private final AccessibilityManager mAccessibilityManager; 89 private final ProgramGuide mProgramGuide; 90 private final Handler mHandler = new Handler(); 91 private final List<ProgramListAdapter> mProgramListAdapters = new ArrayList<>(); 92 private final RecycledViewPool mRecycledViewPool; 93 // views to be be reused when displaying critic scores 94 private final List<LinearLayout> mCriticScoreViews; 95 96 private final int mChannelLogoWidth; 97 private final int mChannelLogoHeight; 98 private final int mImageWidth; 99 private final int mImageHeight; 100 private final String mProgramTitleForNoInformation; 101 private final String mProgramTitleForBlockedChannel; 102 private final int mChannelTextColor; 103 private final int mChannelBlockedTextColor; 104 private final int mDetailTextColor; 105 private final int mDetailGrayedTextColor; 106 private final int mAnimationDuration; 107 private final int mDetailPadding; 108 private final TextAppearanceSpan mEpisodeTitleStyle; 109 private final String mProgramRecordableText; 110 private final String mRecordingScheduledText; 111 private final String mRecordingConflictText; 112 private final String mRecordingFailedText; 113 private final String mRecordingInProgressText; 114 private final int mDvrPaddingStartWithTrack; 115 private final int mDvrPaddingStartWithOutTrack; 116 117 ProgramTableAdapter(Context context, ProgramGuide programGuide) { 118 mContext = context; 119 mAccessibilityManager = 120 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 121 mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper(); 122 if (CommonFeatures.DVR.isEnabled(context)) { 123 mDvrManager = TvApplication.getSingletons(context).getDvrManager(); 124 mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); 125 } else { 126 mDvrManager = null; 127 mDvrDataManager = null; 128 } 129 mProgramGuide = programGuide; 130 mProgramManager = programGuide.getProgramManager(); 131 132 Resources res = context.getResources(); 133 mChannelLogoWidth = res.getDimensionPixelSize( 134 R.dimen.program_guide_table_header_column_channel_logo_width); 135 mChannelLogoHeight = res.getDimensionPixelSize( 136 R.dimen.program_guide_table_header_column_channel_logo_height); 137 mImageWidth = res.getDimensionPixelSize( 138 R.dimen.program_guide_table_detail_image_width); 139 mImageHeight = res.getDimensionPixelSize( 140 R.dimen.program_guide_table_detail_image_height); 141 mProgramTitleForNoInformation = res.getString( 142 R.string.program_title_for_no_information); 143 mProgramTitleForBlockedChannel = res.getString( 144 R.string.program_title_for_blocked_channel); 145 mChannelTextColor = res.getColor( 146 R.color.program_guide_table_header_column_channel_number_text_color, null); 147 mChannelBlockedTextColor = res.getColor( 148 R.color.program_guide_table_header_column_channel_number_blocked_text_color, null); 149 mDetailTextColor = res.getColor( 150 R.color.program_guide_table_detail_title_text_color, null); 151 mDetailGrayedTextColor = res.getColor( 152 R.color.program_guide_table_detail_title_grayed_text_color, null); 153 mAnimationDuration = 154 res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration); 155 mDetailPadding = res.getDimensionPixelOffset( 156 R.dimen.program_guide_table_detail_padding); 157 mProgramRecordableText = res.getString(R.string.dvr_epg_program_recordable); 158 mRecordingScheduledText = res.getString(R.string.dvr_epg_program_recording_scheduled); 159 mRecordingConflictText = res.getString(R.string.dvr_epg_program_recording_conflict); 160 mRecordingFailedText = res.getString(R.string.dvr_epg_program_recording_failed); 161 mRecordingInProgressText = res.getString(R.string.dvr_epg_program_recording_in_progress); 162 mDvrPaddingStartWithTrack = res.getDimensionPixelOffset( 163 R.dimen.program_guide_table_detail_dvr_margin_start); 164 mDvrPaddingStartWithOutTrack = res.getDimensionPixelOffset( 165 R.dimen.program_guide_table_detail_dvr_margin_start_without_track); 166 167 int episodeTitleSize = res.getDimensionPixelSize( 168 R.dimen.program_guide_table_detail_episode_title_text_size); 169 ColorStateList episodeTitleColor = ColorStateList.valueOf( 170 res.getColor(R.color.program_guide_table_detail_episode_title_text_color, null)); 171 mEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize, 172 episodeTitleColor, null); 173 174 mCriticScoreViews = new ArrayList<>(); 175 mRecycledViewPool = new RecycledViewPool(); 176 mRecycledViewPool.setMaxRecycledViews(R.layout.program_guide_table_item, 177 context.getResources().getInteger( 178 R.integer.max_recycled_view_pool_epg_table_item)); 179 mProgramManager.addListener(new ProgramManager.ListenerAdapter() { 180 @Override 181 public void onChannelsUpdated() { 182 update(); 183 } 184 }); 185 update(); 186 mProgramManager.addTableEntryChangedListener(this); 187 } 188 189 private void update() { 190 if (DEBUG) Log.d(TAG, "update " + mProgramManager.getChannelCount() + " channels"); 191 for (TableEntriesUpdatedListener listener : mProgramListAdapters) { 192 mProgramManager.removeTableEntriesUpdatedListener(listener); 193 } 194 mProgramListAdapters.clear(); 195 for (int i = 0; i < mProgramManager.getChannelCount(); i++) { 196 ProgramListAdapter listAdapter = new ProgramListAdapter(mContext.getResources(), 197 mProgramGuide, i); 198 mProgramManager.addTableEntriesUpdatedListener(listAdapter); 199 mProgramListAdapters.add(listAdapter); 200 } 201 notifyDataSetChanged(); 202 } 203 204 @Override 205 public int getItemCount() { 206 return mProgramListAdapters.size(); 207 } 208 209 @Override 210 public int getItemViewType(int position) { 211 return R.layout.program_guide_table_row; 212 } 213 214 @Override 215 public void onBindViewHolder(ProgramRowViewHolder holder, int position) { 216 holder.onBind(position); 217 } 218 219 @Override 220 public void onBindViewHolder(ProgramRowViewHolder holder, int position, List<Object> payloads) { 221 if (!payloads.isEmpty()) { 222 holder.updateDetailView(); 223 } else { 224 super.onBindViewHolder(holder, position, payloads); 225 } 226 } 227 228 @Override 229 public ProgramRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 230 View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); 231 ProgramRow programRow = (ProgramRow) itemView.findViewById(R.id.row); 232 programRow.setRecycledViewPool(mRecycledViewPool); 233 return new ProgramRowViewHolder(itemView); 234 } 235 236 @Override 237 public void onTableEntryChanged(ProgramManager.TableEntry tableEntry) { 238 int channelIndex = mProgramManager.getChannelIndex(tableEntry.channelId); 239 int pos = mProgramManager.getProgramIdIndex(tableEntry.channelId, tableEntry.getId()); 240 if (DEBUG) Log.d(TAG, "update(" + channelIndex + ", " + pos + ")"); 241 mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry); 242 notifyItemChanged(channelIndex, true); 243 } 244 245 class ProgramRowViewHolder extends RecyclerView.ViewHolder 246 implements ProgramRow.ChildFocusListener { 247 248 private final ViewGroup mContainer; 249 private final ProgramRow mProgramRow; 250 private ProgramManager.TableEntry mSelectedEntry; 251 private Animator mDetailOutAnimator; 252 private Animator mDetailInAnimator; 253 private final Runnable mDetailInStarter = new Runnable() { 254 @Override 255 public void run() { 256 mProgramRow.removeOnScrollListener(mOnScrollListener); 257 if (mDetailInAnimator != null) { 258 mDetailInAnimator.start(); 259 } 260 } 261 }; 262 private final Runnable mUpdateDetailViewRunnable = new Runnable() { 263 @Override 264 public void run() { 265 updateDetailView(); 266 } 267 }; 268 269 private final RecyclerView.OnScrollListener mOnScrollListener = 270 new RecyclerView.OnScrollListener() { 271 @Override 272 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 273 onHorizontalScrolled(); 274 } 275 }; 276 277 private final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener = 278 new ViewTreeObserver.OnGlobalFocusChangeListener() { 279 @Override 280 public void onGlobalFocusChanged(View oldFocus, View newFocus) { 281 onChildFocus( 282 GuideUtils.isDescendant(mContainer, oldFocus) ? oldFocus : null, 283 GuideUtils.isDescendant(mContainer, newFocus) ? newFocus : null); 284 } 285 }; 286 287 // Members of Program Details 288 private final ViewGroup mDetailView; 289 private final ImageView mImageView; 290 private final ImageView mBlockView; 291 private final TextView mTitleView; 292 private final TextView mTimeView; 293 private final LinearLayout mCriticScoresLayout; 294 private final TextView mDescriptionView; 295 private final TextView mAspectRatioView; 296 private final TextView mResolutionView; 297 private final ImageView mDvrIconView; 298 private final TextView mDvrTextIconView; 299 private final TextView mDvrStatusView; 300 private final ViewGroup mDvrIndicator; 301 302 // Members of Channel Header 303 private Channel mChannel; 304 private final View mChannelHeaderView; 305 private final TextView mChannelNumberView; 306 private final TextView mChannelNameView; 307 private final ImageView mChannelLogoView; 308 private final ImageView mChannelBlockView; 309 private final ImageView mInputLogoView; 310 311 private boolean mIsInputLogoVisible; 312 private AccessibilityStateChangeListener mAccessibilityStateChangeListener = 313 new AccessibilityManager.AccessibilityStateChangeListener() { 314 @Override 315 public void onAccessibilityStateChanged(boolean enable) { 316 enable &= !TvCommonUtils.isRunningInTest(); 317 mDetailView.setFocusable(enable); 318 mChannelHeaderView.setFocusable(enable); 319 } 320 }; 321 322 ProgramRowViewHolder(View itemView) { 323 super(itemView); 324 325 mContainer = (ViewGroup) itemView; 326 mContainer.addOnAttachStateChangeListener( 327 new View.OnAttachStateChangeListener() { 328 @Override 329 public void onViewAttachedToWindow(View v) { 330 mContainer.getViewTreeObserver() 331 .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener); 332 mAccessibilityManager.addAccessibilityStateChangeListener( 333 mAccessibilityStateChangeListener); 334 } 335 336 @Override 337 public void onViewDetachedFromWindow(View v) { 338 mContainer.getViewTreeObserver() 339 .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener); 340 mAccessibilityManager.removeAccessibilityStateChangeListener( 341 mAccessibilityStateChangeListener); 342 } 343 }); 344 mProgramRow = (ProgramRow) mContainer.findViewById(R.id.row); 345 346 mDetailView = (ViewGroup) mContainer.findViewById(R.id.detail); 347 mImageView = (ImageView) mDetailView.findViewById(R.id.image); 348 mBlockView = (ImageView) mDetailView.findViewById(R.id.block); 349 mTitleView = (TextView) mDetailView.findViewById(R.id.title); 350 mTimeView = (TextView) mDetailView.findViewById(R.id.time); 351 mDescriptionView = (TextView) mDetailView.findViewById(R.id.desc); 352 mAspectRatioView = (TextView) mDetailView.findViewById(R.id.aspect_ratio); 353 mResolutionView = (TextView) mDetailView.findViewById(R.id.resolution); 354 mDvrIconView = (ImageView) mDetailView.findViewById(R.id.dvr_icon); 355 mDvrTextIconView = (TextView) mDetailView.findViewById(R.id.dvr_text_icon); 356 mDvrStatusView = (TextView) mDetailView.findViewById(R.id.dvr_status); 357 mDvrIndicator = (ViewGroup) mContainer.findViewById(R.id.dvr_indicator); 358 mCriticScoresLayout = (LinearLayout) mDetailView.findViewById(R.id.critic_scores); 359 360 mChannelHeaderView = mContainer.findViewById(R.id.header_column); 361 mChannelNumberView = (TextView) mContainer.findViewById(R.id.channel_number); 362 mChannelNameView = (TextView) mContainer.findViewById(R.id.channel_name); 363 mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo); 364 mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block); 365 mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo); 366 367 boolean accessibilityEnabled = mAccessibilityManager.isEnabled() 368 && !TvCommonUtils.isRunningInTest(); 369 mDetailView.setFocusable(accessibilityEnabled); 370 mChannelHeaderView.setFocusable(accessibilityEnabled); 371 } 372 373 public void onBind(int position) { 374 onBindChannel(mProgramManager.getChannel(position)); 375 376 mProgramRow.swapAdapter(mProgramListAdapters.get(position), true); 377 mProgramRow.setProgramGuide(mProgramGuide); 378 mProgramRow.setChannel(mProgramManager.getChannel(position)); 379 mProgramRow.setChildFocusListener(this); 380 mProgramRow.resetScroll(mProgramGuide.getTimelineRowScrollOffset()); 381 382 mDetailView.setVisibility(View.GONE); 383 384 // The bottom-left of the last channel header view will have a rounded corner. 385 mChannelHeaderView.setBackgroundResource((position < mProgramListAdapters.size() - 1) 386 ? R.drawable.program_guide_table_header_column_item_background 387 : R.drawable.program_guide_table_header_column_last_item_background); 388 } 389 390 private void onBindChannel(Channel channel) { 391 if (DEBUG) Log.d(TAG, "onBindChannel " + channel); 392 393 mChannel = channel; 394 mInputLogoView.setVisibility(View.GONE); 395 mIsInputLogoVisible = false; 396 if (channel == null) { 397 mChannelNumberView.setVisibility(View.GONE); 398 mChannelNameView.setVisibility(View.GONE); 399 mChannelLogoView.setVisibility(View.GONE); 400 mChannelBlockView.setVisibility(View.GONE); 401 return; 402 } 403 404 String displayNumber = channel.getDisplayNumber(); 405 if (displayNumber == null) { 406 mChannelNumberView.setVisibility(View.GONE); 407 } else { 408 int size; 409 if (displayNumber.length() <= 4) { 410 size = R.dimen.program_guide_table_header_column_channel_number_large_font_size; 411 } else { 412 size = R.dimen.program_guide_table_header_column_channel_number_small_font_size; 413 } 414 mChannelNumberView.setTextSize(TypedValue.COMPLEX_UNIT_PX, 415 mChannelNumberView.getContext().getResources().getDimension(size)); 416 mChannelNumberView.setText(displayNumber); 417 mChannelNumberView.setVisibility(View.VISIBLE); 418 } 419 mChannelNumberView.setTextColor( 420 isChannelLocked(channel) ? mChannelBlockedTextColor : mChannelTextColor); 421 422 mChannelLogoView.setImageBitmap(null); 423 mChannelLogoView.setVisibility(View.GONE); 424 if (isChannelLocked(channel)) { 425 mChannelNameView.setVisibility(View.GONE); 426 mChannelBlockView.setVisibility(View.VISIBLE); 427 } else { 428 mChannelNameView.setText(channel.getDisplayName()); 429 mChannelNameView.setVisibility(View.VISIBLE); 430 mChannelBlockView.setVisibility(View.GONE); 431 432 mChannel.loadBitmap(itemView.getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, 433 mChannelLogoWidth, mChannelLogoHeight, 434 createChannelLogoLoadedCallback(this, channel.getId())); 435 } 436 } 437 438 @Override 439 public void onChildFocus(View oldFocus, View newFocus) { 440 if (newFocus == null) { 441 return; 442 } // When the accessibility service is enabled, focus might be put on channel's header or 443 // detail view, besides program items. 444 if (newFocus == mChannelHeaderView) { 445 mSelectedEntry = ((ProgramItemView) mProgramRow.getChildAt(0)).getTableEntry(); 446 } else if (newFocus == mDetailView) { 447 return; 448 } else { 449 mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry(); 450 } 451 if (oldFocus == null) { 452 // Focus moved from other row. 453 if (mProgramGuide.getProgramGrid().isInLayout()) { 454 // We need to post runnable to avoid updating detail view when 455 // the recycler view is in layout, which may cause detail view not 456 // laid out according to the updated contents. 457 mHandler.post(mUpdateDetailViewRunnable); 458 } else { 459 updateDetailView(); 460 } 461 return; 462 } 463 464 if (Program.isValid(mSelectedEntry.program)) { 465 Program program = mSelectedEntry.program; 466 if (getProgramBlock(program) == null) { 467 program.prefetchPosterArt(itemView.getContext(), mImageWidth, mImageHeight); 468 } 469 } 470 471 // -1 means the selection goes rightwards and 1 goes leftwards 472 int direction = oldFocus.getLeft() < newFocus.getLeft() ? -1 : 1; 473 View detailContentView = mDetailView.findViewById(R.id.detail_content); 474 475 if (mDetailInAnimator == null) { 476 mDetailOutAnimator = ObjectAnimator.ofPropertyValuesHolder(detailContentView, 477 PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), 478 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 479 0f, direction * mDetailPadding)); 480 mDetailOutAnimator.setDuration(mAnimationDuration); 481 mDetailOutAnimator.addListener( 482 new HardwareLayerAnimatorListenerAdapter(detailContentView) { 483 @Override 484 public void onAnimationEnd(Animator animator) { 485 super.onAnimationEnd(animator); 486 mDetailOutAnimator = null; 487 mHandler.removeCallbacks(mDetailInStarter); 488 mHandler.postDelayed(mDetailInStarter, mAnimationDuration); 489 } 490 }); 491 492 mProgramRow.addOnScrollListener(mOnScrollListener); 493 mDetailOutAnimator.start(); 494 } else { 495 if (mDetailInAnimator.isStarted()) { 496 mDetailInAnimator.cancel(); 497 detailContentView.setAlpha(0); 498 } 499 500 mHandler.removeCallbacks(mDetailInStarter); 501 mHandler.postDelayed(mDetailInStarter, mAnimationDuration); 502 } 503 504 mDetailInAnimator = ObjectAnimator.ofPropertyValuesHolder(detailContentView, 505 PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), 506 PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 507 direction * -mDetailPadding, 0f)); 508 mDetailInAnimator.setDuration(mAnimationDuration); 509 mDetailInAnimator.addListener( 510 new HardwareLayerAnimatorListenerAdapter(detailContentView) { 511 @Override 512 public void onAnimationStart(Animator animator) { 513 super.onAnimationStart(animator); 514 updateDetailView(); 515 } 516 517 @Override 518 public void onAnimationEnd(Animator animator) { 519 super.onAnimationEnd(animator); 520 mDetailInAnimator = null; 521 } 522 }); 523 } 524 525 private void updateDetailView() { 526 if (mSelectedEntry == null) { 527 // The view holder is never on focus before. 528 return; 529 } 530 if (DEBUG) Log.d(TAG, "updateDetailView"); 531 mCriticScoresLayout.removeAllViews(); 532 if (Program.isValid(mSelectedEntry.program)) { 533 mTitleView.setTextColor(mDetailTextColor); 534 Context context = itemView.getContext(); 535 Program program = mSelectedEntry.program; 536 537 TvContentRating blockedRating = getProgramBlock(program); 538 539 updatePosterArt(null); 540 if (blockedRating == null) { 541 program.loadPosterArt(context, mImageWidth, mImageHeight, 542 createProgramPosterArtCallback(this, program)); 543 } 544 545 String episodeTitle = program.getEpisodeDisplayTitle(mContext); 546 if (TextUtils.isEmpty(episodeTitle)) { 547 mTitleView.setText(program.getTitle()); 548 } else { 549 String title = program.getTitle(); 550 String fullTitle = title + " " + episodeTitle; 551 552 SpannableString text = new SpannableString(fullTitle); 553 text.setSpan(mEpisodeTitleStyle, 554 fullTitle.length() - episodeTitle.length(), fullTitle.length(), 555 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 556 mTitleView.setText(text); 557 } 558 559 updateTextView(mTimeView, Utils.getDurationString(context, 560 program.getStartTimeUtcMillis(), 561 program.getEndTimeUtcMillis(), false)); 562 563 boolean trackMetaDataVisible = updateTextView(mAspectRatioView, Utils 564 .getAspectRatioString(program.getVideoWidth(), program.getVideoHeight())); 565 566 int videoDefinitionLevel = Utils.getVideoDefinitionLevelFromSize( 567 program.getVideoWidth(), program.getVideoHeight()); 568 trackMetaDataVisible |= 569 updateTextView(mResolutionView, Utils.getVideoDefinitionLevelString( 570 context, videoDefinitionLevel)); 571 572 if (mDvrManager != null && mDvrManager.isProgramRecordable(program)) { 573 ScheduledRecording scheduledRecording = 574 mDvrDataManager.getScheduledRecordingForProgramId(program.getId()); 575 String statusText = mProgramRecordableText; 576 int iconResId = 0; 577 if (scheduledRecording != null) { 578 if (mDvrManager.isConflicting(scheduledRecording)) { 579 iconResId = R.drawable.ic_warning_white_12dp; 580 statusText = mRecordingConflictText; 581 } else { 582 switch (scheduledRecording.getState()) { 583 case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: 584 iconResId = R.drawable.ic_recording_program; 585 statusText = mRecordingInProgressText; 586 break; 587 case ScheduledRecording.STATE_RECORDING_NOT_STARTED: 588 iconResId = R.drawable.ic_scheduled_white; 589 statusText = mRecordingScheduledText; 590 break; 591 case ScheduledRecording.STATE_RECORDING_FAILED: 592 iconResId = R.drawable.ic_warning_white_12dp; 593 statusText = mRecordingFailedText; 594 break; 595 default: 596 iconResId = 0; 597 } 598 } 599 } 600 if (iconResId == 0) { 601 mDvrIconView.setVisibility(View.GONE); 602 mDvrTextIconView.setVisibility(View.VISIBLE); 603 } else { 604 mDvrTextIconView.setVisibility(View.GONE); 605 mDvrIconView.setImageResource(iconResId); 606 mDvrIconView.setVisibility(View.VISIBLE); 607 } 608 if (!trackMetaDataVisible) { 609 mDvrIndicator.setPaddingRelative(mDvrPaddingStartWithOutTrack, 0, 0, 0); 610 } else { 611 mDvrIndicator.setPaddingRelative(mDvrPaddingStartWithTrack, 0, 0, 0); 612 } 613 mDvrIndicator.setVisibility(View.VISIBLE); 614 mDvrStatusView.setText(statusText); 615 } else { 616 mDvrIndicator.setVisibility(View.GONE); 617 } 618 619 620 if (blockedRating == null) { 621 mBlockView.setVisibility(View.GONE); 622 updateTextView(mDescriptionView, program.getDescription()); 623 } else { 624 mBlockView.setVisibility(View.VISIBLE); 625 updateTextView(mDescriptionView, getBlockedDescription(blockedRating)); 626 } 627 } else { 628 mTitleView.setTextColor(mDetailGrayedTextColor); 629 if (mSelectedEntry.isBlocked()) { 630 updateTextView(mTitleView, mProgramTitleForBlockedChannel); 631 } else { 632 updateTextView(mTitleView, mProgramTitleForNoInformation); 633 } 634 mImageView.setVisibility(View.GONE); 635 mBlockView.setVisibility(View.GONE); 636 mTimeView.setVisibility(View.GONE); 637 mDvrIndicator.setVisibility(View.GONE); 638 mDescriptionView.setVisibility(View.GONE); 639 mAspectRatioView.setVisibility(View.GONE); 640 mResolutionView.setVisibility(View.GONE); 641 } 642 } 643 644 private TvContentRating getProgramBlock(Program program) { 645 ParentalControlSettings parental = mTvInputManagerHelper.getParentalControlSettings(); 646 if (!parental.isParentalControlsEnabled()) { 647 return null; 648 } 649 return parental.getBlockedRating(program.getContentRatings()); 650 } 651 652 private boolean isChannelLocked(Channel channel) { 653 return mTvInputManagerHelper.getParentalControlSettings().isParentalControlsEnabled() 654 && channel.isLocked(); 655 } 656 657 private String getBlockedDescription(TvContentRating blockedRating) { 658 String name = mTvInputManagerHelper.getContentRatingsManager() 659 .getDisplayNameForRating(blockedRating); 660 if (TextUtils.isEmpty(name)) { 661 return mContext.getString(R.string.program_guide_content_locked); 662 } else { 663 return TvContentRating.UNRATED.equals(blockedRating) 664 ? mContext.getString(R.string.program_guide_content_locked_unrated) 665 : mContext.getString(R.string.program_guide_content_locked_format, name); 666 } 667 } 668 669 /** 670 * Update tv input logo. It should be called when the visible child item in ProgramGrid 671 * changed. 672 */ 673 void updateInputLogo(int lastPosition, boolean forceShow) { 674 if (mChannel == null) { 675 mInputLogoView.setVisibility(View.GONE); 676 mIsInputLogoVisible = false; 677 return; 678 } 679 680 boolean showLogo = forceShow; 681 if (!showLogo) { 682 Channel lastChannel = mProgramManager.getChannel(lastPosition); 683 if (lastChannel == null 684 || !mChannel.getInputId().equals(lastChannel.getInputId())) { 685 showLogo = true; 686 } 687 } 688 689 if (showLogo) { 690 if (!mIsInputLogoVisible) { 691 mIsInputLogoVisible = true; 692 TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(mChannel.getInputId()); 693 if (info != null) { 694 LoadTvInputLogoTask task = new LoadTvInputLogoTask( 695 itemView.getContext(), ImageCache.getInstance(), info); 696 ImageLoader.loadBitmap(createTvInputLogoLoadedCallback(info, this), task); 697 } 698 } 699 } else { 700 mInputLogoView.setVisibility(View.GONE); 701 mInputLogoView.setImageDrawable(null); 702 mIsInputLogoVisible = false; 703 } 704 } 705 706 // The return value of this method will indicate the target view is visible (true) 707 // or gone (false). 708 private boolean updateTextView(TextView textView, String text) { 709 if (!TextUtils.isEmpty(text)) { 710 textView.setVisibility(View.VISIBLE); 711 textView.setText(text); 712 return true; 713 } else { 714 textView.setVisibility(View.GONE); 715 return false; 716 } 717 } 718 719 private void updatePosterArt(@Nullable Bitmap posterArt) { 720 mImageView.setImageBitmap(posterArt); 721 mImageView.setVisibility(posterArt == null ? View.GONE : View.VISIBLE); 722 } 723 724 private void updateChannelLogo(@Nullable Bitmap logo) { 725 mChannelLogoView.setImageBitmap(logo); 726 mChannelNameView.setVisibility(View.GONE); 727 mChannelLogoView.setVisibility(View.VISIBLE); 728 } 729 730 private void updateInputLogoInternal(@NonNull Bitmap tvInputLogo) { 731 if (!mIsInputLogoVisible) { 732 return; 733 } 734 mInputLogoView.setImageBitmap(tvInputLogo); 735 mInputLogoView.setVisibility(View.VISIBLE); 736 } 737 738 private void updateCriticScoreView(ProgramRowViewHolder holder, final long programId, 739 CriticScore criticScore, View view) { 740 TextView criticScoreSource = (TextView) view.findViewById(R.id.critic_score_source); 741 TextView criticScoreText = (TextView) view.findViewById(R.id.critic_score_score); 742 ImageView criticScoreLogo = (ImageView) view.findViewById(R.id.critic_score_logo); 743 744 //set the appropriate information in the views 745 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 746 criticScoreSource.setText(Html.fromHtml(criticScore.source, 747 Html.FROM_HTML_MODE_LEGACY)); 748 } else { 749 criticScoreSource.setText(Html.fromHtml(criticScore.source)); 750 } 751 criticScoreText.setText(criticScore.score); 752 criticScoreSource.setVisibility(View.VISIBLE); 753 criticScoreText.setVisibility(View.VISIBLE); 754 ImageLoader.loadBitmap(mContext, criticScore.logoUrl, 755 createCriticScoreLogoCallback(holder, programId, criticScoreLogo)); 756 } 757 758 private void onHorizontalScrolled() { 759 if (mDetailInAnimator != null) { 760 mHandler.removeCallbacks(mDetailInStarter); 761 mHandler.postDelayed(mDetailInStarter, mAnimationDuration); 762 } 763 } 764 } 765 766 private static ImageLoaderCallback<ProgramRowViewHolder> createCriticScoreLogoCallback( 767 ProgramRowViewHolder holder, final long programId, ImageView logoView) { 768 return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { 769 @Override 770 public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logoImage) { 771 if (logoImage == null || holder.mSelectedEntry == null 772 || holder.mSelectedEntry.program == null 773 || holder.mSelectedEntry.program.getId() != programId) { 774 logoView.setVisibility(View.GONE); 775 } else { 776 logoView.setImageBitmap(logoImage); 777 logoView.setVisibility(View.VISIBLE); 778 } 779 } 780 }; 781 } 782 783 private static ImageLoaderCallback<ProgramRowViewHolder> createProgramPosterArtCallback( 784 ProgramRowViewHolder holder, final Program program) { 785 return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { 786 @Override 787 public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap posterArt) { 788 if (posterArt == null || holder.mSelectedEntry == null 789 || holder.mSelectedEntry.program == null) { 790 return; 791 } 792 String posterArtUri = holder.mSelectedEntry.program.getPosterArtUri(); 793 if (posterArtUri == null || !posterArtUri.equals(program.getPosterArtUri())) { 794 return; 795 } 796 holder.updatePosterArt(posterArt); 797 } 798 }; 799 } 800 801 private static ImageLoaderCallback<ProgramRowViewHolder> createChannelLogoLoadedCallback( 802 ProgramRowViewHolder holder, final long channelId) { 803 return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { 804 @Override 805 public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) { 806 if (logo == null || holder.mChannel == null 807 || holder.mChannel.getId() != channelId) { 808 return; 809 } 810 holder.updateChannelLogo(logo); 811 } 812 }; 813 } 814 815 private static ImageLoaderCallback<ProgramRowViewHolder> createTvInputLogoLoadedCallback( 816 final TvInputInfo info, ProgramRowViewHolder holder) { 817 return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { 818 @Override 819 public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) { 820 if (logo != null && holder.mChannel != null && info.getId() 821 .equals(holder.mChannel.getInputId())) { 822 holder.updateInputLogoInternal(logo); 823 } 824 } 825 }; 826 } 827} 828