/* * 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.TvContentRating; import android.media.tv.TvInputInfo; 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.UiThread; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; 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.TvOptionsManager; import com.android.tv.TvSingletons; 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.data.ProgramDataManager; import com.android.tv.dialog.DvrHistoryDialogFragment; import com.android.tv.dialog.FullscreenDialogFragment; import com.android.tv.dialog.HalfSizedDialogFragment; 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.browse.DvrBrowseActivity; import com.android.tv.guide.ProgramGuide; import com.android.tv.license.LicenseDialogFragment; 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.menu.TvOptionsRowAdapter; import com.android.tv.onboarding.NewSourcesFragment; import com.android.tv.onboarding.SetupSourcesFragment; import com.android.tv.search.ProgramGuideSearchFragment; import com.android.tv.ui.TvTransitionManager.SceneType; import com.android.tv.ui.sidepanel.SideFragmentManager; import com.android.tv.ui.sidepanel.parentalcontrols.RatingsFragment; import com.android.tv.util.TvInputManagerHelper; import com.google.auto.factory.AutoFactory; import com.google.auto.factory.Provided; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.Set; /** A class responsible for the life cycle and event handling of the pop-ups over TV view. */ @UiThread @AutoFactory public class TvOverlayManager implements AccessibilityStateChangeListener { private static final String TAG = "TvOverlayManager"; private static final boolean DEBUG = false; private 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 }) private @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; private 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. /** The overlay type which indicates that there are no overlays. */ private static final int OVERLAY_TYPE_NONE = 0b000000000; /** The overlay type for menu. */ private static final int OVERLAY_TYPE_MENU = 0b000000001; /** The overlay type for the side fragment. */ private static final int OVERLAY_TYPE_SIDE_FRAGMENT = 0b000000010; /** The overlay type for dialog fragment. */ private static final int OVERLAY_TYPE_DIALOG = 0b000000100; /** The overlay type for program guide. */ private static final int OVERLAY_TYPE_GUIDE = 0b000001000; /** The overlay type for channel banner. */ private static final int OVERLAY_TYPE_SCENE_CHANNEL_BANNER = 0b000010000; /** The overlay type for input banner. */ private static final int OVERLAY_TYPE_SCENE_INPUT_BANNER = 0b000100000; /** The overlay type for keypad channel switch view. */ private static final int OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH = 0b001000000; /** The overlay type for select input view. */ private static final int OVERLAY_TYPE_SCENE_SELECT_INPUT = 0b010000000; /** The overlay type for fragment other than the side fragment and dialog fragment. */ private static final int OVERLAY_TYPE_FRAGMENT = 0b100000000; // Used for the padded print of the overlay type. private static final int NUM_OVERLAY_TYPES = 9; @Retention(RetentionPolicy.SOURCE) @IntDef({ UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW, UPDATE_CHANNEL_BANNER_REASON_TUNE, UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST, UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO, UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK, UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO }) private @interface ChannelBannerUpdateReason {} /** Updates channel banner because the channel banner is forced to show. */ public static final int UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW = 1; /** Updates channel banner because of tuning. */ public static final int UPDATE_CHANNEL_BANNER_REASON_TUNE = 2; /** Updates channel banner because of fast tuning. */ public static final int UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST = 3; /** Updates channel banner because of info updating. */ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO = 4; /** Updates channel banner because the current watched channel is locked or unlocked. */ public static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5; /** Updates channel banner because of stream info updating. */ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6; /** Updates channel banner because of channel signal updating. */ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH = 7; private static final String FRAGMENT_TAG_SETUP_SOURCES = "tag_setup_sources"; private static final String FRAGMENT_TAG_NEW_SOURCES = "tag_new_sources"; private static final Set AVAILABLE_DIALOG_TAGS = new HashSet<>(); static { AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(DvrHistoryDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(PinDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(LicenseDialogFragment.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 TvInputManagerHelper mInputManager; private final Menu mMenu; private final TunableTvView mTvView; private final SideFragmentManager mSideFragmentManager; private final ProgramGuide mProgramGuide; private final ChannelBannerView mChannelBannerView; private final KeypadChannelSwitchView mKeypadChannelSwitchView; private final SelectInputView mSelectInputView; private final ProgramGuideSearchFragment mSearchFragment; private final Tracker mTracker; private SafeDismissDialogFragment mCurrentDialog; private boolean mSetupFragmentActive; private boolean mNewSourcesFragmentActive; private boolean mChannelBannerHiddenBySideFragment; private final Handler mHandler = new TvOverlayHandler(this); private @TvOverlayType int mOpenedOverlays; private final List mPendingActions = new ArrayList<>(); private final Queue mPendingDialogActionQueue = new LinkedList<>(); private final TvOptionsRowAdapter.Factory mTvOptionsRowAdapterFactory; private OnBackStackChangedListener mOnBackStackChangedListener; public TvOverlayManager( MainActivity mainActivity, ChannelTuner channelTuner, TunableTvView tvView, TvOptionsManager optionsManager, KeypadChannelSwitchView keypadChannelSwitchView, ChannelBannerView channelBannerView, InputBannerView inputBannerView, SelectInputView selectInputView, ViewGroup sceneContainer, ProgramGuideSearchFragment searchFragment, @Provided ChannelDataManager channelDataManager, @Provided TvInputManagerHelper tvInputManager, @Provided ProgramDataManager programDataManager, @Provided TvOptionsRowAdapter.Factory mTvOptionsRowAdapterFactory) { mMainActivity = mainActivity; mChannelTuner = channelTuner; this.mTvOptionsRowAdapterFactory = mTvOptionsRowAdapterFactory; TvSingletons singletons = TvSingletons.getSingletons(mainActivity); mChannelDataManager = channelDataManager; mInputManager = tvInputManager; mTvView = tvView; mChannelBannerView = channelBannerView; 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 onOverlayOpened first so that the listener can know that a new scene // will be opened when the onOverlayClosed 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, tvView, optionsManager, menuView, new MenuRowFactory(mainActivity, tvView, this.mTvOptionsRowAdapterFactory), new Menu.OnMenuVisibilityChangeListener() { @Override public void onMenuVisibilityChange(boolean visible) { if (visible) { onOverlayOpened(OVERLAY_TYPE_MENU); } else { onOverlayClosed(OVERLAY_TYPE_MENU); } } }); mMenu.setChannelTuner(mChannelTuner); // Side Fragment mSideFragmentManager = new SideFragmentManager( mainActivity, () -> { onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT); hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS); }, () -> { showChannelBannerIfHiddenBySideFragment(); onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT); }); // Program Guide Runnable preShowRunnable = () -> onOverlayOpened(OVERLAY_TYPE_GUIDE); Runnable postHideRunnable = () -> onOverlayClosed(OVERLAY_TYPE_GUIDE); DvrDataManager dvrDataManager = CommonFeatures.DVR.isEnabled(mainActivity) ? singletons.getDvrDataManager() : null; mProgramGuide = new ProgramGuide( mainActivity, channelTuner, mInputManager, mChannelDataManager, programDataManager, dvrDataManager, singletons.getDvrScheduleManager(), singletons.getTracker(), preShowRunnable, postHideRunnable); mMainActivity.addOnActionClickListener( new OnActionClickListener() { @Override public boolean onActionClick(String category, int id, Bundle params) { switch (category) { case SetupSourcesFragment.ACTION_CATEGORY: switch (id) { case SetupMultiPaneFragment.ACTION_DONE: closeSetupFragment(true); return true; case SetupSourcesFragment.ACTION_ONLINE_STORE: mMainActivity.showMerchantCollection(); return true; case SetupSourcesFragment.ACTION_SETUP_INPUT: { String inputId = params.getString( SetupSourcesFragment .ACTION_PARAM_KEY_INPUT_ID); TvInputInfo input = mInputManager.getTvInputInfo(inputId); mMainActivity.startSetupActivity(input, true); return true; } } break; case NewSourcesFragment.ACTION_CATEOGRY: switch (id) { case NewSourcesFragment.ACTION_SETUP: closeNewSourcesFragment(false); showSetupFragment(); return true; case NewSourcesFragment.ACTION_SKIP: // Don't remove the fragment because new fragment will be // replaced // with this fragment. closeNewSourcesFragment(true); return true; } break; } return false; } }); } /** * 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); if (mKeypadChannelSwitchView != null) { mKeypadChannelSwitchView.setChannels(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() { // "getSetupSourcesFragment() != null" doesn't return the correct state. That's because, // when we call showSetupFragment(), we need to put off showing the fragment until the side // fragment is closed. Until then, getSetupSourcesFragment() returns null. So we need // to keep additional variable which indicates if showSetupFragment() is called. return mSetupFragmentActive; } private Fragment getSetupSourcesFragment() { return mMainActivity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG_SETUP_SOURCES); } /** Checks whether the new sources fragment is active or not. */ public boolean isNewSourcesFragmentActive() { // See the comment in "isSetupFragmentActive". return mNewSourcesFragmentActive; } private Fragment getNewSourcesFragment() { return mMainActivity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG_NEW_SOURCES); } /** 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; } // Do not open two dialogs at the same time. if (mCurrentDialog != null) { mPendingDialogActionQueue.offer( new PendingDialogAction(tag, dialog, keepSidePanelHistory, keepProgramGuide)); 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); } /** * Should be called by {@link MainActivity} when the currently browsable channels are updated. */ public void onBrowsableChannelsUpdated() { mKeypadChannelSwitchView.setChannels(mChannelTuner.getBrowsableChannelList()); } private void runAfterSideFragmentsAreClosed(final Runnable runnable) { if (mSideFragmentManager.isSidePanelVisible()) { // When the side panel is closing, it closes all the fragments, so the new fragment // should be opened after the side fragment becomes invisible. final FragmentManager manager = mMainActivity.getFragmentManager(); mOnBackStackChangedListener = new OnBackStackChangedListener() { @Override public void onBackStackChanged() { if (manager.getBackStackEntryCount() == 0) { manager.removeOnBackStackChangedListener(this); mOnBackStackChangedListener = null; runnable.run(); } } }; manager.addOnBackStackChangedListener(mOnBackStackChangedListener); } else { runnable.run(); } } private void showFragment(final Fragment fragment, final String tag) { hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); onOverlayOpened(OVERLAY_TYPE_FRAGMENT); runAfterSideFragmentsAreClosed( () -> { if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")"); mMainActivity .getFragmentManager() .beginTransaction() .replace(R.id.fragment_container, fragment, tag) .commit(); }); } private void closeFragment(String fragmentTagToRemove) { if (DEBUG) Log.d(TAG, "closeFragment(" + fragmentTagToRemove + ")"); onOverlayClosed(OVERLAY_TYPE_FRAGMENT); if (fragmentTagToRemove != null) { Fragment fragmentToRemove = mMainActivity.getFragmentManager().findFragmentByTag(fragmentTagToRemove); if (fragmentToRemove == null) { // If the fragment has not been added to the fragment manager yet, just remove the // listener not to add the fragment. This is needed because the side fragment is // closed asynchronously. mMainActivity .getFragmentManager() .removeOnBackStackChangedListener(mOnBackStackChangedListener); mOnBackStackChangedListener = null; } else { mMainActivity .getFragmentManager() .beginTransaction() .remove(fragmentToRemove) .commit(); } } } /** Shows setup dialog. */ public void showSetupFragment() { if (DEBUG) Log.d(TAG, "showSetupFragment"); mSetupFragmentActive = true; SetupSourcesFragment setupFragment = new SetupSourcesFragment(); setupFragment.enableFragmentTransition( SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_EXIT_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION | SetupFragment.FRAGMENT_REENTER_TRANSITION); setupFragment.setFragmentTransition(SetupFragment.FRAGMENT_EXIT_TRANSITION, Gravity.END); showFragment(setupFragment, FRAGMENT_TAG_SETUP_SOURCES); } // 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 ? FRAGMENT_TAG_SETUP_SOURCES : null); if (mChannelDataManager.getChannelCount() == 0) { if (DEBUG) Log.d(TAG, "Finishing MainActivity because there are no channels."); mMainActivity.finish(); } } /** Shows new sources dialog. */ public void showNewSourcesFragment() { if (DEBUG) Log.d(TAG, "showNewSourcesFragment"); mNewSourcesFragmentActive = true; showFragment(new NewSourcesFragment(), FRAGMENT_TAG_NEW_SOURCES); } // 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"); if (!mNewSourcesFragmentActive) { return; } mNewSourcesFragmentActive = false; closeFragment(removeFragment ? FRAGMENT_TAG_NEW_SOURCES : null); } /** Shows DVR manager. */ public void showDvrManager() { Intent intent = new Intent(mMainActivity, DvrBrowseActivity.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 DVR history dialog. */ public void showDvrHistoryDialog() { showDialogFragment( DvrHistoryDialogFragment.DIALOG_TAG, new DvrHistoryDialogFragment(), false); } /** Shows banner view. */ public void showBanner() { mTransitionManager.goToChannelBannerScene(); } /** * Pops up the KeypadChannelSwitchView with the given key input event. * * @param keyCode A key code of the key event. */ public void showKeypadChannelSwitch(int keyCode) { if (mChannelTuner.areAllChannelsLoaded()) { 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(); mKeypadChannelSwitchView.onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0); } } /** 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; PendingDialogAction action = mPendingDialogActionQueue.poll(); if (action == null) { onOverlayClosed(OVERLAY_TYPE_DIALOG); } else { action.run(); } } /** Shows the program guide. */ public void showProgramGuide() { mProgramGuide.show( () -> hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE)); } /** * Shows/hides the program guide according to it's hidden or shown now. * * @return {@code true} if program guide is going to be shown, otherwise {@code false}. */ public boolean toggleProgramGuide() { if (mProgramGuide.isActive()) { mProgramGuide.onBackPressed(); return false; } else { showProgramGuide(); return true; } } /** Sets blocking content rating of the currently playing TV channel. */ public void setBlockingContentRating(TvContentRating rating) { if (!mMainActivity.isChannelChangeKeyDownReceived()) { mChannelBannerView.setBlockingContentRating(rating); updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); } } public boolean isOverlayOpened() { return mOpenedOverlays != OVERLAY_TYPE_NONE; } /** 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) { // We don't want any OnPinCheckedListener is triggered to prevent any possible // side effects. Dismisses the dialog silently. ((PinDialogFragment) mCurrentDialog).dismissSilently(); } else { mCurrentDialog.dismiss(); } } mPendingDialogActionQueue.clear(); mCurrentDialog = null; } boolean withAnimation = (flags & FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION) == 0; if ((flags & FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT) == 0) { Fragment setupSourcesFragment = getSetupSourcesFragment(); Fragment newSourcesFragment = getNewSourcesFragment(); if (mSetupFragmentActive) { if (!withAnimation && setupSourcesFragment != null) { setupSourcesFragment.setReturnTransition(null); setupSourcesFragment.setExitTransition(null); } closeSetupFragment(true); } if (mNewSourcesFragmentActive) { if (!withAnimation && newSourcesFragment != null) { newSourcesFragment.setReturnTransition(null); newSourcesFragment.setExitTransition(null); } 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.isActive()) { 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(); } } @Override public void onAccessibilityStateChanged(boolean enabled) { // Propagate this to all elements that need it mChannelBannerView.onAccessibilityStateChanged(enabled); mProgramGuide.onAccessibilityStateChanged(enabled); mSideFragmentManager.onAccessibilityStateChanged(enabled); } /** * 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; } /** Updates and shows channel banner if it's needed. */ public void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) { if (DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")"); if (mMainActivity.isChannelChangeKeyDownReceived() && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) { // Tuning is still ongoing, no need to update banner for other reasons return; } if (!mChannelTuner.isCurrentChannelPassthrough()) { int lockType = ChannelBannerView.LOCK_NONE; if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) { if (mMainActivity.getParentalControlSettings().isParentalControlsEnabled() && mMainActivity.getCurrentChannel().isLocked()) { lockType = ChannelBannerView.LOCK_CHANNEL_INFO; } else { // Do not show detailed program information while fast-tuning. lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; } } else if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE) { if (mMainActivity.getParentalControlSettings().isParentalControlsEnabled()) { if (mMainActivity.getCurrentChannel().isLocked()) { lockType = ChannelBannerView.LOCK_CHANNEL_INFO; } else { // If parental control is turned on, // assumes that program is locked by default and waits for onContentAllowed. lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; } } } else if (mTvView.isScreenBlocked()) { lockType = ChannelBannerView.LOCK_CHANNEL_INFO; } else if (mTvView.isContentBlocked() || (mMainActivity.getParentalControlSettings().isParentalControlsEnabled() && !mTvView.isVideoOrAudioAvailable())) { // If the parental control is enabled, do not show the program detail until the // video becomes available. lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL; } // If lock type is not changed, we don't need to update channel banner by parental // control. int previousLockType = mChannelBannerView.setLockType(lockType); if (previousLockType == lockType && reason == UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK) { return; } else if (reason == UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO) { mChannelBannerView.updateStreamInfo(mTvView); // If parental control is enabled, we shows program description when the video is // available, instead of tuning. Therefore we need to check it here if the program // description is previously hidden by parental control. if (previousLockType == ChannelBannerView.LOCK_PROGRAM_DETAIL && lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) { mChannelBannerView.updateViews(false); } } else if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mMainActivity) && reason == UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH) { mChannelBannerView.updateChannelSignalStrengthView( mTvView.getChannelSignalStrength()); } else { mChannelBannerView.updateViews( reason == UPDATE_CHANNEL_BANNER_REASON_TUNE || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST); } } boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST); if (needToShowBanner && !mMainActivity.willShowOverlayUiWhenResume() && getCurrentDialog() == null && !isSetupFragmentActive() && !isNewSourcesFragmentActive()) { if (mChannelTuner.getCurrentChannel() == null) { mChannelBannerHiddenBySideFragment = false; } else if (getSideFragmentManager().isActive()) { mChannelBannerHiddenBySideFragment = true; } else { mChannelBannerHiddenBySideFragment = false; showBanner(); } } } @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; } } private void onOverlayOpened(@TvOverlayType int overlayType) { if (DEBUG) Log.d(TAG, "Overlay opened: " + toBinaryString(overlayType)); mOpenedOverlays |= overlayType; if (DEBUG) Log.d(TAG, "Opened overlays: " + toBinaryString(mOpenedOverlays)); mHandler.removeMessages(MSG_OVERLAY_CLOSED); mMainActivity.updateKeyInputFocus(); } private void onOverlayClosed(@TvOverlayType int overlayType) { if (DEBUG) Log.d(TAG, "Overlay closed: " + toBinaryString(overlayType)); mOpenedOverlays &= ~overlayType; if (DEBUG) Log.d(TAG, "Opened overlays: " + 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); } } /** * Shows the channel banner if it was hidden from the side fragment. * *

