1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.guide;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorInflater;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.AnimatorSet;
23 import android.animation.ObjectAnimator;
24 import android.animation.PropertyValuesHolder;
25 import android.animation.ValueAnimator;
26 import android.content.SharedPreferences;
27 import android.content.res.Resources;
28 import android.graphics.Point;
29 import android.os.Handler;
30 import android.os.Message;
31 import android.os.SystemClock;
32 import android.preference.PreferenceManager;
33 import android.support.annotation.NonNull;
34 import android.support.annotation.Nullable;
35 import android.support.v17.leanback.widget.OnChildSelectedListener;
36 import android.support.v17.leanback.widget.SearchOrbView;
37 import android.support.v17.leanback.widget.VerticalGridView;
38 import android.support.v7.widget.RecyclerView;
39 import android.util.Log;
40 import android.view.View;
41 import android.view.View.MeasureSpec;
42 import android.view.ViewGroup;
43 import android.view.ViewGroup.LayoutParams;
44 import android.view.ViewTreeObserver;
45 
46 import com.android.tv.ChannelTuner;
47 import com.android.tv.Features;
48 import com.android.tv.MainActivity;
49 import com.android.tv.R;
50 import com.android.tv.analytics.DurationTimer;
51 import com.android.tv.analytics.Tracker;
52 import com.android.tv.common.WeakHandler;
53 import com.android.tv.data.ChannelDataManager;
54 import com.android.tv.data.GenreItems;
55 import com.android.tv.data.ProgramDataManager;
56 import com.android.tv.dvr.DvrDataManager;
57 import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
58 import com.android.tv.util.TvInputManagerHelper;
59 import com.android.tv.util.Utils;
60 
61 import java.util.ArrayList;
62 import java.util.List;
63 import java.util.concurrent.TimeUnit;
64 
65 /**
66  * The program guide.
67  */
68 public 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 
ProgramGuide(MainActivity activity, ChannelTuner channelTuner, TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, Tracker tracker, Runnable preShowRunnable, Runnable postHideRunnable)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 
updateGuidePosition()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
onRequestChildFocus(View oldFocus, View newFocus)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 
createAnimator(int sidePanelAnimResId, int sidePanelGridAnimResId, int tableAnimResId)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      */
isActive()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      */
show(final Runnable runnableAfterAnimatorReady)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      */
hide()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 
scheduleHide()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      */
getTimelineRowScrollOffset()590     public int getTimelineRowScrollOffset() {
591         return mTimelineRow.getScrollOffset();
592     }
593 
594     /**
595      * Cancel hiding the program guide.
596      */
cancelHide()597     public void cancelHide() {
598         mHandler.removeCallbacks(mHideRunnable);
599     }
600 
601     // Returns if program table is full screen mode.
isFull()602     private boolean isFull() {
603         return mPartialToFullAnimator.isStarted() || mTable.getTranslationX() == 0;
604     }
605 
startFull()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 
startPartial()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      */
onBackPressed()627     public void onBackPressed() {
628         hide();
629     }
630 
631     /**
632      * Gets {@link VerticalGridView} for "genre select" side panel.
633      */
getSidePanel()634     public VerticalGridView getSidePanel() {
635         return mSidePanelGridView;
636     }
637 
638     /**
639      * Requests change genre to {@code genreId}.
640      */
requestGenreChange(int genreId)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 
startCurrentTimeIndicator(long initialDelay)672     private void startCurrentTimeIndicator(long initialDelay) {
673         mHandler.postDelayed(mUpdateTimeIndicator, initialDelay);
674     }
675 
stopCurrentTimeIndicator()676     private void stopCurrentTimeIndicator() {
677         mHandler.removeCallbacks(mUpdateTimeIndicator);
678     }
679 
positionCurrentTimeIndicator()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 
resetTimelineScroll()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 
onHorizontalScrolled(int dx)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 
resetRowSelection()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 
selectRow(View row)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 
animateRowChange(View outRow, View inRow)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