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