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