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