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