/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.ui; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentManager.OnBackStackChangedListener; import android.content.Intent; import android.media.tv.TvInputInfo; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.v4.os.BuildCompat; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Space; import com.android.tv.ApplicationSingletons; import com.android.tv.ChannelTuner; import com.android.tv.MainActivity; import com.android.tv.MainActivity.KeyHandlerResultType; import com.android.tv.R; import com.android.tv.TimeShiftManager; import com.android.tv.TvApplication; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.ui.setup.OnActionClickListener; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.data.ChannelDataManager; import com.android.tv.dialog.FullscreenDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.RecentlyWatchedDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.ui.DvrActivity; import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.guide.ProgramGuide; import com.android.tv.menu.Menu; import com.android.tv.menu.Menu.MenuShowReason; import com.android.tv.menu.MenuRowFactory; import com.android.tv.menu.MenuView; import com.android.tv.onboarding.NewSourcesFragment; import com.android.tv.onboarding.SetupSourcesFragment; import com.android.tv.onboarding.SetupSourcesFragment.InputSetupRunnable; import com.android.tv.search.ProgramGuideSearchFragment; import com.android.tv.ui.TvTransitionManager.SceneType; import com.android.tv.ui.sidepanel.SettingsFragment; import com.android.tv.ui.sidepanel.SideFragmentManager; import com.android.tv.ui.sidepanel.parentalcontrols.RatingsFragment; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * A class responsible for the life cycle and event handling of the pop-ups over TV view. */ // TODO: Put TvTransitionManager into this class. @UiThread public class TvOverlayManager { private static final String TAG = "TvOverlayManager"; private static final boolean DEBUG = false; public static final String INTRO_TRACKER_LABEL = "Intro dialog"; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_HIDE_OVERLAYS_DEFAULT, FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION, FLAG_HIDE_OVERLAYS_KEEP_SCENE, FLAG_HIDE_OVERLAYS_KEEP_DIALOG, FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS, FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY, FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE, FLAG_HIDE_OVERLAYS_KEEP_MENU, FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT}) public @interface HideOverlayFlag {} // FLAG_HIDE_OVERLAYs must be bitwise exclusive. public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b000000000; public static final int FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION = 0b000000010; public static final int FLAG_HIDE_OVERLAYS_KEEP_SCENE = 0b000000100; public static final int FLAG_HIDE_OVERLAYS_KEEP_DIALOG = 0b000001000; public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS = 0b000010000; public static final int FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY = 0b000100000; public static final int FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE = 0b001000000; public static final int FLAG_HIDE_OVERLAYS_KEEP_MENU = 0b010000000; public static final int FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT = 0b100000000; public static final int MSG_OVERLAY_CLOSED = 1000; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {OVERLAY_TYPE_NONE, OVERLAY_TYPE_MENU, OVERLAY_TYPE_SIDE_FRAGMENT, OVERLAY_TYPE_DIALOG, OVERLAY_TYPE_GUIDE, OVERLAY_TYPE_SCENE_CHANNEL_BANNER, OVERLAY_TYPE_SCENE_INPUT_BANNER, OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH, OVERLAY_TYPE_SCENE_SELECT_INPUT, OVERLAY_TYPE_FRAGMENT}) private @interface TvOverlayType {} // OVERLAY_TYPEs must be bitwise exclusive. private static final int OVERLAY_TYPE_NONE = 0b000000000; private static final int OVERLAY_TYPE_MENU = 0b000000001; private static final int OVERLAY_TYPE_SIDE_FRAGMENT = 0b000000010; private static final int OVERLAY_TYPE_DIALOG = 0b000000100; private static final int OVERLAY_TYPE_GUIDE = 0b000001000; private static final int OVERLAY_TYPE_SCENE_CHANNEL_BANNER = 0b000010000; private static final int OVERLAY_TYPE_SCENE_INPUT_BANNER = 0b000100000; private static final int OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH = 0b001000000; private static final int OVERLAY_TYPE_SCENE_SELECT_INPUT = 0b010000000; private static final int OVERLAY_TYPE_FRAGMENT = 0b100000000; private static final Set AVAILABLE_DIALOG_TAGS = new HashSet<>(); static { AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(PinDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(SettingsFragment.LicenseActionItem.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(RatingsFragment.AttributionItem.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(HalfSizedDialogFragment.DIALOG_TAG); } private final MainActivity mMainActivity; private final ChannelTuner mChannelTuner; private final TvTransitionManager mTransitionManager; private final ChannelDataManager mChannelDataManager; private final Menu mMenu; private final SideFragmentManager mSideFragmentManager; private final ProgramGuide mProgramGuide; private final KeypadChannelSwitchView mKeypadChannelSwitchView; private final SelectInputView mSelectInputView; private final ProgramGuideSearchFragment mSearchFragment; private final Tracker mTracker; private SafeDismissDialogFragment mCurrentDialog; private final SetupSourcesFragment mSetupFragment; private boolean mSetupFragmentActive; private final NewSourcesFragment mNewSourcesFragment; private boolean mNewSourcesFragmentActive; private final Handler mHandler = new TvOverlayHandler(this); private @TvOverlayType int mOpenedOverlays; private final List mPendingActions = new ArrayList<>(); public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner, KeypadChannelSwitchView keypadChannelSwitchView, ChannelBannerView channelBannerView, InputBannerView inputBannerView, SelectInputView selectInputView, ViewGroup sceneContainer, ProgramGuideSearchFragment searchFragment) { mMainActivity = mainActivity; mChannelTuner = channelTuner; ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity); mChannelDataManager = singletons.getChannelDataManager(); mKeypadChannelSwitchView = keypadChannelSwitchView; mSelectInputView = selectInputView; mSearchFragment = searchFragment; mTracker = singletons.getTracker(); mTransitionManager = new TvTransitionManager(mainActivity, sceneContainer, channelBannerView, inputBannerView, mKeypadChannelSwitchView, selectInputView); mTransitionManager.setListener(new TvTransitionManager.Listener() { @Override public void onSceneChanged(int fromScene, int toScene) { // Call notifyOverlayOpened first so that the listener can know that a new scene // will be opened when the notifyOverlayClosed is called. if (toScene != TvTransitionManager.SCENE_TYPE_EMPTY) { onOverlayOpened(convertSceneToOverlayType(toScene)); } if (fromScene != TvTransitionManager.SCENE_TYPE_EMPTY) { onOverlayClosed(convertSceneToOverlayType(fromScene)); } } }); // Menu MenuView menuView = (MenuView) mainActivity.findViewById(R.id.menu); mMenu = new Menu(mainActivity, menuView, new MenuRowFactory(mainActivity), new Menu.OnMenuVisibilityChangeListener() { @Override public void onMenuVisibilityChange(boolean visible) { if (visible) { onOverlayOpened(OVERLAY_TYPE_MENU); } else { onOverlayClosed(OVERLAY_TYPE_MENU); } } }); // Side Fragment mSideFragmentManager = new SideFragmentManager(mainActivity, new Runnable() { @Override public void run() { onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT); hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS); } }, new Runnable() { @Override public void run() { mMainActivity.showChannelBannerIfHiddenBySideFragment(); onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT); } }); // Program Guide Runnable preShowRunnable = new Runnable() { @Override public void run() { onOverlayOpened(OVERLAY_TYPE_GUIDE); } }; Runnable postHideRunnable = new Runnable() { @Override public void run() { onOverlayClosed(OVERLAY_TYPE_GUIDE); } }; DvrDataManager dvrDataManager = CommonFeatures.DVR.isEnabled(mainActivity) && BuildCompat.isAtLeastN() ? singletons .getDvrDataManager() : null; mProgramGuide = new ProgramGuide(mainActivity, channelTuner, singletons.getTvInputManagerHelper(), mChannelDataManager, singletons.getProgramDataManager(), dvrDataManager, singletons.getTracker(), preShowRunnable, postHideRunnable); mSetupFragment = new SetupSourcesFragment(); mSetupFragment.setOnActionClickListener(new OnActionClickListener() { @Override public void onActionClick(String category, int id) { switch (id) { case SetupMultiPaneFragment.ACTION_DONE: closeSetupFragment(true); break; case SetupSourcesFragment.ACTION_PLAY_STORE: mMainActivity.showMerchantCollection(); break; } } }); mSetupFragment.setInputSetupRunnable(new InputSetupRunnable() { @Override public void runInputSetup(TvInputInfo input) { mMainActivity.startSetupActivity(input, true); } }); mNewSourcesFragment = new NewSourcesFragment(); mNewSourcesFragment.setOnActionClickListener(new OnActionClickListener() { @Override public void onActionClick(String category, int id) { switch (id) { case NewSourcesFragment.ACTION_SETUP: closeNewSourcesFragment(false); showSetupFragment(); break; case NewSourcesFragment.ACTION_SKIP: // Don't remove the fragment because new fragment will be replaced with // this fragment. closeNewSourcesFragment(true); break; } } }); } /** * A method to release all the allocated resources or unregister listeners. * This is called from {@link MainActivity#onDestroy}. */ public void release() { mMenu.release(); mHandler.removeCallbacksAndMessages(null); } /** * Returns the instance of {@link Menu}. */ public Menu getMenu() { return mMenu; } /** * Returns the instance of {@link SideFragmentManager}. */ public SideFragmentManager getSideFragmentManager() { return mSideFragmentManager; } /** * Returns the currently opened dialog. */ public SafeDismissDialogFragment getCurrentDialog() { return mCurrentDialog; } /** * Checks whether the setup fragment is active or not. */ public boolean isSetupFragmentActive() { return mSetupFragmentActive; } /** * Checks whether the new sources fragment is active or not. */ public boolean isNewSourcesFragmentActive() { return mNewSourcesFragmentActive; } /** * Returns the instance of {@link ProgramGuide}. */ public ProgramGuide getProgramGuide() { return mProgramGuide; } /** * Shows the main menu. */ public void showMenu(@MenuShowReason int reason) { if (mChannelTuner != null && mChannelTuner.areAllChannelsLoaded()) { mMenu.show(reason); } } /** * Shows the play controller of the menu if the playback is paused. */ public boolean showMenuWithTimeShiftPauseIfNeeded() { if (mMainActivity.getTimeShiftManager().isPaused()) { showMenu(Menu.REASON_PLAY_CONTROLS_PAUSE); return true; } return false; } /** * Shows the given dialog. */ public void showDialogFragment(String tag, SafeDismissDialogFragment dialog, boolean keepSidePanelHistory) { showDialogFragment(tag, dialog, keepSidePanelHistory, false); } public void showDialogFragment(String tag, SafeDismissDialogFragment dialog, boolean keepSidePanelHistory, boolean keepProgramGuide) { int flags = FLAG_HIDE_OVERLAYS_KEEP_DIALOG; if (keepSidePanelHistory) { flags |= FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY; } if (keepProgramGuide) { flags |= FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE; } hideOverlays(flags); // A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV. if (!AVAILABLE_DIALOG_TAGS.contains(tag)) { return; } Fragment old = mMainActivity.getFragmentManager().findFragmentByTag(tag); // Do not show the dialog if the same kind of dialog is already opened. if (old != null) { return; } mCurrentDialog = dialog; dialog.show(mMainActivity.getFragmentManager(), tag); // Calling this from SafeDismissDialogFragment.onCreated() might be late // because it takes time for onCreated to be called // and next key events can be handled by MainActivity, not Dialog. onOverlayOpened(OVERLAY_TYPE_DIALOG); } private void runAfterSideFragmentsAreClosed(final Runnable runnable) { final FragmentManager manager = mMainActivity.getFragmentManager(); if (mSideFragmentManager.isSidePanelVisible()) { manager.addOnBackStackChangedListener(new OnBackStackChangedListener() { @Override public void onBackStackChanged() { if (manager.getBackStackEntryCount() == 0) { manager.removeOnBackStackChangedListener(this); runnable.run(); } } }); } else { runnable.run(); } } private void showFragment(final Fragment fragment) { hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); onOverlayOpened(OVERLAY_TYPE_FRAGMENT); runAfterSideFragmentsAreClosed(new Runnable() { @Override public void run() { mMainActivity.getFragmentManager().beginTransaction() .replace(R.id.fragment_container, fragment).commit(); } }); } private void closeFragment(Fragment fragmentToRemove) { onOverlayClosed(OVERLAY_TYPE_FRAGMENT); if (fragmentToRemove != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // In L, NPE happens if there is no next fragment when removing or hiding a fragment // which has an exit transition. b/22631964 // A workaround is just replacing with a dummy fragment. mMainActivity.getFragmentManager().beginTransaction() .replace(R.id.fragment_container, new DummyFragment()).commit(); } else { mMainActivity.getFragmentManager().beginTransaction().remove(fragmentToRemove) .commit(); } } } /** * Shows setup dialog. */ public void showSetupFragment() { if (DEBUG) Log.d(TAG, "showSetupFragment"); mSetupFragmentActive = true; mSetupFragment.enableFragmentTransition(SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION | SetupFragment.FRAGMENT_REENTER_TRANSITION); mSetupFragment.setFragmentTransition(SetupFragment.FRAGMENT_EXIT_TRANSITION, Gravity.END); showFragment(mSetupFragment); } // Set removeFragment to false only when the new fragment is going to be shown. private void closeSetupFragment(boolean removeFragment) { if (DEBUG) Log.d(TAG, "closeSetupFragment"); if (!mSetupFragmentActive) { return; } mSetupFragmentActive = false; closeFragment(removeFragment ? mSetupFragment : null); if (mChannelDataManager.getChannelCount() == 0) { mMainActivity.finish(); } } /** * Shows new sources dialog. */ public void showNewSourcesFragment() { if (DEBUG) Log.d(TAG, "showNewSourcesFragment"); mNewSourcesFragmentActive = true; showFragment(mNewSourcesFragment); } // Set removeFragment to false only when the new fragment is going to be shown. private void closeNewSourcesFragment(boolean removeFragment) { if (DEBUG) Log.d(TAG, "closeNewSourcesFragment"); mNewSourcesFragmentActive = false; closeFragment(removeFragment ? mNewSourcesFragment : null); } /** * Shows DVR manager. */ public void showDvrManager() { Intent intent = new Intent(mMainActivity, DvrActivity.class); mMainActivity.startActivity(intent); } /** * Shows intro dialog. */ public void showIntroDialog() { if (DEBUG) Log.d(TAG,"showIntroDialog"); showDialogFragment(FullscreenDialogFragment.DIALOG_TAG, FullscreenDialogFragment.newInstance(R.layout.intro_dialog, INTRO_TRACKER_LABEL), false); } /** * Shows recently watched dialog. */ public void showRecentlyWatchedDialog() { showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG, new RecentlyWatchedDialogFragment(), false); } /** * Shows banner view. */ public void showBanner() { mTransitionManager.goToChannelBannerScene(); } public void showKeypadChannelSwitch() { hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); mTransitionManager.goToKeypadChannelSwitchScene(); } /** * Shows select input view. */ public void showSelectInputView() { hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE); mTransitionManager.goToSelectInputScene(); } /** * Initializes animators if animators are not initialized yet. */ public void initAnimatorIfNeeded() { mTransitionManager.initIfNeeded(); } /** * It is called when a SafeDismissDialogFragment is destroyed. */ public void onDialogDestroyed() { mCurrentDialog = null; onOverlayClosed(OVERLAY_TYPE_DIALOG); } /** * Shows the program guide. */ public void showProgramGuide() { mProgramGuide.show(new Runnable() { @Override public void run() { hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE); } }); } /** * Hides all the opened overlays according to the flags. */ // TODO: Add test for this method. public void hideOverlays(@HideOverlayFlag int flags) { if (mMainActivity.needToKeepSetupScreenWhenHidingOverlay()) { flags |= FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT; } if ((flags & FLAG_HIDE_OVERLAYS_KEEP_DIALOG) != 0) { // Keeps the dialog. } else { if (mCurrentDialog != null) { if (mCurrentDialog instanceof PinDialogFragment) { // The result listener of PinDialogFragment could call MenuView when // the dialog is dismissed. In order not to call it, set the result listener // to null. ((PinDialogFragment) mCurrentDialog).setResultListener(null); } mCurrentDialog.dismiss(); } mCurrentDialog = null; } boolean withAnimation = (flags & FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION) == 0; if ((flags & FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT) == 0) { if (mSetupFragmentActive) { if (!withAnimation) { mSetupFragment.setReturnTransition(null); mSetupFragment.setExitTransition(null); } closeSetupFragment(true); } else if (mNewSourcesFragmentActive) { closeNewSourcesFragment(true); } } if ((flags & FLAG_HIDE_OVERLAYS_KEEP_MENU) != 0) { // Keeps the menu. } else { mMenu.hide(withAnimation); } if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SCENE) != 0) { // Keeps the current scene. } else { mTransitionManager.goToEmptyScene(withAnimation); } if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS) != 0) { // Keeps side panels. } else if (mSideFragmentManager.isSidePanelVisible()) { if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY) != 0) { mSideFragmentManager.hideSidePanel(withAnimation); } else { mSideFragmentManager.hideAll(withAnimation); } } if ((flags & FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE) != 0) { // Keep the program guide. } else { mProgramGuide.hide(); } } /** * Returns true, if a main view needs to hide informational text. Specifically, when overlay * UIs except banner is shown, the informational text needs to be hidden for clean UI. */ public boolean needHideTextOnMainView() { return mSideFragmentManager.isActive() || getMenu().isActive() || mTransitionManager.isKeypadChannelSwitchActive() || mTransitionManager.isSelectInputActive() || mSetupFragmentActive || mNewSourcesFragmentActive; } @TvOverlayType private int convertSceneToOverlayType(@SceneType int sceneType) { switch (sceneType) { case TvTransitionManager.SCENE_TYPE_CHANNEL_BANNER: return OVERLAY_TYPE_SCENE_CHANNEL_BANNER; case TvTransitionManager.SCENE_TYPE_INPUT_BANNER: return OVERLAY_TYPE_SCENE_INPUT_BANNER; case TvTransitionManager.SCENE_TYPE_KEYPAD_CHANNEL_SWITCH: return OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH; case TvTransitionManager.SCENE_TYPE_SELECT_INPUT: return OVERLAY_TYPE_SCENE_SELECT_INPUT; case TvTransitionManager.SCENE_TYPE_EMPTY: default: return OVERLAY_TYPE_NONE; } } @UiThread private void onOverlayOpened(@TvOverlayType int overlayType) { if (DEBUG) Log.d(TAG, "Overlay opened: 0b" + Integer.toBinaryString(overlayType)); mOpenedOverlays |= overlayType; if (DEBUG) Log.d(TAG, "Opened overlays: 0b" + Integer.toBinaryString(mOpenedOverlays)); mHandler.removeMessages(MSG_OVERLAY_CLOSED); mMainActivity.updateKeyInputFocus(); } @UiThread private void onOverlayClosed(@TvOverlayType int overlayType) { if (DEBUG) Log.d(TAG, "Overlay closed: 0b" + Integer.toBinaryString(overlayType)); mOpenedOverlays &= ~overlayType; if (DEBUG) Log.d(TAG, "Opened overlays: 0b" + Integer.toBinaryString(mOpenedOverlays)); mHandler.removeMessages(MSG_OVERLAY_CLOSED); mMainActivity.updateKeyInputFocus(); // Show the main menu again if there are no pop-ups or banners only. // The main menu should not be shown when the activity is in paused state. boolean menuAboutToShow = false; if (canExecuteCloseAction()) { menuAboutToShow = mMainActivity.getTimeShiftManager().isPaused(); mHandler.sendEmptyMessage(MSG_OVERLAY_CLOSED); } // Don't set screen name to main if the overlay closing is a banner // or if a non banner overlay is still open // or if we just opened the menu if (overlayType != OVERLAY_TYPE_SCENE_CHANNEL_BANNER && overlayType != OVERLAY_TYPE_SCENE_INPUT_BANNER && isOnlyBannerOrNoneOpened() && !menuAboutToShow) { mTracker.sendScreenView(MainActivity.SCREEN_NAME); } } private boolean canExecuteCloseAction() { return mMainActivity.isActivityResumed() && isOnlyBannerOrNoneOpened(); } private boolean isOnlyBannerOrNoneOpened() { return (mOpenedOverlays & ~OVERLAY_TYPE_SCENE_CHANNEL_BANNER & ~OVERLAY_TYPE_SCENE_INPUT_BANNER) == 0; } /** * Runs a given {@code action} after all the overlays are closed. */ @UiThread public void runAfterOverlaysAreClosed(Runnable action) { if (canExecuteCloseAction()) { action.run(); } else { mPendingActions.add(action); } } /** * Handles the onUserInteraction event of the {@link MainActivity}. */ public void onUserInteraction() { if (mSideFragmentManager.isActive()) { mSideFragmentManager.scheduleHideAll(); } else if (mMenu.isActive()) { mMenu.scheduleHide(); } else if (mProgramGuide.isActive()) { mProgramGuide.scheduleHide(); } } /** * Handles the onKeyDown event of the {@link MainActivity}. */ @KeyHandlerResultType public int onKeyDown(int keyCode, KeyEvent event) { if (mCurrentDialog != null) { // Consumes the keys while a Dialog is creating. return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } // Handle media key here because it is related to the menu. if (isMediaStartKey(keyCode)) { // Consumes the keys which may trigger system's default music player. return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } if (mMenu.isActive() || mSideFragmentManager.isActive() || mProgramGuide.isActive() || mSetupFragmentActive || mNewSourcesFragmentActive) { return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY; } if (mTransitionManager.isKeypadChannelSwitchActive()) { return mKeypadChannelSwitchView.onKeyDown(keyCode, event) ? MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED; } if (mTransitionManager.isSelectInputActive()) { return mSelectInputView.onKeyDown(keyCode, event) ? MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED; } return MainActivity.KEY_EVENT_HANDLER_RESULT_PASSTHROUGH; } /** * Handles the onKeyUp event of the {@link MainActivity}. */ @KeyHandlerResultType public int onKeyUp(int keyCode, KeyEvent event) { // Handle media key here because it is related to the menu. if (isMediaStartKey(keyCode)) { // The media key should not be passed up to the system in any cases. if (mCurrentDialog != null || mProgramGuide.isActive() || mSideFragmentManager.isActive() || mSearchFragment.isVisible() || mTransitionManager.isKeypadChannelSwitchActive() || mTransitionManager.isSelectInputActive() || mSetupFragmentActive || mNewSourcesFragmentActive) { // Do not handle media key when any pop-ups which can handle keys are active. return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } TimeShiftManager timeShiftManager = mMainActivity.getTimeShiftManager(); if (!timeShiftManager.isAvailable()) { return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY: timeShiftManager.play(); showMenu(Menu.REASON_PLAY_CONTROLS_PLAY); break; case KeyEvent.KEYCODE_MEDIA_PAUSE: timeShiftManager.pause(); showMenu(Menu.REASON_PLAY_CONTROLS_PAUSE); break; case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: timeShiftManager.togglePlayPause(); showMenu(Menu.REASON_PLAY_CONTROLS_PLAY_PAUSE); break; case KeyEvent.KEYCODE_MEDIA_REWIND: timeShiftManager.rewind(); showMenu(Menu.REASON_PLAY_CONTROLS_REWIND); break; case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: timeShiftManager.fastForward(); showMenu(Menu.REASON_PLAY_CONTROLS_FAST_FORWARD); break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: timeShiftManager.jumpToPrevious(); showMenu(Menu.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS); break; case KeyEvent.KEYCODE_MEDIA_NEXT: timeShiftManager.jumpToNext(); showMenu(Menu.REASON_PLAY_CONTROLS_JUMP_TO_NEXT); break; default: // Does nothing. break; } return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } if (keyCode == KeyEvent.KEYCODE_I || keyCode == KeyEvent.KEYCODE_TV_INPUT) { if (mTransitionManager.isSelectInputActive()) { mSelectInputView.onKeyUp(keyCode, event); } else { showSelectInputView(); } return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } if (mCurrentDialog != null) { // Consumes the keys while a Dialog is showing. // This can be happen while a Dialog isn't created yet. return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } if (mProgramGuide.isActive()) { if (keyCode == KeyEvent.KEYCODE_BACK) { mProgramGuide.onBackPressed(); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY; } if (mSideFragmentManager.isActive()) { if (keyCode == KeyEvent.KEYCODE_BACK || mSideFragmentManager.isHideKeyForCurrentPanel(keyCode)) { mSideFragmentManager.popSideFragment(); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY; } if (mMenu.isActive() || mTransitionManager.isSceneActive()) { if (keyCode == KeyEvent.KEYCODE_BACK) { TimeShiftManager timeShiftManager = mMainActivity.getTimeShiftManager(); if (timeShiftManager.isPaused()) { timeShiftManager.play(); } hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } if (mMenu.isActive()) { if (KeypadChannelSwitchView.isChannelNumberKey(keyCode)) { mMainActivity.showKeypadChannelSwitchView(keyCode); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY; } } if (mTransitionManager.isKeypadChannelSwitchActive()) { if (keyCode == KeyEvent.KEYCODE_BACK) { mTransitionManager.goToEmptyScene(true); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } return mKeypadChannelSwitchView.onKeyUp(keyCode, event) ? MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED; } if (mTransitionManager.isSelectInputActive()) { if (keyCode == KeyEvent.KEYCODE_BACK) { mTransitionManager.goToEmptyScene(true); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } return mSelectInputView.onKeyUp(keyCode, event) ? MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED : MainActivity.KEY_EVENT_HANDLER_RESULT_NOT_HANDLED; } if (mSetupFragmentActive) { if (keyCode == KeyEvent.KEYCODE_BACK) { closeSetupFragment(true); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY; } if (mNewSourcesFragmentActive) { if (keyCode == KeyEvent.KEYCODE_BACK) { closeNewSourcesFragment(true); return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED; } return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY; } return MainActivity.KEY_EVENT_HANDLER_RESULT_PASSTHROUGH; } /** * Checks whether the given {@code keyCode} can start the system's music app or not. */ private static boolean isMediaStartKey(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_REWIND: case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: return true; } return false; } private static class TvOverlayHandler extends WeakHandler { public TvOverlayHandler(TvOverlayManager ref) { super(ref); } @Override public void handleMessage(Message msg, @NonNull TvOverlayManager tvOverlayManager) { switch (msg.what) { case MSG_OVERLAY_CLOSED: if (!tvOverlayManager.canExecuteCloseAction()) { return; } if (tvOverlayManager.showMenuWithTimeShiftPauseIfNeeded()) { return; } if (!tvOverlayManager.mPendingActions.isEmpty()) { Runnable action = tvOverlayManager.mPendingActions.get(0); tvOverlayManager.mPendingActions.remove(action); action.run(); } break; } } } /** * Dummny class for the workaround of b/22631964. See {@link #closeFragment}. */ public static class DummyFragment extends Fragment { @Override public @Nullable View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View v = new Space(inflater.getContext()); v.setVisibility(View.GONE); return v; } } }