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