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.ui;
18 
19 import android.app.Fragment;
20 import android.app.FragmentManager;
21 import android.app.FragmentManager.OnBackStackChangedListener;
22 import android.content.Intent;
23 import android.media.tv.TvInputInfo;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.Message;
27 import android.support.annotation.IntDef;
28 import android.support.annotation.NonNull;
29 import android.support.annotation.UiThread;
30 import android.util.Log;
31 import android.view.Gravity;
32 import android.view.KeyEvent;
33 import android.view.ViewGroup;
34 
35 import com.android.tv.ApplicationSingletons;
36 import com.android.tv.ChannelTuner;
37 import com.android.tv.MainActivity;
38 import com.android.tv.MainActivity.KeyHandlerResultType;
39 import com.android.tv.R;
40 import com.android.tv.TimeShiftManager;
41 import com.android.tv.TvApplication;
42 import com.android.tv.analytics.Tracker;
43 import com.android.tv.common.WeakHandler;
44 import com.android.tv.common.feature.CommonFeatures;
45 import com.android.tv.common.ui.setup.OnActionClickListener;
46 import com.android.tv.common.ui.setup.SetupFragment;
47 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
48 import com.android.tv.data.ChannelDataManager;
49 import com.android.tv.dialog.FullscreenDialogFragment;
50 import com.android.tv.dialog.PinDialogFragment;
51 import com.android.tv.dialog.RecentlyWatchedDialogFragment;
52 import com.android.tv.dialog.SafeDismissDialogFragment;
53 import com.android.tv.dvr.DvrDataManager;
54 import com.android.tv.dvr.ui.DvrActivity;
55 import com.android.tv.dvr.ui.HalfSizedDialogFragment;
56 import com.android.tv.guide.ProgramGuide;
57 import com.android.tv.menu.Menu;
58 import com.android.tv.menu.Menu.MenuShowReason;
59 import com.android.tv.menu.MenuRowFactory;
60 import com.android.tv.menu.MenuView;
61 import com.android.tv.onboarding.NewSourcesFragment;
62 import com.android.tv.onboarding.SetupSourcesFragment;
63 import com.android.tv.search.ProgramGuideSearchFragment;
64 import com.android.tv.ui.TvTransitionManager.SceneType;
65 import com.android.tv.ui.sidepanel.SettingsFragment;
66 import com.android.tv.ui.sidepanel.SideFragmentManager;
67 import com.android.tv.ui.sidepanel.parentalcontrols.RatingsFragment;
68 import com.android.tv.util.TvInputManagerHelper;
69 
70 import java.lang.annotation.Retention;
71 import java.lang.annotation.RetentionPolicy;
72 import java.util.ArrayList;
73 import java.util.HashSet;
74 import java.util.LinkedList;
75 import java.util.List;
76 import java.util.Queue;
77 import java.util.Set;
78 
79 /**
80  * A class responsible for the life cycle and event handling of the pop-ups over TV view.
81  */
82 @UiThread
83 public class TvOverlayManager {
84     private static final String TAG = "TvOverlayManager";
85     private static final boolean DEBUG = false;
86     private static final String INTRO_TRACKER_LABEL = "Intro dialog";
87 
88     @Retention(RetentionPolicy.SOURCE)
89     @IntDef(flag = true,
90             value = {FLAG_HIDE_OVERLAYS_DEFAULT, FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION,
91                     FLAG_HIDE_OVERLAYS_KEEP_SCENE, FLAG_HIDE_OVERLAYS_KEEP_DIALOG,
92                     FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS, FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY,
93                     FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE, FLAG_HIDE_OVERLAYS_KEEP_MENU,
94                     FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT})
95     private @interface HideOverlayFlag {}
96     // FLAG_HIDE_OVERLAYs must be bitwise exclusive.
97     public static final int FLAG_HIDE_OVERLAYS_DEFAULT =                 0b000000000;
98     public static final int FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION =       0b000000010;
99     public static final int FLAG_HIDE_OVERLAYS_KEEP_SCENE =              0b000000100;
100     public static final int FLAG_HIDE_OVERLAYS_KEEP_DIALOG =             0b000001000;
101     public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS =        0b000010000;
102     public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY = 0b000100000;
103     public static final int FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE =      0b001000000;
104     public static final int FLAG_HIDE_OVERLAYS_KEEP_MENU =               0b010000000;
105     public static final int FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT =           0b100000000;
106 
107     private static final int MSG_OVERLAY_CLOSED = 1000;
108 
109     @Retention(RetentionPolicy.SOURCE)
110     @IntDef(flag = true,
111             value = {OVERLAY_TYPE_NONE, OVERLAY_TYPE_MENU, OVERLAY_TYPE_SIDE_FRAGMENT,
112                     OVERLAY_TYPE_DIALOG, OVERLAY_TYPE_GUIDE, OVERLAY_TYPE_SCENE_CHANNEL_BANNER,
113                     OVERLAY_TYPE_SCENE_INPUT_BANNER, OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH,
114                     OVERLAY_TYPE_SCENE_SELECT_INPUT, OVERLAY_TYPE_FRAGMENT})
115     private @interface TvOverlayType {}
116     // OVERLAY_TYPEs must be bitwise exclusive.
117     /**
118      * The overlay type which indicates that there are no overlays.
119      */
120     private static final int OVERLAY_TYPE_NONE =                        0b000000000;
121     /**
122      * The overlay type for menu.
123      */
124     private static final int OVERLAY_TYPE_MENU =                        0b000000001;
125     /**
126      * The overlay type for the side fragment.
127      */
128     private static final int OVERLAY_TYPE_SIDE_FRAGMENT =               0b000000010;
129     /**
130      * The overlay type for dialog fragment.
131      */
132     private static final int OVERLAY_TYPE_DIALOG =                      0b000000100;
133     /**
134      * The overlay type for program guide.
135      */
136     private static final int OVERLAY_TYPE_GUIDE =                       0b000001000;
137     /**
138      * The overlay type for channel banner.
139      */
140     private static final int OVERLAY_TYPE_SCENE_CHANNEL_BANNER =        0b000010000;
141     /**
142      * The overlay type for input banner.
143      */
144     private static final int OVERLAY_TYPE_SCENE_INPUT_BANNER =          0b000100000;
145     /**
146      * The overlay type for keypad channel switch view.
147      */
148     private static final int OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH = 0b001000000;
149     /**
150      * The overlay type for select input view.
151      */
152     private static final int OVERLAY_TYPE_SCENE_SELECT_INPUT =          0b010000000;
153     /**
154      * The overlay type for fragment other than the side fragment and dialog fragment.
155      */
156     private static final int OVERLAY_TYPE_FRAGMENT =                    0b100000000;
157     // Used for the padded print of the overlay type.
158     private static final int NUM_OVERLAY_TYPES = 9;
159 
160     private static final String FRAGMENT_TAG_SETUP_SOURCES = "tag_setup_sources";
161     private static final String FRAGMENT_TAG_NEW_SOURCES = "tag_new_sources";
162 
163     private static final Set<String> AVAILABLE_DIALOG_TAGS = new HashSet<>();
164     static {
165         AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG);
166         AVAILABLE_DIALOG_TAGS.add(PinDialogFragment.DIALOG_TAG);
167         AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG);
168         AVAILABLE_DIALOG_TAGS.add(SettingsFragment.LicenseActionItem.DIALOG_TAG);
169         AVAILABLE_DIALOG_TAGS.add(RatingsFragment.AttributionItem.DIALOG_TAG);
170         AVAILABLE_DIALOG_TAGS.add(HalfSizedDialogFragment.DIALOG_TAG);
171     }
172 
173     private final MainActivity mMainActivity;
174     private final ChannelTuner mChannelTuner;
175     private final TvTransitionManager mTransitionManager;
176     private final ChannelDataManager mChannelDataManager;
177     private final TvInputManagerHelper mInputManager;
178     private final Menu mMenu;
179     private final SideFragmentManager mSideFragmentManager;
180     private final ProgramGuide mProgramGuide;
181     private final KeypadChannelSwitchView mKeypadChannelSwitchView;
182     private final SelectInputView mSelectInputView;
183     private final ProgramGuideSearchFragment mSearchFragment;
184     private final Tracker mTracker;
185     private SafeDismissDialogFragment mCurrentDialog;
186     private boolean mSetupFragmentActive;
187     private boolean mNewSourcesFragmentActive;
188     private final Handler mHandler = new TvOverlayHandler(this);
189 
190     private @TvOverlayType int mOpenedOverlays;
191 
192     private final List<Runnable> mPendingActions = new ArrayList<>();
193     private final Queue<PendingDialogAction> mPendingDialogActionQueue = new LinkedList<>();
194 
195     private OnBackStackChangedListener mOnBackStackChangedListener;
196 
TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner, TunableTvView tvView, KeypadChannelSwitchView keypadChannelSwitchView, ChannelBannerView channelBannerView, InputBannerView inputBannerView, SelectInputView selectInputView, ViewGroup sceneContainer, ProgramGuideSearchFragment searchFragment)197     public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner,
198             TunableTvView tvView, KeypadChannelSwitchView keypadChannelSwitchView,
199             ChannelBannerView channelBannerView, InputBannerView inputBannerView,
200             SelectInputView selectInputView, ViewGroup sceneContainer,
201             ProgramGuideSearchFragment searchFragment) {
202         mMainActivity = mainActivity;
203         mChannelTuner = channelTuner;
204         ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity);
205         mChannelDataManager = singletons.getChannelDataManager();
206         mInputManager = singletons.getTvInputManagerHelper();
207         mKeypadChannelSwitchView = keypadChannelSwitchView;
208         mSelectInputView = selectInputView;
209         mSearchFragment = searchFragment;
210         mTracker = singletons.getTracker();
211         mTransitionManager = new TvTransitionManager(mainActivity, sceneContainer,
212                 channelBannerView, inputBannerView, mKeypadChannelSwitchView, selectInputView);
213         mTransitionManager.setListener(new TvTransitionManager.Listener() {
214             @Override
215             public void onSceneChanged(int fromScene, int toScene) {
216                 // Call onOverlayOpened first so that the listener can know that a new scene
217                 // will be opened when the onOverlayClosed is called.
218                 if (toScene != TvTransitionManager.SCENE_TYPE_EMPTY) {
219                     onOverlayOpened(convertSceneToOverlayType(toScene));
220                 }
221                 if (fromScene != TvTransitionManager.SCENE_TYPE_EMPTY) {
222                     onOverlayClosed(convertSceneToOverlayType(fromScene));
223                 }
224             }
225         });
226         // Menu
227         MenuView menuView = (MenuView) mainActivity.findViewById(R.id.menu);
228         mMenu = new Menu(mainActivity, tvView, menuView, new MenuRowFactory(mainActivity, tvView),
229                 new Menu.OnMenuVisibilityChangeListener() {
230                     @Override
231                     public void onMenuVisibilityChange(boolean visible) {
232                         if (visible) {
233                             onOverlayOpened(OVERLAY_TYPE_MENU);
234                         } else {
235                             onOverlayClosed(OVERLAY_TYPE_MENU);
236                         }
237                     }
238                 });
239         mMenu.setChannelTuner(mChannelTuner);
240         // Side Fragment
241         mSideFragmentManager = new SideFragmentManager(mainActivity,
242                 new Runnable() {
243                     @Override
244                     public void run() {
245                         onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT);
246                         hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS);
247                     }
248                 },
249                 new Runnable() {
250                     @Override
251                     public void run() {
252                         mMainActivity.showChannelBannerIfHiddenBySideFragment();
253                         onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT);
254                     }
255                 });
256         // Program Guide
257         Runnable preShowRunnable = new Runnable() {
258             @Override
259             public void run() {
260                 onOverlayOpened(OVERLAY_TYPE_GUIDE);
261             }
262         };
263         Runnable postHideRunnable = new Runnable() {
264             @Override
265             public void run() {
266                 onOverlayClosed(OVERLAY_TYPE_GUIDE);
267             }
268         };
269         DvrDataManager dvrDataManager = CommonFeatures.DVR.isEnabled(mainActivity)
270                 ? singletons.getDvrDataManager() : null;
271         mProgramGuide = new ProgramGuide(mainActivity, channelTuner,
272                 singletons.getTvInputManagerHelper(), mChannelDataManager,
273                 singletons.getProgramDataManager(), dvrDataManager,
274                 singletons.getDvrScheduleManager(), singletons.getTracker(), preShowRunnable,
275                 postHideRunnable);
276         mMainActivity.addOnActionClickListener(new OnActionClickListener() {
277             @Override
278             public boolean onActionClick(String category, int id, Bundle params) {
279                 switch (category) {
280                     case SetupSourcesFragment.ACTION_CATEGORY:
281                         switch (id) {
282                             case SetupMultiPaneFragment.ACTION_DONE:
283                                 closeSetupFragment(true);
284                                 return true;
285                             case SetupSourcesFragment.ACTION_ONLINE_STORE:
286                                 mMainActivity.showMerchantCollection();
287                                 return true;
288                             case SetupSourcesFragment.ACTION_SETUP_INPUT: {
289                                 String inputId = params.getString(
290                                         SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID);
291                                 TvInputInfo input = mInputManager.getTvInputInfo(inputId);
292                                 mMainActivity.startSetupActivity(input, true);
293                                 return true;
294                             }
295                         }
296                         break;
297                     case NewSourcesFragment.ACTION_CATEOGRY:
298                         switch (id) {
299                             case NewSourcesFragment.ACTION_SETUP:
300                                 closeNewSourcesFragment(false);
301                                 showSetupFragment();
302                                 return true;
303                             case NewSourcesFragment.ACTION_SKIP:
304                                 // Don't remove the fragment because new fragment will be replaced
305                                 // with this fragment.
306                                 closeNewSourcesFragment(true);
307                                 return true;
308                         }
309                         break;
310                 }
311                 return false;
312             }
313         });
314     }
315 
316     /**
317      * A method to release all the allocated resources or unregister listeners.
318      * This is called from {@link MainActivity#onDestroy}.
319      */
release()320     public void release() {
321         mMenu.release();
322         mHandler.removeCallbacksAndMessages(null);
323     }
324 
325     /**
326      * Returns the instance of {@link Menu}.
327      */
getMenu()328     public Menu getMenu() {
329         return mMenu;
330     }
331 
332     /**
333      * Returns the instance of {@link SideFragmentManager}.
334      */
getSideFragmentManager()335     public SideFragmentManager getSideFragmentManager() {
336         return mSideFragmentManager;
337     }
338 
339     /**
340      * Returns the currently opened dialog.
341      */
getCurrentDialog()342     public SafeDismissDialogFragment getCurrentDialog() {
343         return mCurrentDialog;
344     }
345 
346     /**
347      * Checks whether the setup fragment is active or not.
348      */
isSetupFragmentActive()349     public boolean isSetupFragmentActive() {
350         // "getSetupSourcesFragment() != null" doesn't return the correct state. That's because,
351         // when we call showSetupFragment(), we need to put off showing the fragment until the side
352         // fragment is closed. Until then, getSetupSourcesFragment() returns null. So we need
353         // to keep additional variable which indicates if showSetupFragment() is called.
354         return mSetupFragmentActive;
355     }
356 
getSetupSourcesFragment()357     private Fragment getSetupSourcesFragment() {
358         return mMainActivity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG_SETUP_SOURCES);
359     }
360 
361     /**
362      * Checks whether the new sources fragment is active or not.
363      */
isNewSourcesFragmentActive()364     public boolean isNewSourcesFragmentActive() {
365         // See the comment in "isSetupFragmentActive".
366         return mNewSourcesFragmentActive;
367     }
368 
getNewSourcesFragment()369     private Fragment getNewSourcesFragment() {
370         return mMainActivity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG_NEW_SOURCES);
371     }
372 
373     /**
374      * Returns the instance of {@link ProgramGuide}.
375      */
getProgramGuide()376     public ProgramGuide getProgramGuide() {
377         return mProgramGuide;
378     }
379 
380     /**
381      * Shows the main menu.
382      */
showMenu(@enuShowReason int reason)383     public void showMenu(@MenuShowReason int reason) {
384         if (mChannelTuner != null && mChannelTuner.areAllChannelsLoaded()) {
385             mMenu.show(reason);
386         }
387     }
388 
389     /**
390      * Shows the play controller of the menu if the playback is paused.
391      */
showMenuWithTimeShiftPauseIfNeeded()392     public boolean showMenuWithTimeShiftPauseIfNeeded() {
393         if (mMainActivity.getTimeShiftManager().isPaused()) {
394             showMenu(Menu.REASON_PLAY_CONTROLS_PAUSE);
395             return true;
396         }
397         return false;
398     }
399 
400     /**
401      * Shows the given dialog.
402      */
showDialogFragment(String tag, SafeDismissDialogFragment dialog, boolean keepSidePanelHistory)403     public void showDialogFragment(String tag, SafeDismissDialogFragment dialog,
404             boolean keepSidePanelHistory) {
405         showDialogFragment(tag, dialog, keepSidePanelHistory, false);
406     }
407 
showDialogFragment(String tag, SafeDismissDialogFragment dialog, boolean keepSidePanelHistory, boolean keepProgramGuide)408     public void showDialogFragment(String tag, SafeDismissDialogFragment dialog,
409             boolean keepSidePanelHistory, boolean keepProgramGuide) {
410         int flags = FLAG_HIDE_OVERLAYS_KEEP_DIALOG;
411         if (keepSidePanelHistory) {
412             flags |= FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY;
413         }
414         if (keepProgramGuide) {
415             flags |= FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE;
416         }
417         hideOverlays(flags);
418         // A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV.
419         if (!AVAILABLE_DIALOG_TAGS.contains(tag)) {
420             return;
421         }
422 
423         // Do not open two dialogs at the same time.
424         if (mCurrentDialog != null) {
425             mPendingDialogActionQueue.offer(new PendingDialogAction(tag, dialog,
426                     keepSidePanelHistory, keepProgramGuide));
427             return;
428         }
429 
430         mCurrentDialog = dialog;
431         dialog.show(mMainActivity.getFragmentManager(), tag);
432 
433         // Calling this from SafeDismissDialogFragment.onCreated() might be late
434         // because it takes time for onCreated to be called
435         // and next key events can be handled by MainActivity, not Dialog.
436         onOverlayOpened(OVERLAY_TYPE_DIALOG);
437     }
438 
runAfterSideFragmentsAreClosed(final Runnable runnable)439     private void runAfterSideFragmentsAreClosed(final Runnable runnable) {
440         if (mSideFragmentManager.isSidePanelVisible()) {
441             // When the side panel is closing, it closes all the fragments, so the new fragment
442             // should be opened after the side fragment becomes invisible.
443             final FragmentManager manager = mMainActivity.getFragmentManager();
444             mOnBackStackChangedListener = new OnBackStackChangedListener() {
445                 @Override
446                 public void onBackStackChanged() {
447                     if (manager.getBackStackEntryCount() == 0) {
448                         manager.removeOnBackStackChangedListener(this);
449                         mOnBackStackChangedListener = null;
450                         runnable.run();
451                     }
452                 }
453             };
454             manager.addOnBackStackChangedListener(mOnBackStackChangedListener);
455         } else {
456             runnable.run();
457         }
458     }
459 
showFragment(final Fragment fragment, final String tag)460     private void showFragment(final Fragment fragment, final String tag) {
461         hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
462         onOverlayOpened(OVERLAY_TYPE_FRAGMENT);
463         runAfterSideFragmentsAreClosed(new Runnable() {
464             @Override
465             public void run() {
466                 if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")");
467                 mMainActivity.getFragmentManager().beginTransaction()
468                         .replace(R.id.fragment_container, fragment, tag).commit();
469             }
470         });
471     }
472 
closeFragment(String fragmentTagToRemove)473     private void closeFragment(String fragmentTagToRemove) {
474         if (DEBUG) Log.d(TAG, "closeFragment(" + fragmentTagToRemove + ")");
475         onOverlayClosed(OVERLAY_TYPE_FRAGMENT);
476         if (fragmentTagToRemove != null) {
477             Fragment fragmentToRemove = mMainActivity.getFragmentManager()
478                     .findFragmentByTag(fragmentTagToRemove);
479             if (fragmentToRemove == null) {
480                 // If the fragment has not been added to the fragment manager yet, just remove the
481                 // listener not to add the fragment. This is needed because the side fragment is
482                 // closed asynchronously.
483                 mMainActivity.getFragmentManager().removeOnBackStackChangedListener(
484                         mOnBackStackChangedListener);
485                 mOnBackStackChangedListener = null;
486             } else {
487                 mMainActivity.getFragmentManager().beginTransaction().remove(fragmentToRemove)
488                         .commit();
489             }
490         }
491     }
492 
493     /**
494      * Shows setup dialog.
495      */
showSetupFragment()496     public void showSetupFragment() {
497         if (DEBUG) Log.d(TAG, "showSetupFragment");
498         mSetupFragmentActive = true;
499         SetupSourcesFragment setupFragment = new SetupSourcesFragment();
500         setupFragment.enableFragmentTransition(SetupFragment.FRAGMENT_ENTER_TRANSITION
501                 | SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION
502                 | SetupFragment.FRAGMENT_REENTER_TRANSITION);
503         setupFragment.setFragmentTransition(SetupFragment.FRAGMENT_EXIT_TRANSITION, Gravity.END);
504         showFragment(setupFragment, FRAGMENT_TAG_SETUP_SOURCES);
505     }
506 
507     // Set removeFragment to false only when the new fragment is going to be shown.
closeSetupFragment(boolean removeFragment)508     private void closeSetupFragment(boolean removeFragment) {
509         if (DEBUG) Log.d(TAG, "closeSetupFragment");
510         if (!mSetupFragmentActive) {
511             return;
512         }
513         mSetupFragmentActive = false;
514         closeFragment(removeFragment ? FRAGMENT_TAG_SETUP_SOURCES : null);
515         if (mChannelDataManager.getChannelCount() == 0) {
516             if (DEBUG) Log.d(TAG, "Finishing MainActivity because there are no channels.");
517             mMainActivity.finish();
518         }
519     }
520 
521     /**
522      * Shows new sources dialog.
523      */
showNewSourcesFragment()524     public void showNewSourcesFragment() {
525         if (DEBUG) Log.d(TAG, "showNewSourcesFragment");
526         mNewSourcesFragmentActive = true;
527         showFragment(new NewSourcesFragment(), FRAGMENT_TAG_NEW_SOURCES);
528     }
529 
530     // Set removeFragment to false only when the new fragment is going to be shown.
closeNewSourcesFragment(boolean removeFragment)531     private void closeNewSourcesFragment(boolean removeFragment) {
532         if (DEBUG) Log.d(TAG, "closeNewSourcesFragment");
533         if (!mNewSourcesFragmentActive) {
534             return;
535         }
536         mNewSourcesFragmentActive = false;
537         closeFragment(removeFragment ? FRAGMENT_TAG_NEW_SOURCES : null);
538     }
539 
540     /**
541      * Shows DVR manager.
542      */
showDvrManager()543     public void showDvrManager() {
544         Intent intent = new Intent(mMainActivity, DvrActivity.class);
545         mMainActivity.startActivity(intent);
546     }
547 
548     /**
549      * Shows intro dialog.
550      */
showIntroDialog()551     public void showIntroDialog() {
552         if (DEBUG) Log.d(TAG,"showIntroDialog");
553         showDialogFragment(FullscreenDialogFragment.DIALOG_TAG,
554                 FullscreenDialogFragment.newInstance(R.layout.intro_dialog, INTRO_TRACKER_LABEL),
555                 false);
556     }
557 
558     /**
559      * Shows recently watched dialog.
560      */
showRecentlyWatchedDialog()561     public void showRecentlyWatchedDialog() {
562         showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG,
563                 new RecentlyWatchedDialogFragment(), false);
564     }
565 
566     /**
567      * Shows banner view.
568      */
showBanner()569     public void showBanner() {
570         mTransitionManager.goToChannelBannerScene();
571     }
572 
showKeypadChannelSwitch()573     public void showKeypadChannelSwitch() {
574         hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE
575                 | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
576                 | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
577                 | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
578         mTransitionManager.goToKeypadChannelSwitchScene();
579     }
580 
581     /**
582      * Shows select input view.
583      */
showSelectInputView()584     public void showSelectInputView() {
585         hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE);
586         mTransitionManager.goToSelectInputScene();
587     }
588 
589     /**
590      * Initializes animators if animators are not initialized yet.
591      */
initAnimatorIfNeeded()592     public void initAnimatorIfNeeded() {
593         mTransitionManager.initIfNeeded();
594     }
595 
596     /**
597      * It is called when a SafeDismissDialogFragment is destroyed.
598      */
onDialogDestroyed()599     public void onDialogDestroyed() {
600         mCurrentDialog = null;
601         PendingDialogAction action = mPendingDialogActionQueue.poll();
602         if (action == null) {
603             onOverlayClosed(OVERLAY_TYPE_DIALOG);
604         } else {
605             action.run();
606         }
607     }
608 
609     /**
610      * Shows the program guide.
611      */
showProgramGuide()612     public void showProgramGuide() {
613         mProgramGuide.show(new Runnable() {
614             @Override
615             public void run() {
616                 hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE);
617             }
618         });
619     }
620 
621     /**
622      * Hides all the opened overlays according to the flags.
623      */
624     // TODO: Add test for this method.
hideOverlays(@ideOverlayFlag int flags)625     public void hideOverlays(@HideOverlayFlag int flags) {
626         if (mMainActivity.needToKeepSetupScreenWhenHidingOverlay()) {
627             flags |= FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT;
628         }
629         if ((flags & FLAG_HIDE_OVERLAYS_KEEP_DIALOG) != 0) {
630             // Keeps the dialog.
631         } else {
632             if (mCurrentDialog != null) {
633                 if (mCurrentDialog instanceof PinDialogFragment) {
634                     // The result listener of PinDialogFragment could call MenuView when
635                     // the dialog is dismissed. In order not to call it, set the result listener
636                     // to null.
637                     ((PinDialogFragment) mCurrentDialog).setResultListener(null);
638                 }
639                 mCurrentDialog.dismiss();
640             }
641             mPendingDialogActionQueue.clear();
642             mCurrentDialog = null;
643         }
644         boolean withAnimation = (flags & FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION) == 0;
645 
646         if ((flags & FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT) == 0) {
647             Fragment setupSourcesFragment = getSetupSourcesFragment();
648             Fragment newSourcesFragment = getNewSourcesFragment();
649             if (mSetupFragmentActive) {
650                 if (!withAnimation && setupSourcesFragment != null) {
651                     setupSourcesFragment.setReturnTransition(null);
652                     setupSourcesFragment.setExitTransition(null);
653                 }
654                 closeSetupFragment(true);
655             }
656             if (mNewSourcesFragmentActive) {
657                 if (!withAnimation && newSourcesFragment != null) {
658                     newSourcesFragment.setReturnTransition(null);
659                     newSourcesFragment.setExitTransition(null);
660                 }
661                 closeNewSourcesFragment(true);
662             }
663         }
664 
665         if ((flags & FLAG_HIDE_OVERLAYS_KEEP_MENU) != 0) {
666             // Keeps the menu.
667         } else {
668             mMenu.hide(withAnimation);
669         }
670         if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SCENE) != 0) {
671             // Keeps the current scene.
672         } else {
673             mTransitionManager.goToEmptyScene(withAnimation);
674         }
675         if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS) != 0) {
676             // Keeps side panels.
677         } else if (mSideFragmentManager.isSidePanelVisible()) {
678             if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY) != 0) {
679                 mSideFragmentManager.hideSidePanel(withAnimation);
680             } else {
681                 mSideFragmentManager.hideAll(withAnimation);
682             }
683         }
684         if ((flags & FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE) != 0) {
685             // Keep the program guide.
686         } else {
687             mProgramGuide.hide();
688         }
689     }
690 
691     /**
692      * Returns true, if a main view needs to hide informational text. Specifically, when overlay
693      * UIs except banner is shown, the informational text needs to be hidden for clean UI.
694      */
needHideTextOnMainView()695     public boolean needHideTextOnMainView() {
696         return mSideFragmentManager.isActive()
697                 || getMenu().isActive()
698                 || mTransitionManager.isKeypadChannelSwitchActive()
699                 || mTransitionManager.isSelectInputActive()
700                 || mSetupFragmentActive
701                 || mNewSourcesFragmentActive;
702     }
703 
convertSceneToOverlayType(@ceneType int sceneType)704     @TvOverlayType private int convertSceneToOverlayType(@SceneType int sceneType) {
705         switch (sceneType) {
706             case TvTransitionManager.SCENE_TYPE_CHANNEL_BANNER:
707                 return OVERLAY_TYPE_SCENE_CHANNEL_BANNER;
708             case TvTransitionManager.SCENE_TYPE_INPUT_BANNER:
709                 return OVERLAY_TYPE_SCENE_INPUT_BANNER;
710             case TvTransitionManager.SCENE_TYPE_KEYPAD_CHANNEL_SWITCH:
711                 return OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH;
712             case TvTransitionManager.SCENE_TYPE_SELECT_INPUT:
713                 return OVERLAY_TYPE_SCENE_SELECT_INPUT;
714             case TvTransitionManager.SCENE_TYPE_EMPTY:
715             default:
716                 return OVERLAY_TYPE_NONE;
717         }
718     }
719 
onOverlayOpened(@vOverlayType int overlayType)720     private void onOverlayOpened(@TvOverlayType int overlayType) {
721         if (DEBUG) Log.d(TAG, "Overlay opened:  " + toBinaryString(overlayType));
722         mOpenedOverlays |= overlayType;
723         if (DEBUG) Log.d(TAG, "Opened overlays: " + toBinaryString(mOpenedOverlays));
724         mHandler.removeMessages(MSG_OVERLAY_CLOSED);
725         mMainActivity.updateKeyInputFocus();
726     }
727 
onOverlayClosed(@vOverlayType int overlayType)728     private void onOverlayClosed(@TvOverlayType int overlayType) {
729         if (DEBUG) Log.d(TAG, "Overlay closed:  " + toBinaryString(overlayType));
730         mOpenedOverlays &= ~overlayType;
731         if (DEBUG) Log.d(TAG, "Opened overlays: " + toBinaryString(mOpenedOverlays));
732         mHandler.removeMessages(MSG_OVERLAY_CLOSED);
733         mMainActivity.updateKeyInputFocus();
734         // Show the main menu again if there are no pop-ups or banners only.
735         // The main menu should not be shown when the activity is in paused state.
736         boolean menuAboutToShow = false;
737         if (canExecuteCloseAction()) {
738             menuAboutToShow = mMainActivity.getTimeShiftManager().isPaused();
739             mHandler.sendEmptyMessage(MSG_OVERLAY_CLOSED);
740         }
741         // Don't set screen name to main if the overlay closing is a banner
742         // or if a non banner overlay is still open
743         // or if we just opened the menu
744         if (overlayType != OVERLAY_TYPE_SCENE_CHANNEL_BANNER
745                 && overlayType != OVERLAY_TYPE_SCENE_INPUT_BANNER
746                 && isOnlyBannerOrNoneOpened()
747                 && !menuAboutToShow) {
748             mTracker.sendScreenView(MainActivity.SCREEN_NAME);
749         }
750     }
751 
toBinaryString(int value)752     private String toBinaryString(int value) {
753         return String.format("0b%" + NUM_OVERLAY_TYPES + "s", Integer.toBinaryString(value))
754                 .replace(' ', '0');
755     }
756 
canExecuteCloseAction()757     private boolean canExecuteCloseAction() {
758         return mMainActivity.isActivityResumed() && isOnlyBannerOrNoneOpened();
759     }
760 
isOnlyBannerOrNoneOpened()761     private boolean isOnlyBannerOrNoneOpened() {
762         return (mOpenedOverlays & ~OVERLAY_TYPE_SCENE_CHANNEL_BANNER
763                 & ~OVERLAY_TYPE_SCENE_INPUT_BANNER) == 0;
764     }
765 
766     /**
767      * Runs a given {@code action} after all the overlays are closed.
768      */
runAfterOverlaysAreClosed(Runnable action)769     public void runAfterOverlaysAreClosed(Runnable action) {
770         if (canExecuteCloseAction()) {
771             action.run();
772         } else {
773             mPendingActions.add(action);
774         }
775     }
776 
777     /**
778      * Handles the onUserInteraction event of the {@link MainActivity}.
779      */
onUserInteraction()780     public void onUserInteraction() {
781         if (mSideFragmentManager.isActive()) {
782             mSideFragmentManager.scheduleHideAll();
783         } else if (mMenu.isActive()) {
784             mMenu.scheduleHide();
785         } else if (mProgramGuide.isActive()) {
786             mProgramGuide.scheduleHide();
787         }
788     }
789 
790     /**
791      * Handles the onKeyDown event of the {@link MainActivity}.
792      */
onKeyDown(int keyCode, KeyEvent event)793     @KeyHandlerResultType public int onKeyDown(int keyCode, KeyEvent event) {
794         if (mCurrentDialog != null) {
795             // Consumes the keys while a Dialog is creating.
796             return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
797         }
798         // Handle media key here because it is related to the menu.
799         if (isMediaStartKey(keyCode)) {
800             // Consumes the keys which may trigger system's default music player.
801             return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
802         }
803         if (mMenu.isActive() || mSideFragmentManager.isActive() || mProgramGuide.isActive()
804                 || mSetupFragmentActive || mNewSourcesFragmentActive) {
805             return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
806         }
807         if (mTransitionManager.isKeypadChannelSwitchActive()) {
808             return mKeypadChannelSwitchView.onKeyDown(keyCode, event) ?
809                     MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED
810                     : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED;
811         }
812         if (mTransitionManager.isSelectInputActive()) {
813             return mSelectInputView.onKeyDown(keyCode, event) ?
814                     MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED
815                     : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED;
816         }
817         return MainActivity.KEY_EVENT_HANDLER_RESULT_PASSTHROUGH;
818     }
819 
820     /**
821      * Handles the onKeyUp event of the {@link MainActivity}.
822      */
onKeyUp(int keyCode, KeyEvent event)823     @KeyHandlerResultType public int onKeyUp(int keyCode, KeyEvent event) {
824         // Handle media key here because it is related to the menu.
825         if (isMediaStartKey(keyCode)) {
826             // The media key should not be passed up to the system in any cases.
827             if (mCurrentDialog != null || mProgramGuide.isActive()
828                     || mSideFragmentManager.isActive()
829                     || mSearchFragment.isVisible()
830                     || mTransitionManager.isKeypadChannelSwitchActive()
831                     || mTransitionManager.isSelectInputActive()
832                     || mSetupFragmentActive
833                     || mNewSourcesFragmentActive) {
834                 // Do not handle media key when any pop-ups which can handle keys are active.
835                 return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
836             }
837             TimeShiftManager timeShiftManager = mMainActivity.getTimeShiftManager();
838             if (!timeShiftManager.isAvailable()) {
839                 return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
840             }
841             switch (keyCode) {
842                 case KeyEvent.KEYCODE_MEDIA_PLAY:
843                     timeShiftManager.play();
844                     showMenu(Menu.REASON_PLAY_CONTROLS_PLAY);
845                     break;
846                 case KeyEvent.KEYCODE_MEDIA_PAUSE:
847                     timeShiftManager.pause();
848                     showMenu(Menu.REASON_PLAY_CONTROLS_PAUSE);
849                     break;
850                 case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
851                     timeShiftManager.togglePlayPause();
852                     showMenu(Menu.REASON_PLAY_CONTROLS_PLAY_PAUSE);
853                     break;
854                 case KeyEvent.KEYCODE_MEDIA_REWIND:
855                     timeShiftManager.rewind();
856                     showMenu(Menu.REASON_PLAY_CONTROLS_REWIND);
857                     break;
858                 case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
859                     timeShiftManager.fastForward();
860                     showMenu(Menu.REASON_PLAY_CONTROLS_FAST_FORWARD);
861                     break;
862                 case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
863                 case KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD:
864                     timeShiftManager.jumpToPrevious();
865                     showMenu(Menu.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS);
866                     break;
867                 case KeyEvent.KEYCODE_MEDIA_NEXT:
868                 case KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD:
869                     timeShiftManager.jumpToNext();
870                     showMenu(Menu.REASON_PLAY_CONTROLS_JUMP_TO_NEXT);
871                     break;
872                 default:
873                     // Does nothing.
874                     break;
875             }
876             return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
877         }
878         if (keyCode == KeyEvent.KEYCODE_I || keyCode == KeyEvent.KEYCODE_TV_INPUT) {
879             if (mTransitionManager.isSelectInputActive()) {
880                 mSelectInputView.onKeyUp(keyCode, event);
881             } else {
882                 showSelectInputView();
883             }
884             return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
885         }
886         if (mCurrentDialog != null) {
887             // Consumes the keys while a Dialog is showing.
888             // This can be happen while a Dialog isn't created yet.
889             return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
890         }
891         if (mProgramGuide.isActive()) {
892             if (keyCode == KeyEvent.KEYCODE_BACK) {
893                 mProgramGuide.onBackPressed();
894                 return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
895             }
896             return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
897         }
898         if (mSideFragmentManager.isActive()) {
899             if (keyCode == KeyEvent.KEYCODE_BACK
900                     || mSideFragmentManager.isHideKeyForCurrentPanel(keyCode)) {
901                 mSideFragmentManager.popSideFragment();
902                 return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
903             }
904             return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
905         }
906         if (mMenu.isActive() || mTransitionManager.isSceneActive()) {
907             if (keyCode == KeyEvent.KEYCODE_BACK) {
908                 TimeShiftManager timeShiftManager = mMainActivity.getTimeShiftManager();
909                 if (timeShiftManager.isPaused()) {
910                     timeShiftManager.play();
911                 }
912                 hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
913                         | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
914                         | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
915                 return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
916             }
917             if (mMenu.isActive()) {
918                 if (KeypadChannelSwitchView.isChannelNumberKey(keyCode)) {
919                     mMainActivity.showKeypadChannelSwitchView(keyCode);
920                     return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
921                 }
922                 return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
923             }
924         }
925         if (mTransitionManager.isKeypadChannelSwitchActive()) {
926             if (keyCode == KeyEvent.KEYCODE_BACK) {
927                 mTransitionManager.goToEmptyScene(true);
928                 return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
929             }
930             return mKeypadChannelSwitchView.onKeyUp(keyCode, event) ?
931                     MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED
932                     : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED;
933         }
934         if (mTransitionManager.isSelectInputActive()) {
935             if (keyCode == KeyEvent.KEYCODE_BACK) {
936                 mTransitionManager.goToEmptyScene(true);
937                 return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
938             }
939             return mSelectInputView.onKeyUp(keyCode, event) ?
940                     MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED
941                     : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED;
942         }
943         if (mSetupFragmentActive) {
944             if (keyCode == KeyEvent.KEYCODE_BACK) {
945                 closeSetupFragment(true);
946                 return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
947             }
948             return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
949         }
950         if (mNewSourcesFragmentActive) {
951             if (keyCode == KeyEvent.KEYCODE_BACK) {
952                 closeNewSourcesFragment(true);
953                 return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
954             }
955             return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
956         }
957         return MainActivity.KEY_EVENT_HANDLER_RESULT_PASSTHROUGH;
958     }
959 
960     /**
961      * Checks whether the given {@code keyCode} can start the system's music app or not.
962      */
isMediaStartKey(int keyCode)963     private static boolean isMediaStartKey(int keyCode) {
964         switch (keyCode) {
965             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
966             case KeyEvent.KEYCODE_MEDIA_PLAY:
967             case KeyEvent.KEYCODE_MEDIA_PAUSE:
968             case KeyEvent.KEYCODE_MEDIA_NEXT:
969             case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
970             case KeyEvent.KEYCODE_MEDIA_REWIND:
971             case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
972             case KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD:
973             case KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD:
974                 return true;
975         }
976         return false;
977     }
978 
979     private static class TvOverlayHandler extends WeakHandler<TvOverlayManager> {
TvOverlayHandler(TvOverlayManager ref)980         TvOverlayHandler(TvOverlayManager ref) {
981             super(ref);
982         }
983 
984         @Override
handleMessage(Message msg, @NonNull TvOverlayManager tvOverlayManager)985         public void handleMessage(Message msg, @NonNull TvOverlayManager tvOverlayManager) {
986             switch (msg.what) {
987                 case MSG_OVERLAY_CLOSED:
988                     if (!tvOverlayManager.canExecuteCloseAction()) {
989                         return;
990                     }
991                     if (tvOverlayManager.showMenuWithTimeShiftPauseIfNeeded()) {
992                         return;
993                     }
994                     if (!tvOverlayManager.mPendingActions.isEmpty()) {
995                         Runnable action = tvOverlayManager.mPendingActions.get(0);
996                         tvOverlayManager.mPendingActions.remove(action);
997                         action.run();
998                     }
999                     break;
1000             }
1001         }
1002     }
1003 
1004     private class PendingDialogAction {
1005         private final String mTag;
1006         private final SafeDismissDialogFragment mDialog;
1007         private final boolean mKeepSidePanelHistory;
1008         private final boolean mKeepProgramGuide;
1009 
PendingDialogAction(String tag, SafeDismissDialogFragment dialog, boolean keepSidePanelHistory, boolean keepProgramGuide)1010         PendingDialogAction(String tag, SafeDismissDialogFragment dialog,
1011                 boolean keepSidePanelHistory, boolean keepProgramGuide) {
1012             mTag = tag;
1013             mDialog = dialog;
1014             mKeepSidePanelHistory = keepSidePanelHistory;
1015             mKeepProgramGuide = keepProgramGuide;
1016         }
1017 
run()1018         void run() {
1019             showDialogFragment(mTag, mDialog, mKeepSidePanelHistory, mKeepProgramGuide);
1020         }
1021     }
1022 }
1023