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