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