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;
46import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
47import com.android.tv.ChannelTuner;
48import com.android.tv.MainActivity;
49import com.android.tv.R;
50import com.android.tv.TvFeatures;
51import com.android.tv.analytics.Tracker;
52import com.android.tv.common.WeakHandler;
53import com.android.tv.common.util.DurationTimer;
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.ui.hideable.AutoHideScheduler;
62import com.android.tv.util.TvInputManagerHelper;
63import com.android.tv.util.Utils;
64import java.util.ArrayList;
65import java.util.List;
66import java.util.concurrent.TimeUnit;
67
68/** The program guide. */
69public 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