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