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.AnimatorInflater; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.animation.PropertyValuesHolder; 25 import android.content.Context; 26 import android.content.SharedPreferences; 27 import android.content.res.Resources; 28 import android.graphics.Point; 29 import android.os.Handler; 30 import android.os.Message; 31 import android.os.SystemClock; 32 import android.preference.PreferenceManager; 33 import android.support.annotation.NonNull; 34 import android.support.annotation.Nullable; 35 import android.support.v17.leanback.widget.OnChildSelectedListener; 36 import android.support.v17.leanback.widget.SearchOrbView; 37 import android.support.v17.leanback.widget.VerticalGridView; 38 import android.support.v7.widget.RecyclerView; 39 import android.util.Log; 40 import android.view.View; 41 import android.view.View.MeasureSpec; 42 import android.view.ViewGroup; 43 import android.view.ViewGroup.LayoutParams; 44 import android.view.ViewTreeObserver; 45 import android.view.accessibility.AccessibilityManager; 46 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; 47 import com.android.tv.ChannelTuner; 48 import com.android.tv.MainActivity; 49 import com.android.tv.R; 50 import com.android.tv.TvSingletons; 51 import com.android.tv.analytics.Tracker; 52 import com.android.tv.common.WeakHandler; 53 import com.android.tv.common.util.DurationTimer; 54 import com.android.tv.data.ChannelDataManager; 55 import com.android.tv.data.GenreItems; 56 import com.android.tv.data.ProgramDataManager; 57 import com.android.tv.dvr.DvrDataManager; 58 import com.android.tv.dvr.DvrScheduleManager; 59 import com.android.tv.features.TvFeatures; 60 import com.android.tv.perf.EventNames; 61 import com.android.tv.perf.PerformanceMonitor; 62 import com.android.tv.perf.TimerEvent; 63 import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; 64 import com.android.tv.ui.ViewUtils; 65 import com.android.tv.ui.hideable.AutoHideScheduler; 66 import com.android.tv.util.TvInputManagerHelper; 67 import com.android.tv.util.Utils; 68 import com.android.tv.common.flags.BackendKnobsFlags; 69 import java.util.ArrayList; 70 import java.util.List; 71 import java.util.concurrent.TimeUnit; 72 73 /** The program guide. */ 74 public class ProgramGuide 75 implements ProgramGrid.ChildFocusListener, AccessibilityStateChangeListener { 76 private static final String TAG = "ProgramGuide"; 77 private static final boolean DEBUG = false; 78 79 // Whether we should show the guide partially. The first time the user enters the program guide, 80 // we show the grid partially together with the genre side panel on the left. Next time 81 // the program guide is entered, we recover the previous state (partial or full). 82 private static final String KEY_SHOW_GUIDE_PARTIAL = "show_guide_partial"; 83 private static final long TIME_INDICATOR_UPDATE_FREQUENCY = TimeUnit.SECONDS.toMillis(1); 84 private static final long HOUR_IN_MILLIS = TimeUnit.HOURS.toMillis(1); 85 private static final long HALF_HOUR_IN_MILLIS = HOUR_IN_MILLIS / 2; 86 // We keep the duration between mStartTime and the current time larger than this value. 87 // We clip out the first program entry in ProgramManager, if it does not have enough width. 88 // In order to prevent from clipping out the current program, this value need be larger than 89 // or equal to ProgramManager.FIRST_ENTRY_MIN_DURATION. 90 private static final long MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME = 91 ProgramManager.FIRST_ENTRY_MIN_DURATION; 92 93 private static final int MSG_PROGRAM_TABLE_FADE_IN_ANIM = 1000; 94 95 private static final String SCREEN_NAME = "EPG"; 96 97 private final MainActivity mActivity; 98 private final ProgramManager mProgramManager; 99 private final AccessibilityManager mAccessibilityManager; 100 private final ChannelTuner mChannelTuner; 101 private final Tracker mTracker; 102 private final DurationTimer mVisibleDuration = new DurationTimer(); 103 private final Runnable mPreShowRunnable; 104 private final Runnable mPostHideRunnable; 105 106 private final int mWidthPerHour; 107 private final long mViewPortMillis; 108 private final int mRowHeight; 109 private final int mDetailHeight; 110 private final int mSelectionRow; // Row that is focused 111 private final int mTableFadeAnimDuration; 112 private final int mAnimationDuration; 113 private final int mDetailPadding; 114 private final SearchOrbView mSearchOrb; 115 private int mCurrentTimeIndicatorWidth; 116 117 private final View mContainer; 118 private final View mSidePanel; 119 private final VerticalGridView mSidePanelGridView; 120 private final View mTable; 121 private final TimelineRow mTimelineRow; 122 private final ProgramGrid mGrid; 123 private final TimeListAdapter mTimeListAdapter; 124 private final View mCurrentTimeIndicator; 125 126 private final Animator mShowAnimatorFull; 127 private final Animator mShowAnimatorPartial; 128 // mHideAnimatorFull and mHideAnimatorPartial are created from the same animation xmls. 129 // When we share the one animator for two different animations, the starting value 130 // is broken, even though the starting value is not defined in XML. 131 private final Animator mHideAnimatorFull; 132 private final Animator mHideAnimatorPartial; 133 private final Animator mPartialToFullAnimator; 134 private final Animator mFullToPartialAnimator; 135 private final Animator mProgramTableFadeOutAnimator; 136 private final Animator mProgramTableFadeInAnimator; 137 138 // When the program guide is popped up, we keep the previous state of the guide. 139 private boolean mShowGuidePartial; 140 private final SharedPreferences mSharedPreference; 141 private View mSelectedRow; 142 private Animator mDetailOutAnimator; 143 private Animator mDetailInAnimator; 144 145 private long mStartUtcTime; 146 private boolean mTimelineAnimation; 147 private int mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS; 148 private boolean mIsDuringResetRowSelection; 149 private final Handler mHandler = new ProgramGuideHandler(this); 150 private boolean mActive; 151 152 private final AutoHideScheduler mAutoHideScheduler; 153 private final long mShowDurationMillis; 154 private ViewTreeObserver.OnGlobalLayoutListener mOnLayoutListenerForShow; 155 156 private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener(); 157 158 private final PerformanceMonitor mPerformanceMonitor; 159 private TimerEvent mTimerEvent; 160 161 private final Runnable mUpdateTimeIndicator = 162 new Runnable() { 163 @Override 164 public void run() { 165 positionCurrentTimeIndicator(); 166 mHandler.postAtTime( 167 this, 168 Utils.ceilTime( 169 SystemClock.uptimeMillis(), TIME_INDICATOR_UPDATE_FREQUENCY)); 170 } 171 }; 172 173 @SuppressWarnings("RestrictTo") ProgramGuide( MainActivity activity, ChannelTuner channelTuner, TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, @Nullable DvrScheduleManager dvrScheduleManager, Tracker tracker, Runnable preShowRunnable, Runnable postHideRunnable)174 public ProgramGuide( 175 MainActivity activity, 176 ChannelTuner channelTuner, 177 TvInputManagerHelper tvInputManagerHelper, 178 ChannelDataManager channelDataManager, 179 ProgramDataManager programDataManager, 180 @Nullable DvrDataManager dvrDataManager, 181 @Nullable DvrScheduleManager dvrScheduleManager, 182 Tracker tracker, 183 Runnable preShowRunnable, 184 Runnable postHideRunnable) { 185 mActivity = activity; 186 TvSingletons singletons = TvSingletons.getSingletons(mActivity); 187 mPerformanceMonitor = singletons.getPerformanceMonitor(); 188 BackendKnobsFlags backendKnobsFlags = singletons.getBackendKnobs(); 189 mProgramManager = 190 new ProgramManager( 191 tvInputManagerHelper, 192 channelDataManager, 193 programDataManager, 194 dvrDataManager, 195 dvrScheduleManager, 196 backendKnobsFlags); 197 mChannelTuner = channelTuner; 198 mTracker = tracker; 199 mPreShowRunnable = preShowRunnable; 200 mPostHideRunnable = postHideRunnable; 201 202 Resources res = activity.getResources(); 203 204 mWidthPerHour = res.getDimensionPixelSize(R.dimen.program_guide_table_width_per_hour); 205 GuideUtils.setWidthPerHour(mWidthPerHour); 206 207 Point displaySize = new Point(); 208 mActivity.getWindowManager().getDefaultDisplay().getSize(displaySize); 209 int gridWidth = 210 displaySize.x 211 - res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start) 212 - res.getDimensionPixelSize( 213 R.dimen.program_guide_table_header_column_width); 214 mViewPortMillis = (gridWidth * HOUR_IN_MILLIS) / mWidthPerHour; 215 216 mRowHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_item_row_height); 217 mDetailHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_height); 218 mSelectionRow = res.getInteger(R.integer.program_guide_selection_row); 219 mTableFadeAnimDuration = 220 res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration); 221 mShowDurationMillis = res.getInteger(R.integer.program_guide_show_duration); 222 mAnimationDuration = 223 res.getInteger(R.integer.program_guide_table_detail_toggle_anim_duration); 224 mDetailPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_detail_padding); 225 226 mContainer = mActivity.findViewById(R.id.program_guide); 227 ViewTreeObserver.OnGlobalFocusChangeListener globalFocusChangeListener = 228 new GlobalFocusChangeListener(); 229 mContainer.getViewTreeObserver().addOnGlobalFocusChangeListener(globalFocusChangeListener); 230 231 GenreListAdapter genreListAdapter = new GenreListAdapter(mActivity, mProgramManager, this); 232 mSidePanel = mContainer.findViewById(R.id.program_guide_side_panel); 233 mSidePanelGridView = 234 (VerticalGridView) mContainer.findViewById(R.id.program_guide_side_panel_grid_view); 235 mSidePanelGridView 236 .getRecycledViewPool() 237 .setMaxRecycledViews( 238 R.layout.program_guide_side_panel_row, 239 res.getInteger(R.integer.max_recycled_view_pool_epg_side_panel_row)); 240 mSidePanelGridView.setAdapter(genreListAdapter); 241 mSidePanelGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 242 mSidePanelGridView.setWindowAlignmentOffset( 243 mActivity 244 .getResources() 245 .getDimensionPixelOffset(R.dimen.program_guide_side_panel_alignment_y)); 246 mSidePanelGridView.setWindowAlignmentOffsetPercent( 247 VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 248 249 if (TvFeatures.EPG_SEARCH.isEnabled(mActivity)) { 250 mSearchOrb = 251 (SearchOrbView) 252 mContainer.findViewById(R.id.program_guide_side_panel_search_orb); 253 mSearchOrb.setVisibility(View.VISIBLE); 254 255 mSearchOrb.setOnOrbClickedListener( 256 new View.OnClickListener() { 257 @Override 258 public void onClick(View view) { 259 hide(); 260 mActivity.showProgramGuideSearchFragment(); 261 } 262 }); 263 mSidePanelGridView.setOnChildSelectedListener( 264 new android.support.v17.leanback.widget.OnChildSelectedListener() { 265 @Override 266 public void onChildSelected(ViewGroup viewGroup, View view, int i, long l) { 267 mSearchOrb.animate().alpha(i == 0 ? 1.0f : 0.0f); 268 } 269 }); 270 } else { 271 mSearchOrb = null; 272 } 273 274 mTable = mContainer.findViewById(R.id.program_guide_table); 275 276 mTimelineRow = (TimelineRow) mTable.findViewById(R.id.time_row); 277 mTimeListAdapter = new TimeListAdapter(res); 278 mTimelineRow 279 .getRecycledViewPool() 280 .setMaxRecycledViews( 281 R.layout.program_guide_table_header_row_item, 282 res.getInteger(R.integer.max_recycled_view_pool_epg_header_row_item)); 283 mTimelineRow.setAdapter(mTimeListAdapter); 284 285 ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity, this); 286 programTableAdapter.registerAdapterDataObserver( 287 new RecyclerView.AdapterDataObserver() { 288 @Override 289 public void onChanged() { 290 // It is usually called when Genre is changed. 291 // Reset selection of ProgramGrid 292 resetRowSelection(); 293 updateGuidePosition(); 294 } 295 }); 296 297 mGrid = (ProgramGrid) mTable.findViewById(R.id.grid); 298 mGrid.initialize(mProgramManager); 299 mGrid.getRecycledViewPool() 300 .setMaxRecycledViews( 301 R.layout.program_guide_table_row, 302 res.getInteger(R.integer.max_recycled_view_pool_epg_table_row)); 303 mGrid.setAdapter(programTableAdapter); 304 305 mGrid.setChildFocusListener(this); 306 mGrid.setOnChildSelectedListener( 307 new OnChildSelectedListener() { 308 @Override 309 public void onChildSelected( 310 ViewGroup parent, View view, int position, long id) { 311 if (mIsDuringResetRowSelection) { 312 // Ignore if it's during the first resetRowSelection, because 313 // onChildSelected 314 // will be called again when rows are bound to the program table. if 315 // selectRow 316 // is called here, mSelectedRow is set and the second selectRow call 317 // doesn't 318 // work as intended. 319 mIsDuringResetRowSelection = false; 320 return; 321 } 322 selectRow(view); 323 } 324 }); 325 mGrid.setFocusScrollStrategy(ProgramGrid.FOCUS_SCROLL_ALIGNED); 326 mGrid.setWindowAlignmentOffset(mSelectionRow * mRowHeight); 327 mGrid.setWindowAlignmentOffsetPercent(ProgramGrid.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 328 mGrid.setItemAlignmentOffset(0); 329 mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 330 331 mGrid.addOnScrollListener( 332 new RecyclerView.OnScrollListener() { 333 @Override 334 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 335 if (DEBUG) { 336 Log.d(TAG, "ProgramGrid onScrollStateChanged. newState=" + newState); 337 } 338 if (newState == RecyclerView.SCROLL_STATE_SETTLING) { 339 mPerformanceMonitor.startJankRecorder( 340 EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY); 341 } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { 342 mPerformanceMonitor.stopJankRecorder( 343 EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY); 344 } 345 } 346 }); 347 348 RecyclerView.OnScrollListener onScrollListener = 349 new RecyclerView.OnScrollListener() { 350 @Override 351 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 352 onHorizontalScrolled(dx); 353 } 354 355 @Override 356 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 357 if (DEBUG) { 358 Log.d(TAG, "TimelineRow onScrollStateChanged. newState=" + newState); 359 } 360 if (newState == RecyclerView.SCROLL_STATE_SETTLING) { 361 mPerformanceMonitor.startJankRecorder( 362 EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY); 363 } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { 364 mPerformanceMonitor.stopJankRecorder( 365 EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY); 366 } 367 } 368 }; 369 mTimelineRow.addOnScrollListener(onScrollListener); 370 371 mCurrentTimeIndicator = mTable.findViewById(R.id.current_time_indicator); 372 373 mShowAnimatorFull = 374 createAnimator( 375 R.animator.program_guide_side_panel_enter_full, 376 0, 377 R.animator.program_guide_table_enter_full); 378 mShowAnimatorFull.addListener( 379 new AnimatorListenerAdapter() { 380 @Override 381 public void onAnimationEnd(Animator animation) { 382 if (mTimerEvent != null) { 383 mPerformanceMonitor.stopTimer( 384 mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW); 385 mTimerEvent = null; 386 } 387 mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW); 388 } 389 }); 390 391 mShowAnimatorPartial = 392 createAnimator( 393 R.animator.program_guide_side_panel_enter_partial, 394 0, 395 R.animator.program_guide_table_enter_partial); 396 mShowAnimatorPartial.addListener( 397 new AnimatorListenerAdapter() { 398 @Override 399 public void onAnimationStart(Animator animation) { 400 mSidePanelGridView.setVisibility(View.VISIBLE); 401 mSidePanelGridView.setAlpha(1.0f); 402 } 403 404 @Override 405 public void onAnimationEnd(Animator animation) { 406 if (mTimerEvent != null) { 407 mPerformanceMonitor.stopTimer( 408 mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW); 409 mTimerEvent = null; 410 } 411 mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW); 412 } 413 }); 414 415 mHideAnimatorFull = 416 createAnimator( 417 R.animator.program_guide_side_panel_exit, 418 0, 419 R.animator.program_guide_table_exit); 420 mHideAnimatorFull.addListener( 421 new AnimatorListenerAdapter() { 422 @Override 423 public void onAnimationStart(Animator animation) { 424 mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE); 425 } 426 427 @Override 428 public void onAnimationEnd(Animator animation) { 429 mContainer.setVisibility(View.GONE); 430 } 431 }); 432 mHideAnimatorPartial = 433 createAnimator( 434 R.animator.program_guide_side_panel_exit, 435 0, 436 R.animator.program_guide_table_exit); 437 mHideAnimatorPartial.addListener( 438 new AnimatorListenerAdapter() { 439 @Override 440 public void onAnimationStart(Animator animation) { 441 mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE); 442 } 443 444 @Override 445 public void onAnimationEnd(Animator animation) { 446 mContainer.setVisibility(View.GONE); 447 } 448 }); 449 450 mPartialToFullAnimator = 451 createAnimator( 452 R.animator.program_guide_side_panel_hide, 453 R.animator.program_guide_side_panel_grid_fade_out, 454 R.animator.program_guide_table_partial_to_full); 455 mFullToPartialAnimator = 456 createAnimator( 457 R.animator.program_guide_side_panel_reveal, 458 R.animator.program_guide_side_panel_grid_fade_in, 459 R.animator.program_guide_table_full_to_partial); 460 461 mProgramTableFadeOutAnimator = 462 AnimatorInflater.loadAnimator(mActivity, R.animator.program_guide_table_fade_out); 463 mProgramTableFadeOutAnimator.setTarget(mTable); 464 mProgramTableFadeOutAnimator.addListener( 465 new HardwareLayerAnimatorListenerAdapter(mTable) { 466 @Override 467 public void onAnimationEnd(Animator animation) { 468 super.onAnimationEnd(animation); 469 470 if (!isActive()) { 471 return; 472 } 473 mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId); 474 resetTimelineScroll(); 475 if (!mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) { 476 mHandler.sendEmptyMessage(MSG_PROGRAM_TABLE_FADE_IN_ANIM); 477 } 478 } 479 }); 480 mProgramTableFadeInAnimator = 481 AnimatorInflater.loadAnimator(mActivity, R.animator.program_guide_table_fade_in); 482 mProgramTableFadeInAnimator.setTarget(mTable); 483 mProgramTableFadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable)); 484 mSharedPreference = PreferenceManager.getDefaultSharedPreferences(mActivity); 485 mAccessibilityManager = 486 (AccessibilityManager) mActivity.getSystemService(Context.ACCESSIBILITY_SERVICE); 487 mShowGuidePartial = 488 mAccessibilityManager.isEnabled() 489 || mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true); 490 mAutoHideScheduler = new AutoHideScheduler(activity, this::hide); 491 } 492 493 @Override onRequestChildFocus(View oldFocus, View newFocus)494 public void onRequestChildFocus(View oldFocus, View newFocus) { 495 if (oldFocus != null && newFocus != null) { 496 int selectionRowOffset = mSelectionRow * mRowHeight; 497 if (oldFocus.getTop() < newFocus.getTop()) { 498 // Selection moves downwards 499 // Adjust scroll offset to be at the bottom of the target row and to expand up. This 500 // will set the scroll target to be one row height up from its current position. 501 mGrid.setWindowAlignmentOffset(selectionRowOffset + mRowHeight + mDetailHeight); 502 mGrid.setItemAlignmentOffsetPercent(100); 503 } else if (oldFocus.getTop() > newFocus.getTop()) { 504 // Selection moves upwards 505 // Adjust scroll offset to be at the top of the target row and to expand down. This 506 // will set the scroll target to be one row height down from its current position. 507 mGrid.setWindowAlignmentOffset(selectionRowOffset); 508 mGrid.setItemAlignmentOffsetPercent(0); 509 } 510 } 511 } 512 513 /** 514 * Show the program guide. This reveals the side panel, and the program guide table is shown 515 * partially. 516 * 517 * <p>Note: the animation which starts together with ProgramGuide showing animation needs to be 518 * initiated in {@code runnableAfterAnimatorReady}. If the animation starts together with 519 * show(), the animation may drop some frames. 520 */ show(final Runnable runnableAfterAnimatorReady)521 public void show(final Runnable runnableAfterAnimatorReady) { 522 if (mContainer.getVisibility() == View.VISIBLE) { 523 return; 524 } 525 mTimerEvent = mPerformanceMonitor.startTimer(); 526 mPerformanceMonitor.startJankRecorder(EventNames.PROGRAM_GUIDE_SHOW); 527 mTracker.sendShowEpg(); 528 mTracker.sendScreenView(SCREEN_NAME); 529 if (mPreShowRunnable != null) { 530 mPreShowRunnable.run(); 531 } 532 mVisibleDuration.start(); 533 534 mProgramManager.programGuideVisibilityChanged(true); 535 mStartUtcTime = 536 Utils.floorTime( 537 System.currentTimeMillis() - MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME, 538 HALF_HOUR_IN_MILLIS); 539 mProgramManager.updateInitialTimeRange(mStartUtcTime, mStartUtcTime + mViewPortMillis); 540 mProgramManager.addListener(mProgramManagerListener); 541 mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS; 542 mTimeListAdapter.update(mStartUtcTime); 543 mTimelineRow.resetScroll(); 544 545 mContainer.setVisibility(View.VISIBLE); 546 mActive = true; 547 if (!mShowGuidePartial) { 548 mTable.requestFocus(); 549 } 550 positionCurrentTimeIndicator(); 551 mSidePanelGridView.setSelectedPosition(0); 552 if (DEBUG) { 553 Log.d(TAG, "show()"); 554 } 555 mOnLayoutListenerForShow = 556 new ViewTreeObserver.OnGlobalLayoutListener() { 557 @Override 558 public void onGlobalLayout() { 559 mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); 560 mTable.setLayerType(View.LAYER_TYPE_HARDWARE, null); 561 mSidePanelGridView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 562 mTable.buildLayer(); 563 mSidePanelGridView.buildLayer(); 564 mOnLayoutListenerForShow = null; 565 mTimelineAnimation = true; 566 // Make sure that time indicator update starts after animation is finished. 567 startCurrentTimeIndicator(TIME_INDICATOR_UPDATE_FREQUENCY); 568 if (DEBUG) { 569 mContainer 570 .getViewTreeObserver() 571 .addOnDrawListener( 572 new ViewTreeObserver.OnDrawListener() { 573 long time = System.currentTimeMillis(); 574 int count = 0; 575 576 @Override 577 public void onDraw() { 578 long curtime = System.currentTimeMillis(); 579 Log.d( 580 TAG, 581 "onDraw " 582 + count++ 583 + " " 584 + (curtime - time) 585 + "ms"); 586 time = curtime; 587 if (count > 10) { 588 mContainer 589 .getViewTreeObserver() 590 .removeOnDrawListener(this); 591 } 592 } 593 }); 594 } 595 updateGuidePosition(); 596 runnableAfterAnimatorReady.run(); 597 if (mShowGuidePartial) { 598 mShowAnimatorPartial.start(); 599 } else { 600 mShowAnimatorFull.start(); 601 } 602 } 603 }; 604 mContainer.getViewTreeObserver().addOnGlobalLayoutListener(mOnLayoutListenerForShow); 605 scheduleHide(); 606 } 607 608 /** Hide the program guide. */ hide()609 public void hide() { 610 if (!isActive()) { 611 return; 612 } 613 if (mOnLayoutListenerForShow != null) { 614 mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(mOnLayoutListenerForShow); 615 mOnLayoutListenerForShow = null; 616 } 617 mTracker.sendHideEpg(mVisibleDuration.reset()); 618 cancelHide(); 619 mProgramManager.programGuideVisibilityChanged(false); 620 mProgramManager.removeListener(mProgramManagerListener); 621 mActive = false; 622 if (!mShowGuidePartial) { 623 mHideAnimatorFull.start(); 624 } else { 625 mHideAnimatorPartial.start(); 626 } 627 628 // Clears fade-out/in animation for genre change 629 if (mProgramTableFadeOutAnimator.isRunning()) { 630 mProgramTableFadeOutAnimator.cancel(); 631 } 632 if (mProgramTableFadeInAnimator.isRunning()) { 633 mProgramTableFadeInAnimator.cancel(); 634 } 635 mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM); 636 mTable.setAlpha(1.0f); 637 638 mTimelineAnimation = false; 639 stopCurrentTimeIndicator(); 640 if (mPostHideRunnable != null) { 641 mPostHideRunnable.run(); 642 } 643 } 644 645 /** Schedules hiding the program guide. */ scheduleHide()646 public void scheduleHide() { 647 mAutoHideScheduler.schedule(mShowDurationMillis); 648 } 649 650 /** Cancels hiding the program guide. */ cancelHide()651 public void cancelHide() { 652 mAutoHideScheduler.cancel(); 653 } 654 655 /** Process the {@code KEYCODE_BACK} key event. */ onBackPressed()656 public void onBackPressed() { 657 hide(); 658 } 659 660 /** Returns {@code true} if the program guide should process the input events. */ isActive()661 public boolean isActive() { 662 return mActive; 663 } 664 665 /** 666 * Returns {@code true} if the program guide is shown, i.e. showing animation is done and hiding 667 * animation is not started yet. 668 */ isRunningAnimation()669 public boolean isRunningAnimation() { 670 return mShowAnimatorPartial.isStarted() 671 || mShowAnimatorFull.isStarted() 672 || mHideAnimatorPartial.isStarted() 673 || mHideAnimatorFull.isStarted(); 674 } 675 676 /** Returns if program table is in full screen mode. * */ isFull()677 boolean isFull() { 678 return !mShowGuidePartial; 679 } 680 681 /** Requests change genre to {@code genreId}. */ requestGenreChange(int genreId)682 void requestGenreChange(int genreId) { 683 if (mLastRequestedGenreId == genreId) { 684 // When Recycler.onLayout() removes its children to recycle, 685 // View tries to find next focus candidate immediately 686 // so GenreListAdapter can take focus back while it's hiding. 687 // Returns early here to prevent re-entrance. 688 return; 689 } 690 mLastRequestedGenreId = genreId; 691 if (mProgramTableFadeOutAnimator.isStarted()) { 692 // When requestGenreChange is called repeatedly in short time, we keep the fade-out 693 // state for mTableFadeAnimDuration from now. Without it, we'll see blinks. 694 mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM); 695 mHandler.sendEmptyMessageDelayed( 696 MSG_PROGRAM_TABLE_FADE_IN_ANIM, mTableFadeAnimDuration); 697 return; 698 } 699 if (mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) { 700 mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId); 701 mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM); 702 mHandler.sendEmptyMessageDelayed( 703 MSG_PROGRAM_TABLE_FADE_IN_ANIM, mTableFadeAnimDuration); 704 return; 705 } 706 if (mProgramTableFadeInAnimator.isStarted()) { 707 mProgramTableFadeInAnimator.cancel(); 708 } 709 710 mProgramTableFadeOutAnimator.start(); 711 } 712 713 /** Returns the scroll offset of the time line row in pixels. */ getTimelineRowScrollOffset()714 int getTimelineRowScrollOffset() { 715 return mTimelineRow.getScrollOffset(); 716 } 717 718 /** Returns the program grid view that hold all component views. */ getProgramGrid()719 ProgramGrid getProgramGrid() { 720 return mGrid; 721 } 722 723 /** Returns if Accessibility is enabled. */ isAccessibilityEnabled()724 boolean isAccessibilityEnabled() { 725 return mAccessibilityManager.isEnabled(); 726 } 727 728 /** Gets {@link VerticalGridView} for "genre select" side panel. */ getSidePanel()729 VerticalGridView getSidePanel() { 730 return mSidePanelGridView; 731 } 732 733 /** Returns the program manager the program guide is using to provide program information. */ getProgramManager()734 ProgramManager getProgramManager() { 735 return mProgramManager; 736 } 737 updateGuidePosition()738 private void updateGuidePosition() { 739 // Align EPG at vertical center, if EPG table height is less than the screen size. 740 Resources res = mActivity.getResources(); 741 int screenHeight = mContainer.getHeight(); 742 if (screenHeight <= 0) { 743 // mContainer is not initialized yet. 744 return; 745 } 746 int startPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start); 747 int topPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_top); 748 int bottomPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom); 749 int tableHeight = 750 res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height) 751 + mDetailHeight 752 + mRowHeight * mGrid.getAdapter().getItemCount() 753 + topPadding 754 + bottomPadding; 755 if (tableHeight > screenHeight) { 756 // EPG height is longer that the screen height. 757 mTable.setPaddingRelative(startPadding, topPadding, 0, 0); 758 LayoutParams layoutParams = mTable.getLayoutParams(); 759 layoutParams.height = LayoutParams.WRAP_CONTENT; 760 mTable.setLayoutParams(layoutParams); 761 } else { 762 mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding); 763 LayoutParams layoutParams = mTable.getLayoutParams(); 764 layoutParams.height = tableHeight; 765 mTable.setLayoutParams(layoutParams); 766 } 767 } 768 createAnimator( int sidePanelAnimResId, int sidePanelGridAnimResId, int tableAnimResId)769 private Animator createAnimator( 770 int sidePanelAnimResId, int sidePanelGridAnimResId, int tableAnimResId) { 771 List<Animator> animatorList = new ArrayList<>(); 772 773 Animator sidePanelAnimator = AnimatorInflater.loadAnimator(mActivity, sidePanelAnimResId); 774 sidePanelAnimator.setTarget(mSidePanel); 775 animatorList.add(sidePanelAnimator); 776 777 if (sidePanelGridAnimResId != 0) { 778 Animator sidePanelGridAnimator = 779 AnimatorInflater.loadAnimator(mActivity, sidePanelGridAnimResId); 780 sidePanelGridAnimator.setTarget(mSidePanelGridView); 781 sidePanelGridAnimator.addListener( 782 new HardwareLayerAnimatorListenerAdapter(mSidePanelGridView)); 783 animatorList.add(sidePanelGridAnimator); 784 } 785 Animator tableAnimator = AnimatorInflater.loadAnimator(mActivity, tableAnimResId); 786 tableAnimator.setTarget(mTable); 787 tableAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable)); 788 animatorList.add(tableAnimator); 789 790 AnimatorSet set = new AnimatorSet(); 791 set.playTogether(animatorList); 792 return set; 793 } 794 startFull()795 private void startFull() { 796 if (!mShowGuidePartial) { 797 return; 798 } 799 mShowGuidePartial = false; 800 mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply(); 801 mPartialToFullAnimator.start(); 802 } 803 startPartial()804 private void startPartial() { 805 if (mShowGuidePartial) { 806 return; 807 } 808 mShowGuidePartial = true; 809 mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply(); 810 mFullToPartialAnimator.start(); 811 } 812 startCurrentTimeIndicator(long initialDelay)813 private void startCurrentTimeIndicator(long initialDelay) { 814 mHandler.postDelayed(mUpdateTimeIndicator, initialDelay); 815 } 816 stopCurrentTimeIndicator()817 private void stopCurrentTimeIndicator() { 818 mHandler.removeCallbacks(mUpdateTimeIndicator); 819 } 820 positionCurrentTimeIndicator()821 private void positionCurrentTimeIndicator() { 822 int offset = 823 GuideUtils.convertMillisToPixel(mStartUtcTime, System.currentTimeMillis()) 824 - mTimelineRow.getScrollOffset(); 825 if (offset < 0) { 826 mCurrentTimeIndicator.setVisibility(View.GONE); 827 } else { 828 if (mCurrentTimeIndicatorWidth == 0) { 829 mCurrentTimeIndicator.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 830 mCurrentTimeIndicatorWidth = mCurrentTimeIndicator.getMeasuredWidth(); 831 } 832 mCurrentTimeIndicator.setPaddingRelative( 833 offset - mCurrentTimeIndicatorWidth / 2, 0, 0, 0); 834 mCurrentTimeIndicator.setVisibility(View.VISIBLE); 835 } 836 } 837 resetTimelineScroll()838 private void resetTimelineScroll() { 839 if (mProgramManager.getFromUtcMillis() != mStartUtcTime) { 840 boolean timelineAnimation = mTimelineAnimation; 841 mTimelineAnimation = false; 842 // mProgramManagerListener.onTimeRangeUpdated() will be called by shiftTime(). 843 mProgramManager.shiftTime(mStartUtcTime - mProgramManager.getFromUtcMillis()); 844 mTimelineAnimation = timelineAnimation; 845 } 846 } 847 onHorizontalScrolled(int dx)848 private void onHorizontalScrolled(int dx) { 849 if (DEBUG) Log.d(TAG, "onHorizontalScrolled(dx=" + dx + ")"); 850 positionCurrentTimeIndicator(); 851 for (int i = 0, n = mGrid.getChildCount(); i < n; ++i) { 852 mGrid.getChildAt(i).findViewById(R.id.row).scrollBy(dx, 0); 853 } 854 } 855 resetRowSelection()856 private void resetRowSelection() { 857 if (mDetailOutAnimator != null) { 858 mDetailOutAnimator.end(); 859 } 860 if (mDetailInAnimator != null) { 861 mDetailInAnimator.cancel(); 862 } 863 mSelectedRow = null; 864 mIsDuringResetRowSelection = true; 865 mGrid.setSelectedPosition( 866 Math.max(mProgramManager.getChannelIndex(mChannelTuner.getCurrentChannel()), 0)); 867 mGrid.resetFocusState(); 868 mGrid.onItemSelectionReset(); 869 mIsDuringResetRowSelection = false; 870 } 871 selectRow(View row)872 private void selectRow(View row) { 873 if (row == null || row == mSelectedRow) { 874 return; 875 } 876 if (mSelectedRow == null 877 || mGrid.getChildAdapterPosition(mSelectedRow) == RecyclerView.NO_POSITION) { 878 if (mSelectedRow != null) { 879 View oldDetailView = mSelectedRow.findViewById(R.id.detail); 880 oldDetailView.setVisibility(View.GONE); 881 } 882 View detailView = row.findViewById(R.id.detail); 883 detailView.findViewById(R.id.detail_content_full).setAlpha(1); 884 detailView.findViewById(R.id.detail_content_full).setTranslationY(0); 885 ViewUtils.setLayoutHeight(detailView, mDetailHeight); 886 detailView.setVisibility(View.VISIBLE); 887 888 final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row); 889 programRow.post(programRow::focusCurrentProgram); 890 } else { 891 animateRowChange(mSelectedRow, row); 892 } 893 mSelectedRow = row; 894 } 895 animateRowChange(View outRow, View inRow)896 private void animateRowChange(View outRow, View inRow) { 897 if (mDetailOutAnimator != null) { 898 mDetailOutAnimator.end(); 899 } 900 if (mDetailInAnimator != null) { 901 mDetailInAnimator.cancel(); 902 } 903 904 int operationDirection = mGrid.getLastUpDownDirection(); 905 int animationPadding = 0; 906 if (operationDirection == View.FOCUS_UP) { 907 animationPadding = mDetailPadding; 908 } else if (operationDirection == View.FOCUS_DOWN) { 909 animationPadding = -mDetailPadding; 910 } 911 912 View outDetail = outRow != null ? outRow.findViewById(R.id.detail) : null; 913 if (outDetail != null && outDetail.isShown()) { 914 final View outDetailContent = outDetail.findViewById(R.id.detail_content_full); 915 916 Animator fadeOutAnimator = 917 ObjectAnimator.ofPropertyValuesHolder( 918 outDetailContent, 919 PropertyValuesHolder.ofFloat(View.ALPHA, outDetail.getAlpha(), 0f), 920 PropertyValuesHolder.ofFloat( 921 View.TRANSLATION_Y, 922 outDetailContent.getTranslationY(), 923 animationPadding)); 924 fadeOutAnimator.setStartDelay(0); 925 fadeOutAnimator.setDuration(mAnimationDuration); 926 fadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(outDetailContent)); 927 928 Animator collapseAnimator = 929 ViewUtils.createHeightAnimator( 930 outDetail, ViewUtils.getLayoutHeight(outDetail), 0); 931 collapseAnimator.setStartDelay(mAnimationDuration); 932 collapseAnimator.setDuration(mTableFadeAnimDuration); 933 collapseAnimator.addListener( 934 new AnimatorListenerAdapter() { 935 @Override 936 public void onAnimationStart(Animator animator) { 937 outDetailContent.setVisibility(View.GONE); 938 } 939 940 @Override 941 public void onAnimationEnd(Animator animator) { 942 outDetailContent.setVisibility(View.VISIBLE); 943 } 944 }); 945 946 AnimatorSet outAnimator = new AnimatorSet(); 947 outAnimator.playTogether(fadeOutAnimator, collapseAnimator); 948 outAnimator.addListener( 949 new AnimatorListenerAdapter() { 950 @Override 951 public void onAnimationEnd(Animator animator) { 952 mDetailOutAnimator = null; 953 } 954 }); 955 mDetailOutAnimator = outAnimator; 956 outAnimator.start(); 957 } 958 959 View inDetail = inRow != null ? inRow.findViewById(R.id.detail) : null; 960 if (inDetail != null) { 961 final View inDetailContent = inDetail.findViewById(R.id.detail_content_full); 962 963 Animator expandAnimator = ViewUtils.createHeightAnimator(inDetail, 0, mDetailHeight); 964 expandAnimator.setStartDelay(mAnimationDuration); 965 expandAnimator.setDuration(mTableFadeAnimDuration); 966 expandAnimator.addListener( 967 new AnimatorListenerAdapter() { 968 @Override 969 public void onAnimationStart(Animator animator) { 970 inDetailContent.setVisibility(View.GONE); 971 } 972 973 @Override 974 public void onAnimationEnd(Animator animator) { 975 inDetailContent.setVisibility(View.VISIBLE); 976 inDetailContent.setAlpha(0); 977 } 978 }); 979 Animator fadeInAnimator = 980 ObjectAnimator.ofPropertyValuesHolder( 981 inDetailContent, 982 PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), 983 PropertyValuesHolder.ofFloat( 984 View.TRANSLATION_Y, -animationPadding, 0f)); 985 fadeInAnimator.setDuration(mAnimationDuration); 986 fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent)); 987 988 AnimatorSet inAnimator = new AnimatorSet(); 989 inAnimator.playSequentially(expandAnimator, fadeInAnimator); 990 inAnimator.addListener( 991 new AnimatorListenerAdapter() { 992 @Override 993 public void onAnimationEnd(Animator animator) { 994 mDetailInAnimator = null; 995 } 996 }); 997 mDetailInAnimator = inAnimator; 998 inAnimator.start(); 999 } 1000 } 1001 1002 @Override onAccessibilityStateChanged(boolean enabled)1003 public void onAccessibilityStateChanged(boolean enabled) { 1004 mAutoHideScheduler.onAccessibilityStateChanged(enabled); 1005 } 1006 1007 private class GlobalFocusChangeListener 1008 implements ViewTreeObserver.OnGlobalFocusChangeListener { 1009 private static final int UNKNOWN = 0; 1010 private static final int SIDE_PANEL = 1; 1011 private static final int PROGRAM_TABLE = 2; 1012 private static final int CHANNEL_COLUMN = 3; 1013 1014 @Override onGlobalFocusChanged(View oldFocus, View newFocus)1015 public void onGlobalFocusChanged(View oldFocus, View newFocus) { 1016 if (DEBUG) Log.d(TAG, "onGlobalFocusChanged " + oldFocus + " -> " + newFocus); 1017 if (!isActive()) { 1018 return; 1019 } 1020 int fromLocation = getLocation(oldFocus); 1021 int toLocation = getLocation(newFocus); 1022 if (fromLocation == SIDE_PANEL && toLocation == PROGRAM_TABLE) { 1023 startFull(); 1024 } else if (fromLocation == PROGRAM_TABLE && toLocation == SIDE_PANEL) { 1025 startPartial(); 1026 } else if (fromLocation == CHANNEL_COLUMN && toLocation == PROGRAM_TABLE) { 1027 startFull(); 1028 } else if (fromLocation == PROGRAM_TABLE && toLocation == CHANNEL_COLUMN) { 1029 startPartial(); 1030 } 1031 } 1032 getLocation(View view)1033 private int getLocation(View view) { 1034 if (view == null) { 1035 return UNKNOWN; 1036 } 1037 for (Object obj = view; obj instanceof View; obj = ((View) obj).getParent()) { 1038 if (obj == mSidePanel) { 1039 return SIDE_PANEL; 1040 } else if (obj == mGrid) { 1041 if (view instanceof ProgramItemView) { 1042 return PROGRAM_TABLE; 1043 } else { 1044 return CHANNEL_COLUMN; 1045 } 1046 } 1047 } 1048 return UNKNOWN; 1049 } 1050 } 1051 1052 private class ProgramManagerListener extends ProgramManager.ListenerAdapter { 1053 @Override onTimeRangeUpdated()1054 public void onTimeRangeUpdated() { 1055 int scrollOffset = 1056 (int) (mWidthPerHour * mProgramManager.getShiftedTime() / HOUR_IN_MILLIS); 1057 if (DEBUG) { 1058 Log.d( 1059 TAG, 1060 "Horizontal scroll to " 1061 + scrollOffset 1062 + " pixels (" 1063 + mProgramManager.getShiftedTime() 1064 + " millis)"); 1065 } 1066 mTimelineRow.scrollTo(scrollOffset, mTimelineAnimation); 1067 } 1068 } 1069 1070 private static class ProgramGuideHandler extends WeakHandler<ProgramGuide> { ProgramGuideHandler(ProgramGuide ref)1071 ProgramGuideHandler(ProgramGuide ref) { 1072 super(ref); 1073 } 1074 1075 @Override handleMessage(Message msg, @NonNull ProgramGuide programGuide)1076 public void handleMessage(Message msg, @NonNull ProgramGuide programGuide) { 1077 if (msg.what == MSG_PROGRAM_TABLE_FADE_IN_ANIM) { 1078 programGuide.mProgramTableFadeInAnimator.start(); 1079 } 1080 } 1081 } 1082 } 1083