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