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