When the side fragment is visible, showing the channel banner should be put off until the * side fragment is closed even though the channel changes. */ private void showChannelBannerIfHiddenBySideFragment() { if (mChannelBannerHiddenBySideFragment) { updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW); } } private String toBinaryString(int value) { return String.format("0b%" + NUM_OVERLAY_TYPES + "s", Integer.toBinaryString(value)) .replace(' ', '0'); } 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. */ 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; } if (mTvView.isScreenBlocked()) { // Do not handle media key when screen is blocked. 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_STOP: 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: case KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD: timeShiftManager.jumpToPrevious(); showMenu(Menu.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS); break; case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD: 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)) { showKeypadChannelSwitch(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: case KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD: case KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD: case KeyEvent.KEYCODE_MEDIA_STOP: return true; } return false; } private static class TvOverlayHandler extends WeakHandler { 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; } } } private class PendingDialogAction { private final String mTag; private final SafeDismissDialogFragment mDialog; private final boolean mKeepSidePanelHistory; private final boolean mKeepProgramGuide; PendingDialogAction( String tag, SafeDismissDialogFragment dialog, boolean keepSidePanelHistory, boolean keepProgramGuide) { mTag = tag; mDialog = dialog; mKeepSidePanelHistory = keepSidePanelHistory; mKeepProgramGuide = keepProgramGuide; } void run() { showDialogFragment(mTag, mDialog, mKeepSidePanelHistory, mKeepProgramGuide); } } }