Home | History | Annotate | Download | only in guide
      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