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 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