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