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