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