TvActivity.java revision 545b3ba127213dbe4e388cf99dc7f97b66a46694
1/* 2 * Copyright (C) 2014 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 17package com.android.tv; 18 19import android.app.Activity; 20import android.app.DialogFragment; 21import android.app.Fragment; 22import android.app.FragmentManager; 23import android.app.FragmentTransaction; 24import android.content.ContentUris; 25import android.content.Context; 26import android.content.Intent; 27import android.content.SharedPreferences; 28import android.graphics.Point; 29import android.media.AudioManager; 30import android.media.tv.TvInputInfo; 31import android.media.tv.TvInputManager; 32import android.net.Uri; 33import android.os.Bundle; 34import android.os.Handler; 35import android.os.Message; 36import android.preference.PreferenceManager; 37import android.text.TextUtils; 38import android.util.Log; 39import android.view.Display; 40import android.view.GestureDetector; 41import android.view.GestureDetector.SimpleOnGestureListener; 42import android.view.Gravity; 43import android.view.InputEvent; 44import android.view.KeyEvent; 45import android.view.MotionEvent; 46import android.view.View; 47import android.view.ViewGroup; 48import android.view.animation.Animation; 49import android.view.animation.AnimationUtils; 50import android.widget.FrameLayout; 51import android.widget.LinearLayout; 52import android.widget.Toast; 53 54import com.android.tv.data.DisplayMode; 55import com.android.tv.data.Channel; 56import com.android.tv.data.ChannelMap; 57import com.android.tv.data.StreamInfo; 58import com.android.tv.dialog.EditInputDialogFragment; 59import com.android.tv.dialog.RecentlyWatchedDialogFragment; 60import com.android.tv.input.TisTvInput; 61import com.android.tv.input.TvInput; 62import com.android.tv.input.UnifiedTvInput; 63import com.android.tv.ui.DisplayModeOptionFragment; 64import com.android.tv.ui.BaseSideFragment; 65import com.android.tv.ui.ChannelBannerView; 66import com.android.tv.ui.ClosedCaptionOptionFragment; 67import com.android.tv.ui.EditChannelsFragment; 68import com.android.tv.ui.InputPickerFragment; 69import com.android.tv.ui.MainMenuView; 70import com.android.tv.ui.ChannelNumberView; 71import com.android.tv.ui.PipLocationFragment; 72import com.android.tv.ui.SidePanelContainer; 73import com.android.tv.ui.SimpleGuideFragment; 74import com.android.tv.ui.TunableTvView; 75import com.android.tv.ui.TunableTvView.OnTuneListener; 76import com.android.tv.util.TvInputManagerHelper; 77import com.android.tv.util.TvSettings; 78import com.android.tv.util.Utils; 79 80import java.util.HashSet; 81 82/** 83 * The main activity for demonstrating TV app. 84 */ 85public class TvActivity extends Activity implements AudioManager.OnAudioFocusChangeListener { 86 // STOPSHIP: Turn debugging off 87 private static final boolean DEBUG = true; 88 private static final boolean USE_DEBUG_KEYS = true; 89 private static final String TAG = "TvActivity"; 90 91 private static final int MSG_START_TV_RETRY = 1; 92 93 private static final int DURATION_SHOW_CHANNEL_BANNER = 8000; 94 private static final int DURATION_SHOW_CONTROL_GUIDE = 1000; 95 private static final int DURATION_SHOW_MAIN_MENU = 5000; 96 private static final int DURATION_SHOW_SIDE_FRAGMENT = 60000; 97 private static final float AUDIO_MAX_VOLUME = 1.0f; 98 private static final float AUDIO_MIN_VOLUME = 0.0f; 99 private static final float AUDIO_DUCKING_VOLUME = 0.3f; 100 // Wait for 3 seconds 101 private static final int START_TV_MAX_RETRY = 12; 102 private static final int START_TV_RETRY_INTERVAL = 250; 103 104 private static final int SIDE_FRAGMENT_TAG_SHOW = 0; 105 private static final int SIDE_FRAGMENT_TAG_HIDE = 1; 106 private static final int SIDE_FRAGMENT_TAG_RESET = 2; 107 108 private static final int[] KEYCODE_BLACKLIST = { 109 KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_CHANNEL_UP, KeyEvent.KEYCODE_CHANNEL_DOWN, 110 }; 111 112 private static final int REQUEST_START_SETUP_ACTIIVTY = 0; 113 private static final int REQUEST_START_SETTINGS_ACTIIVTY = 1; 114 115 private static final HashSet<String> AVAILABLE_DIALOG_TAGS = new HashSet<String>(); 116 117 private TunableTvView mTvView; 118 private LinearLayout mControlGuide; 119 private MainMenuView mMainMenuView; 120 private ChannelBannerView mChannelBanner; 121 private ChannelNumberView mChannelNumberView; 122 private SidePanelContainer mSidePanelContainer; 123 private HideRunnable mHideChannelBanner; 124 private HideRunnable mHideControlGuide; 125 private HideRunnable mHideMainMenu; 126 private HideRunnable mHideSideFragment; 127 private int mShortAnimationDuration; 128 private int mDisplayWidth; 129 private GestureDetector mGestureDetector; 130 private ChannelMap mChannelMap; 131 private long mInitChannelId; 132 private String mInitTvInputId; 133 private boolean mChildActivityCanceled; 134 135 private TvInput mTvInputForSetup; 136 private TvInputManagerHelper mTvInputManagerHelper; 137 private AudioManager mAudioManager; 138 private int mAudioFocusStatus; 139 private boolean mTunePendding; 140 private boolean mPipEnabled; 141 private long mPipChannelId; 142 private boolean mDebugNonFullSizeScreen; 143 private boolean mActivityResumed; 144 private boolean mSilenceRequired; 145 private boolean mUseKeycodeBlacklist; 146 private boolean mIsShy = true; 147 148 private boolean mIsClosedCaptionEnabled; 149 private int mDisplayMode; 150 private int mPipLocation; 151 private SharedPreferences mSharedPreferences; 152 153 private SimpleGuideFragment mSimpleGuideFragment; 154 155 static { 156 AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG); 157 AVAILABLE_DIALOG_TAGS.add(EditInputDialogFragment.DIALOG_TAG); 158 } 159 160 // PIP is used for debug/verification of multiple sessions rather than real PIP feature. 161 // When PIP is enabled, the same channel as mTvView is tuned. 162 private TunableTvView mPipView; 163 164 private final Handler mHandler = new Handler() { 165 @Override 166 public void handleMessage(Message msg) { 167 if (msg.what == MSG_START_TV_RETRY) { 168 Object[] arg = (Object[]) msg.obj; 169 TvInput input = (TvInput) arg[0]; 170 long channelId = (Long) arg[1]; 171 int retryCount = msg.arg1; 172 startTvIfAvailableOrRetry(input, channelId, retryCount); 173 } 174 } 175 }; 176 177 @Override 178 protected void onCreate(Bundle savedInstanceState) { 179 super.onCreate(savedInstanceState); 180 181 setContentView(R.layout.activity_tv); 182 mTvView = (TunableTvView) findViewById(R.id.tv_view); 183 mTvView.setOnUnhandledInputEventListener(new TunableTvView.OnUnhandledInputEventListener() { 184 @Override 185 public boolean onUnhandledInputEvent(InputEvent event) { 186 if (event instanceof KeyEvent) { 187 KeyEvent keyEvent = (KeyEvent) event; 188 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.isLongPress()) { 189 if (onKeyLongPress(keyEvent.getKeyCode(), keyEvent)) { 190 return true; 191 } 192 } 193 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 194 return onKeyUp(keyEvent.getKeyCode(), keyEvent); 195 } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { 196 return onKeyDown(keyEvent.getKeyCode(), keyEvent); 197 } 198 } else if (event instanceof MotionEvent) { 199 MotionEvent motionEvent = (MotionEvent) event; 200 if (motionEvent.isTouchEvent()) { 201 return onTouchEvent(motionEvent); 202 } 203 } 204 return false; 205 } 206 }); 207 mPipView = (TunableTvView) findViewById(R.id.pip_view); 208 mPipView.setPip(true); 209 210 mControlGuide = (LinearLayout) findViewById(R.id.control_guide); 211 mChannelBanner = (ChannelBannerView) findViewById(R.id.channel_banner); 212 mMainMenuView = (MainMenuView) findViewById(R.id.main_menu); 213 mSidePanelContainer = (SidePanelContainer) findViewById(R.id.right_panel); 214 mMainMenuView.setTvActivity(this); 215 mChannelNumberView = (ChannelNumberView) findViewById(R.id.channel_number_view); 216 mChannelNumberView.setTvActivity(this); 217 218 // Initially hide the channel banner and the control guide. 219 mChannelBanner.setVisibility(View.GONE); 220 mMainMenuView.setVisibility(View.GONE); 221 mControlGuide.setVisibility(View.GONE); 222 mSidePanelContainer.setVisibility(View.GONE); 223 mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_RESET); 224 225 mHideControlGuide = new HideRunnable(mControlGuide, DURATION_SHOW_CONTROL_GUIDE); 226 mHideChannelBanner = new HideRunnable(mChannelBanner, DURATION_SHOW_CHANNEL_BANNER); 227 mHideMainMenu = new HideRunnable(mMainMenuView, DURATION_SHOW_MAIN_MENU, 228 new Runnable() { 229 @Override 230 public void run() { 231 if (mPipEnabled) { 232 mPipView.setVisibility(View.INVISIBLE); 233 } 234 } 235 }, 236 new Runnable() { 237 @Override 238 public void run() { 239 if (mPipEnabled && mActivityResumed) { 240 mPipView.setVisibility(View.VISIBLE); 241 } 242 } 243 }); 244 mHideSideFragment = new HideRunnable(mSidePanelContainer, DURATION_SHOW_SIDE_FRAGMENT, null, 245 new Runnable() { 246 @Override 247 public void run() { 248 resetSideFragment(); 249 } 250 }); 251 252 mShortAnimationDuration = getResources().getInteger( 253 android.R.integer.config_shortAnimTime); 254 255 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 256 mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; 257 Display display = getWindowManager().getDefaultDisplay(); 258 Point size = new Point(); 259 display.getSize(size); 260 mDisplayWidth = size.x; 261 262 mGestureDetector = new GestureDetector(this, new SimpleOnGestureListener() { 263 static final float CONTROL_MARGIN = 0.2f; 264 final float mLeftMargin = mDisplayWidth * CONTROL_MARGIN; 265 final float mRightMargin = mDisplayWidth * (1 - CONTROL_MARGIN); 266 267 @Override 268 public boolean onDown(MotionEvent event) { 269 if (DEBUG) Log.d(TAG, "onDown: " + event.toString()); 270 if (mChannelMap == null) { 271 return false; 272 } 273 274 mHideControlGuide.showAndHide(); 275 276 if (event.getX() <= mLeftMargin) { 277 channelDown(); 278 return true; 279 } else if (event.getX() >= mRightMargin) { 280 channelUp(); 281 return true; 282 } 283 return false; 284 } 285 286 @Override 287 public boolean onSingleTapUp(MotionEvent event) { 288 if (mChannelMap == null) { 289 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 290 return true; 291 } 292 293 if (event.getX() > mLeftMargin && event.getX() < mRightMargin) { 294 displayMainMenu(true); 295 return true; 296 } 297 return false; 298 } 299 }); 300 301 TvInputManager manager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); 302 mTvInputManagerHelper = new TvInputManagerHelper(manager); 303 mTvInputManagerHelper.start(); 304 305 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 306 restoreClosedCaptionEnabled(); 307 restoreDisplayMode(); 308 restorePipLocation(); 309 onNewIntent(getIntent()); 310 } 311 312 @Override 313 protected void onNewIntent(Intent intent) { 314 // Handle the passed key press, if any. Note that only the key codes that are currently 315 // handled in the TV app will be handled via Intent. 316 // TODO: Consider defining a separate intent filter as passing data of mime type 317 // vnd.android.cursor.item/channel isn't really necessary here. 318 int keyCode = intent.getIntExtra(Utils.EXTRA_KEYCODE, KeyEvent.KEYCODE_UNKNOWN); 319 if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { 320 if (DEBUG) Log.d(TAG, "Got an intent with keycode: " + keyCode); 321 KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keyCode); 322 onKeyUp(keyCode, event); 323 return; 324 } 325 326 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 327 // In case the channel is given explicitly, use it. 328 mInitChannelId = ContentUris.parseId(intent.getData()); 329 } else { 330 mInitChannelId = Channel.INVALID_ID; 331 } 332 } 333 334 @Override 335 protected void onStart() { 336 super.onStart(); 337 } 338 339 @Override 340 protected void onResume() { 341 super.onResume(); 342 mActivityResumed = true; 343 mSilenceRequired = false; 344 mTvInputManagerHelper.update(); 345 if (mTvInputManagerHelper.getTvInputSize() == 0) { 346 Toast.makeText(this, R.string.no_input_device_found, Toast.LENGTH_SHORT).show(); 347 // TODO: Direct the user to a Play Store landing page for TvInputService apps. 348 return; 349 } 350 boolean tvStarted = false; 351 if (mInitTvInputId != null) { 352 TvInputInfo inputInfo = mTvInputManagerHelper.getTvInputInfo(mInitTvInputId); 353 if (inputInfo != null) { 354 startTvIfAvailableOrRetry(new TisTvInput(mTvInputManagerHelper, inputInfo, this), 355 Channel.INVALID_ID, 0); 356 tvStarted = true; 357 } 358 } 359 if (!tvStarted) { 360 startTv(mInitChannelId); 361 } 362 mInitChannelId = Channel.INVALID_ID; 363 mInitTvInputId = null; 364 if (mPipEnabled) { 365 if (!mPipView.isPlaying()) { 366 startPip(); 367 } else if (!mPipView.isShown()) { 368 mPipView.setVisibility(View.VISIBLE); 369 } 370 } 371 } 372 373 @Override 374 protected void onPause() { 375 hideOverlays(true, true, true); 376 if (mPipEnabled) { 377 mPipView.setVisibility(View.INVISIBLE); 378 } 379 mActivityResumed = false; 380 super.onPause(); 381 } 382 383 private void startTv(long channelId) { 384 if (mTvView.isPlaying()) { 385 // TV has already started. 386 if (channelId == Channel.INVALID_ID) { 387 // Simply adjust the volume without tune. 388 setVolumeByAudioFocusStatus(); 389 return; 390 } 391 Uri channelUri = mChannelMap.getCurrentChannelUri(); 392 if (channelUri != null && ContentUris.parseId(channelUri) == channelId) { 393 // The requested channel is already tuned. 394 setVolumeByAudioFocusStatus(); 395 return; 396 } 397 stopTv(); 398 } 399 400 if (channelId == Channel.INVALID_ID) { 401 // If any initial channel id is not given, remember the last channel the user watched. 402 channelId = Utils.getLastWatchedChannelId(this); 403 } 404 if (channelId == Channel.INVALID_ID) { 405 // If failed to pick a channel, try a different input. 406 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 407 return; 408 } 409 String inputId = Utils.getInputIdForChannel(this, channelId); 410 if (TextUtils.isEmpty(inputId)) { 411 // If the channel is invalid, try to use the last selected physical tv input. 412 inputId = Utils.getLastSelectedPhysInputId(this); 413 if (TextUtils.isEmpty(inputId)) { 414 // If failed to determine the input for that channel, try a different input. 415 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 416 return; 417 } 418 } 419 TvInputInfo inputInfo = mTvInputManagerHelper.getTvInputInfo(inputId); 420 if (inputInfo == null) { 421 // TODO: if the last selected TV input is uninstalled, getLastWatchedChannelId 422 // should return Channel.INVALID_ID. 423 Log.w(TAG, "Input (id=" + inputId + ") doesn't exist"); 424 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 425 return; 426 } 427 String lastSelectedInputId = Utils.getLastSelectedInputId(this); 428 TvInput input; 429 if (UnifiedTvInput.ID.equals(lastSelectedInputId)) { 430 input = new UnifiedTvInput(mTvInputManagerHelper, this); 431 } else { 432 input = new TisTvInput(mTvInputManagerHelper, inputInfo, this); 433 } 434 startTvIfAvailableOrRetry(input, channelId, 0); 435 } 436 437 private void startTvIfAvailableOrRetry(TvInput input, long channelId, int retryCount) { 438 if (!input.isAvailable()) { 439 if (retryCount >= START_TV_MAX_RETRY) { 440 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 441 return; 442 } 443 if (DEBUG) Log.d(TAG, "Retry start TV (retryCount=" + retryCount + ")"); 444 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TV_RETRY, 445 retryCount + 1, 0, new Object[]{input, channelId}), 446 START_TV_RETRY_INTERVAL); 447 return; 448 } 449 startTv(input, channelId); 450 } 451 452 @Override 453 protected void onStop() { 454 if (DEBUG) Log.d(TAG, "onStop()"); 455 hideOverlays(true, true, true, false); 456 mHandler.removeMessages(MSG_START_TV_RETRY); 457 stopTv(); 458 stopPip(); 459 super.onStop(); 460 } 461 462 public void onInputPicked(TvInput input) { 463 if (input.equals(getSelectedTvInput())) { 464 // Nothing has changed thus nothing to do. 465 return; 466 } 467 if (!mTvInputManagerHelper.isAvailable(input.getId())) { 468 String message = String.format(getString(R.string.input_is_not_available)); 469 Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); 470 return; 471 } 472 if (!input.hasChannel(false)) { 473 mTvInputForSetup = null; 474 if (!startSetupActivity(input)) { 475 String message = String.format( 476 getString(R.string.empty_channel_tvinput_and_no_setup_activity), 477 input.getDisplayName()); 478 Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); 479 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 480 } 481 return; 482 } 483 484 stopTv(); 485 startTvWithLastWatchedChannel(input); 486 } 487 488 public TvInputManagerHelper getTvInputManagerHelper() { 489 return mTvInputManagerHelper; 490 } 491 492 public TvInput getSelectedTvInput() { 493 return mChannelMap == null ? null : mChannelMap.getTvInput(); 494 } 495 496 public void showEditChannelsFragment(int initiator) { 497 showSideFragment(new EditChannelsFragment(mChannelMap.getChannelList(false)), initiator); 498 } 499 500 public boolean startSetupActivity() { 501 if (getSelectedTvInput() == null) { 502 return false; 503 } 504 return startSetupActivity(getSelectedTvInput()); 505 } 506 507 public boolean startSetupActivity(TvInput input) { 508 Intent intent = input.getIntentForSetupActivity(); 509 if (intent == null) { 510 return false; 511 } 512 startActivityForResult(intent, REQUEST_START_SETUP_ACTIIVTY); 513 mTvInputForSetup = input; 514 mInitTvInputId = null; 515 stopTv(); 516 return true; 517 } 518 519 public boolean startSettingsActivity() { 520 TvInput input = getSelectedTvInput(); 521 if (input == null) { 522 Log.w(TAG, "There is no selected TV input during startSettingsActivity"); 523 return false; 524 } 525 Intent intent = input.getIntentForSettingsActivity(); 526 if (intent == null) { 527 return false; 528 } 529 startActivityForResult(intent, REQUEST_START_SETTINGS_ACTIIVTY); 530 return true; 531 } 532 533 public void showSimpleGuide(int initiator) { 534 mSimpleGuideFragment = new SimpleGuideFragment(this, mChannelMap); 535 showSideFragment(mSimpleGuideFragment, initiator); 536 } 537 538 public void showInputPicker(int initiator) { 539 showSideFragment(new InputPickerFragment(), initiator); 540 } 541 542 public void showDisplayModeOption(int initiator) { 543 showSideFragment(new DisplayModeOptionFragment(), initiator); 544 } 545 546 public void showClosedCaptionOption(int initiator) { 547 showSideFragment(new ClosedCaptionOptionFragment(), initiator); 548 } 549 550 public void showPipLocationOption(int initiator) { 551 showSideFragment(new PipLocationFragment(), initiator); 552 } 553 554 public void showSideFragment(Fragment f, int initiator) { 555 mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_SHOW); 556 mSidePanelContainer.setKeyDispatchable(true); 557 558 Bundle bundle = new Bundle(); 559 bundle.putInt(BaseSideFragment.KEY_INITIATOR, initiator); 560 f.setArguments(bundle); 561 FragmentTransaction ft = getFragmentManager().beginTransaction(); 562 ft.add(R.id.right_panel, f); 563 ft.addToBackStack(null); 564 ft.commit(); 565 566 mHideSideFragment.showAndHide(); 567 } 568 569 public void popFragmentBackStack() { 570 if (getFragmentManager().getBackStackEntryCount() > 1) { 571 getFragmentManager().popBackStack(); 572 } else if (getFragmentManager().getBackStackEntryCount() == 1 573 && (Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_RESET) { 574 if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_SHOW) { 575 mSidePanelContainer.setKeyDispatchable(false); 576 mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_HIDE); 577 mHideSideFragment.hideImmediately(true); 578 } else { 579 // It is during fade-out animation. 580 } 581 } else { 582 getFragmentManager().popBackStack(); 583 } 584 } 585 586 public void onSideFragmentCanceled(int initiator) { 587 if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_RESET) { 588 return; 589 } 590 if (initiator == BaseSideFragment.INITIATOR_MENU) { 591 displayMainMenu(false); 592 } 593 } 594 595 private void resetSideFragment() { 596 while (true) { 597 if (!getFragmentManager().popBackStackImmediate()) { 598 break; 599 } 600 } 601 mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_RESET); 602 } 603 604 @Override 605 public void onActivityResult(int requestCode, int resultCode, Intent data) { 606 if (resultCode == Activity.RESULT_OK) { 607 switch (requestCode) { 608 case REQUEST_START_SETUP_ACTIIVTY: 609 if (mTvInputForSetup != null) { 610 mInitTvInputId = mTvInputForSetup.getId(); 611 } 612 break; 613 614 default: 615 //TODO: Handle failure of setup. 616 } 617 } else { 618 mChildActivityCanceled = true; 619 } 620 mTvInputForSetup = null; 621 } 622 623 @Override 624 public boolean dispatchKeyEvent(KeyEvent event) { 625 if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); 626 if (mMainMenuView.isShown() || getFragmentManager().getBackStackEntryCount() > 0) { 627 return super.dispatchKeyEvent(event); 628 } 629 int eventKeyCode = event.getKeyCode(); 630 for (int keycode : KEYCODE_BLACKLIST) { 631 if (keycode == eventKeyCode) { 632 return super.dispatchKeyEvent(event); 633 } 634 } 635 return dispatchKeyEventToSession(event) || super.dispatchKeyEvent(event); 636 } 637 638 @Override 639 public void onAudioFocusChange(int focusChange) { 640 mAudioFocusStatus = focusChange; 641 setVolumeByAudioFocusStatus(); 642 } 643 644 private void setVolumeByAudioFocusStatus() { 645 if (mTvView.isPlaying()) { 646 switch (mAudioFocusStatus) { 647 case AudioManager.AUDIOFOCUS_GAIN: 648 if (!mSilenceRequired) { 649 mTvView.setStreamVolume(AUDIO_MAX_VOLUME); 650 if (isShyModeSet()) { 651 setShynessMode(false); 652 } 653 } else { 654 mTvView.setStreamVolume(AUDIO_MIN_VOLUME); 655 } 656 break; 657 case AudioManager.AUDIOFOCUS_LOSS: 658 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 659 mTvView.setStreamVolume(AUDIO_MIN_VOLUME); 660 if (!mActivityResumed) { 661 mSilenceRequired = true; 662 } 663 break; 664 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 665 if (!mSilenceRequired) { 666 mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME); 667 } else { 668 mTvView.setStreamVolume(AUDIO_MIN_VOLUME); 669 } 670 break; 671 } 672 } 673 // When the activity loses the audio focus, set the Shy mode regardless of the play status. 674 if (mAudioFocusStatus == AudioManager.AUDIOFOCUS_LOSS || 675 mAudioFocusStatus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { 676 if (!isShyModeSet()) { 677 setShynessMode(true); 678 } 679 } 680 } 681 682 private void startTvWithLastWatchedChannel(TvInput input) { 683 long channelId = Utils.getLastWatchedChannelId(TvActivity.this, input.getId()); 684 startTv(input, channelId); 685 } 686 687 private void startTv(TvInput input, long channelId) { 688 if (mChannelMap != null) { 689 // TODO: when this case occurs, we should remove the case. 690 Log.w(TAG, "The previous variables are not released in startTv"); 691 stopTv(); 692 } 693 694 mMainMenuView.setChannelMap(null); 695 mChannelNumberView.setChannels(null); 696 int result = mAudioManager.requestAudioFocus(TvActivity.this, 697 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 698 mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ? 699 AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS; 700 701 // Prepare a new channel map for the current input. 702 mChannelMap = input.buildChannelMap(this, channelId, mOnChannelsLoadFinished); 703 mTvView.start(mTvInputManagerHelper); 704 setVolumeByAudioFocusStatus(); 705 tune(); 706 } 707 708 private void stopTv() { 709 if (mTvView.isPlaying()) { 710 mTvView.stop(); 711 mAudioManager.abandonAudioFocus(this); 712 } 713 if (mChannelMap != null) { 714 mMainMenuView.setChannelMap(null); 715 mChannelNumberView.setChannels(null); 716 mChannelMap.close(); 717 mChannelMap = null; 718 } 719 mTunePendding = false; 720 721 if (!isShyModeSet()) { 722 setShynessMode(true); 723 } 724 } 725 726 private boolean isPlaying() { 727 return mTvView.isPlaying() && mTvView.getCurrentChannelId() != Channel.INVALID_ID; 728 } 729 730 private void startPip() { 731 if (mPipChannelId == Channel.INVALID_ID) { 732 Log.w(TAG, "PIP channel id is an invalid id."); 733 return; 734 } 735 if (DEBUG) Log.d(TAG, "startPip()"); 736 mPipView.start(mTvInputManagerHelper); 737 boolean success = mPipView.tuneTo(mPipChannelId, new OnTuneListener() { 738 @Override 739 public void onUnexpectedStop(long channelId) { 740 Log.w(TAG, "The PIP is Unexpectedly stopped"); 741 enablePipView(false); 742 } 743 744 @Override 745 public void onTuned(boolean success, long channelId) { 746 if (!success) { 747 Log.w(TAG, "Fail to start the PIP during channel tunning"); 748 enablePipView(false); 749 } else { 750 mPipView.setVisibility(View.VISIBLE); 751 } 752 } 753 754 @Override 755 public void onStreamInfoChanged(StreamInfo info) { 756 // Do nothing. 757 } 758 759 @Override 760 public void onChannelChanged(Uri channel) { 761 // Do nothing. 762 } 763 }); 764 if (!success) { 765 Log.w(TAG, "Fail to start the PIP"); 766 return; 767 } 768 mPipView.setStreamVolume(AUDIO_MIN_VOLUME); 769 } 770 771 private void stopPip() { 772 if (DEBUG) Log.d(TAG, "stopPip"); 773 if (mPipView.isPlaying()) { 774 mPipView.setVisibility(View.INVISIBLE); 775 mPipView.stop(); 776 } 777 } 778 779 private final Runnable mOnChannelsLoadFinished = new Runnable() { 780 @Override 781 public void run() { 782 if (mTunePendding) { 783 tune(); 784 } 785 786 mChannelNumberView.setChannels( 787 (mChannelMap == null) ? null : mChannelMap.getChannelList(false)); 788 mMainMenuView.setChannelMap(mChannelMap); 789 } 790 }; 791 792 private void tune() { 793 if (DEBUG) Log.d(TAG, "tune()"); 794 // Prerequisites to be able to tune. 795 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 796 if (DEBUG) Log.d(TAG, "Channel map not ready"); 797 mTunePendding = true; 798 return; 799 } 800 mTunePendding = false; 801 long channelId = mChannelMap.getCurrentChannelId(); 802 final String inputId = mChannelMap.getTvInput().getId(); 803 if (channelId == Channel.INVALID_ID) { 804 stopTv(); 805 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 806 return; 807 } 808 809 mTvView.tuneTo(channelId, new OnTuneListener() { 810 @Override 811 public void onUnexpectedStop(long channelId) { 812 stopTv(); 813 startTv(Channel.INVALID_ID); 814 } 815 816 @Override 817 public void onTuned(boolean success, long channelId) { 818 if (!success) { 819 Log.w(TAG, "Failed to tune to channel " + channelId); 820 // TODO: show something to user about this error. 821 } else { 822 Utils.setLastWatchedChannelId(TvActivity.this, inputId, 823 mTvView.getCurrentTvInputInfo().getId(), channelId); 824 } 825 } 826 827 @Override 828 public void onStreamInfoChanged(StreamInfo info) { 829 updateChannelBanner(false); 830 applyDisplayMode(info.getVideoWidth(), info.getVideoHeight()); 831 } 832 833 @Override 834 public void onChannelChanged(Uri channel) { 835 // TODO: update {@code mChannelMap} and the banner. 836 } 837 }); 838 updateChannelBanner(true); 839 if (mChildActivityCanceled) { 840 displayMainMenu(false); 841 mChildActivityCanceled = false; 842 } 843 if (isShyModeSet()) { 844 setShynessMode(false); 845 // TODO: Set the shy mode to true when tune() fails. 846 } 847 } 848 849 public void hideOverlays(boolean hideMainMenu, boolean hideChannelBanner, 850 boolean hideSidePanel) { 851 hideOverlays(hideMainMenu, hideChannelBanner, hideSidePanel, true); 852 } 853 854 public void hideOverlays(boolean hideMainMenu, boolean hideChannelBanner, 855 boolean hideSidePanel, boolean withAnimation) { 856 if (hideMainMenu) { 857 mHideMainMenu.hideImmediately(withAnimation); 858 } 859 if (hideChannelBanner) { 860 mHideChannelBanner.hideImmediately(withAnimation); 861 } 862 if (hideSidePanel) { 863 if ((Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_SHOW) { 864 return; 865 } 866 mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_HIDE); 867 mHideSideFragment.hideImmediately(withAnimation); 868 } 869 } 870 871 private void applyDisplayMode(int videoWidth, int videoHeight) { 872 int decorViewWidth = getWindow().getDecorView().getWidth(); 873 int decorViewHeight = getWindow().getDecorView().getHeight(); 874 ViewGroup.LayoutParams layoutParams = mTvView.getLayoutParams(); 875 int displayMode = mDisplayMode; 876 double decorViewRatio = 0; 877 double videoRatio = 0; 878 if (decorViewWidth <= 0 || decorViewHeight <= 0 || videoWidth <= 0 || videoHeight <=0) { 879 displayMode = DisplayMode.MODE_FULL; 880 Log.w(TAG, "Some resolution info is missing during applyDisplayMode. (" 881 + "decorViewWidth=" + decorViewWidth + ", decorViewHeight=" + decorViewHeight 882 + ", width=" + videoWidth + ", height=" + videoHeight + ")"); 883 } else { 884 decorViewRatio = (double) decorViewWidth / decorViewHeight; 885 videoRatio = (double) videoWidth / videoHeight; 886 } 887 if (displayMode == DisplayMode.MODE_FULL) { 888 layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 889 layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 890 } else if (displayMode == DisplayMode.MODE_ZOOM) { 891 if (videoRatio < decorViewRatio) { 892 // Y axis will be clipped. 893 layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 894 layoutParams.height = (int) (decorViewWidth / videoRatio); 895 } else { 896 // X axis will be clipped. 897 layoutParams.width = (int) (decorViewHeight * videoRatio); 898 layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 899 } 900 } else { 901 // MODE_NORMAL (default value) 902 if (videoRatio < decorViewRatio) { 903 // X axis has black area. 904 layoutParams.width = (int) (decorViewHeight * videoRatio); 905 layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 906 } else { 907 // Y axis has black area. 908 layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 909 layoutParams.height = (int) (decorViewWidth / videoRatio); 910 } 911 } 912 mTvView.setLayoutParams(layoutParams); 913 } 914 915 private void updateChannelBanner(final boolean showBanner) { 916 runOnUiThread(new Runnable() { 917 @Override 918 public void run() { 919 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 920 return; 921 } 922 923 mChannelBanner.updateViews(mChannelMap, mTvView); 924 if (showBanner) { 925 mHideChannelBanner.showAndHide(); 926 } 927 } 928 }); 929 } 930 931 private void displayMainMenu(final boolean resetSelectedItemPosition) { 932 runOnUiThread(new Runnable() { 933 @Override 934 public void run() { 935 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 936 return; 937 } 938 939 if (!mMainMenuView.isShown() && resetSelectedItemPosition) { 940 mMainMenuView.resetSelectedItemPosition(); 941 } 942 mHideMainMenu.showAndHide(); 943 } 944 }); 945 } 946 947 public void showRecentlyWatchedDialog() { 948 showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG, 949 new RecentlyWatchedDialogFragment()); 950 } 951 952 @Override 953 protected void onSaveInstanceState(Bundle outState) { 954 // Do not save instance state because restoring instance state when TV app died 955 // unexpectedly can cause some problems like initializing fragments duplicately and 956 // accessing resource before it is initialzed. 957 } 958 959 @Override 960 protected void onDestroy() { 961 if (DEBUG) Log.d(TAG, "onDestroy()"); 962 mTvInputManagerHelper.stop(); 963 super.onDestroy(); 964 } 965 966 @Override 967 public boolean onKeyDown(int keyCode, KeyEvent event) { 968 if (mMainMenuView.getVisibility() == View.VISIBLE 969 || getFragmentManager().getBackStackEntryCount() > 0) { 970 return super.onKeyDown(keyCode, event); 971 } 972 if (mChannelNumberView.isShown()) { 973 return mChannelNumberView.onKeyDown(keyCode, event); 974 } 975 if (mChannelMap == null) { 976 return false; 977 } 978 switch (keyCode) { 979 case KeyEvent.KEYCODE_CHANNEL_UP: 980 case KeyEvent.KEYCODE_DPAD_UP: 981 channelUp(); 982 return true; 983 984 case KeyEvent.KEYCODE_CHANNEL_DOWN: 985 case KeyEvent.KEYCODE_DPAD_DOWN: 986 channelDown(); 987 return true; 988 } 989 return super.onKeyDown(keyCode, event); 990 } 991 992 @Override 993 public boolean onKeyUp(int keyCode, KeyEvent event) { 994 if (getFragmentManager().getBackStackEntryCount() > 0) { 995 if (keyCode == KeyEvent.KEYCODE_BACK) { 996 popFragmentBackStack(); 997 return true; 998 } 999 return super.onKeyUp(keyCode, event); 1000 } 1001 if (mMainMenuView.isShown() || mChannelBanner.isShown()) { 1002 if (keyCode == KeyEvent.KEYCODE_BACK) { 1003 hideOverlays(true, true, false); 1004 return true; 1005 } 1006 if (mMainMenuView.isShown()) { 1007 return super.onKeyUp(keyCode, event); 1008 } 1009 } 1010 if (mChannelNumberView.isShown()) { 1011 if (keyCode == KeyEvent.KEYCODE_BACK) { 1012 mChannelNumberView.hide(); 1013 return true; 1014 } 1015 return mChannelNumberView.onKeyUp(keyCode, event); 1016 } 1017 if (keyCode == KeyEvent.KEYCODE_BACK) { 1018 // When the event is from onUnhandledInputEvent, onBackPressed is not automatically 1019 // called. Therefore, we need to explicitly call onBackPressed(). 1020 onBackPressed(); 1021 return true; 1022 } 1023 1024 if (mHandler.hasMessages(MSG_START_TV_RETRY)) { 1025 // Ignore key events during startTv retry. 1026 return true; 1027 } 1028 if (mChannelMap == null) { 1029 switch (keyCode) { 1030 case KeyEvent.KEYCODE_H: 1031 showRecentlyWatchedDialog(); 1032 return true; 1033 case KeyEvent.KEYCODE_TV_INPUT: 1034 case KeyEvent.KEYCODE_I: 1035 case KeyEvent.KEYCODE_CHANNEL_UP: 1036 case KeyEvent.KEYCODE_DPAD_UP: 1037 case KeyEvent.KEYCODE_CHANNEL_DOWN: 1038 case KeyEvent.KEYCODE_DPAD_DOWN: 1039 case KeyEvent.KEYCODE_NUMPAD_ENTER: 1040 case KeyEvent.KEYCODE_DPAD_CENTER: 1041 case KeyEvent.KEYCODE_E: 1042 case KeyEvent.KEYCODE_MENU: 1043 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 1044 return true; 1045 } 1046 } else { 1047 if (ChannelNumberView.isChannelNumberKey(keyCode) 1048 && keyCode != KeyEvent.KEYCODE_MINUS) { 1049 mChannelNumberView.show(); 1050 mHideChannelBanner.hideImmediately(true); 1051 return mChannelNumberView.onKeyUp(keyCode, event); 1052 } 1053 switch (keyCode) { 1054 case KeyEvent.KEYCODE_H: 1055 showRecentlyWatchedDialog(); 1056 return true; 1057 1058 case KeyEvent.KEYCODE_TV_INPUT: 1059 case KeyEvent.KEYCODE_I: 1060 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 1061 return true; 1062 1063 case KeyEvent.KEYCODE_DPAD_LEFT: 1064 case KeyEvent.KEYCODE_DPAD_RIGHT: 1065 displayMainMenu(true); 1066 return true; 1067 1068 case KeyEvent.KEYCODE_ENTER: 1069 case KeyEvent.KEYCODE_NUMPAD_ENTER: 1070 case KeyEvent.KEYCODE_E: 1071 case KeyEvent.KEYCODE_DPAD_CENTER: 1072 case KeyEvent.KEYCODE_MENU: 1073 if (event.isCanceled()) { 1074 return true; 1075 } 1076 if (keyCode != KeyEvent.KEYCODE_MENU) { 1077 updateChannelBanner(true); 1078 } 1079 if (keyCode != KeyEvent.KEYCODE_E) { 1080 displayMainMenu(true); 1081 } 1082 return true; 1083 } 1084 } 1085 if (USE_DEBUG_KEYS) { 1086 switch (keyCode) { 1087 case KeyEvent.KEYCODE_W: { 1088 mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen; 1089 if (mDebugNonFullSizeScreen) { 1090 mTvView.layout(100, 100, 400, 300); 1091 } else { 1092 ViewGroup.LayoutParams params = mTvView.getLayoutParams(); 1093 params.width = ViewGroup.LayoutParams.MATCH_PARENT; 1094 params.height = ViewGroup.LayoutParams.MATCH_PARENT; 1095 mTvView.setLayoutParams(params); 1096 } 1097 return true; 1098 } 1099 case KeyEvent.KEYCODE_P: { 1100 togglePipView(); 1101 return true; 1102 } 1103 case KeyEvent.KEYCODE_CTRL_LEFT: 1104 case KeyEvent.KEYCODE_CTRL_RIGHT: { 1105 mUseKeycodeBlacklist = !mUseKeycodeBlacklist; 1106 return true; 1107 } 1108 case KeyEvent.KEYCODE_O: { 1109 showDisplayModeOption(BaseSideFragment.INITIATOR_SHORTCUT_KEY); 1110 return true; 1111 } 1112 } 1113 } 1114 return super.onKeyUp(keyCode, event); 1115 } 1116 1117 @Override 1118 public boolean onKeyLongPress(int keyCode, KeyEvent event) { 1119 if (DEBUG) Log.d(TAG, "onKeyLongPress(" + event); 1120 // Treat the BACK key long press as the normal press since we changed the behavior in 1121 // onBackPressed(). 1122 if (keyCode == KeyEvent.KEYCODE_BACK) { 1123 super.onBackPressed(); 1124 return true; 1125 } 1126 return false; 1127 } 1128 1129 @Override 1130 public void onBackPressed() { 1131 if (getFragmentManager().getBackStackEntryCount() <= 0 && isPlaying()) { 1132 // TODO: show the following toast message in the future. 1133// Toast.makeText(getApplicationContext(), getResources().getString( 1134// R.string.long_press_back), Toast.LENGTH_SHORT).show(); 1135 1136 // If back key would exit TV app, 1137 // show McLauncher instead so we can get benefit of McLauncher's shyMode. 1138 Intent startMain = new Intent(Intent.ACTION_MAIN); 1139 startMain.addCategory(Intent.CATEGORY_HOME); 1140 startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1141 startActivity(startMain); 1142 } else { 1143 super.onBackPressed(); 1144 } 1145 } 1146 1147 @Override 1148 public void onUserInteraction() { 1149 super.onUserInteraction(); 1150 if (mHideMainMenu.hasFocus() 1151 && (Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_SHOW) { 1152 mHideMainMenu.showAndHide(); 1153 } 1154 if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_SHOW) { 1155 mHideSideFragment.showAndHide(); 1156 } 1157 } 1158 1159 @Override 1160 public boolean onTouchEvent(MotionEvent event) { 1161 if (mMainMenuView.getVisibility() != View.VISIBLE) { 1162 mGestureDetector.onTouchEvent(event); 1163 } 1164 return super.onTouchEvent(event); 1165 } 1166 1167 public void togglePipView() { 1168 enablePipView(!mPipEnabled); 1169 } 1170 1171 public void enablePipView(boolean enable) { 1172 if (enable == mPipEnabled) { 1173 return; 1174 } 1175 if (enable) { 1176 long pipChannelId = mTvView.getCurrentChannelId(); 1177 if (pipChannelId != Channel.INVALID_ID) { 1178 mPipEnabled = true; 1179 mPipChannelId = pipChannelId; 1180 startPip(); 1181 } 1182 } else { 1183 mPipEnabled = false; 1184 mPipChannelId = Channel.INVALID_ID; 1185 stopPip(); 1186 } 1187 } 1188 1189 private boolean dispatchKeyEventToSession(final KeyEvent event) { 1190 if (DEBUG) Log.d(TAG, "dispatchKeyEventToSession(" + event + ")"); 1191 if (mTvView != null) { 1192 return mTvView.dispatchKeyEvent(event); 1193 } 1194 return false; 1195 } 1196 1197 public void moveToChannel(long id) { 1198 if (mChannelMap != null && mChannelMap.isLoadFinished() 1199 && id != mChannelMap.getCurrentChannelId()) { 1200 if (mChannelMap.moveToChannel(id)) { 1201 tune(); 1202 } else if (!TextUtils.isEmpty(Utils.getInputIdForChannel(this, id))) { 1203 startTv(id); 1204 } else { 1205 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 1206 } 1207 } 1208 } 1209 1210 private void channelUp() { 1211 if (mChannelMap != null && mChannelMap.isLoadFinished()) { 1212 if (mChannelMap.moveToNextChannel()) { 1213 tune(); 1214 } else { 1215 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 1216 } 1217 } 1218 } 1219 1220 private void channelDown() { 1221 if (mChannelMap != null && mChannelMap.isLoadFinished()) { 1222 if (mChannelMap.moveToPreviousChannel()) { 1223 tune(); 1224 } else { 1225 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 1226 } 1227 } 1228 } 1229 1230 public void showDialogFragment(final String tag, final DialogFragment dialog) { 1231 // A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV. 1232 if (!AVAILABLE_DIALOG_TAGS.contains(tag)) { 1233 return; 1234 } 1235 mHandler.post(new Runnable() { 1236 @Override 1237 public void run() { 1238 FragmentManager fm = getFragmentManager(); 1239 fm.executePendingTransactions(); 1240 1241 for (String availableTag : AVAILABLE_DIALOG_TAGS) { 1242 if (fm.findFragmentByTag(availableTag) != null) { 1243 return; 1244 } 1245 } 1246 1247 FragmentTransaction ft = getFragmentManager().beginTransaction(); 1248 ft.addToBackStack(null); 1249 dialog.show(ft, tag); 1250 } 1251 }); 1252 } 1253 1254 public boolean isClosedCaptionEnabled() { 1255 return mIsClosedCaptionEnabled; 1256 } 1257 1258 public void setClosedCaptionEnabled(boolean enable, boolean storeInPreference) { 1259 mIsClosedCaptionEnabled = enable; 1260 if (storeInPreference) { 1261 mSharedPreferences.edit().putBoolean(TvSettings.PREF_CLOSED_CAPTION_ENABLED, enable) 1262 .apply(); 1263 } 1264 // TODO: send the change to TIS 1265 } 1266 1267 public void restoreClosedCaptionEnabled() { 1268 setClosedCaptionEnabled(mSharedPreferences.getBoolean( 1269 TvSettings.PREF_CLOSED_CAPTION_ENABLED, false), false); 1270 } 1271 1272 // Returns a constant defined in DisplayMode. 1273 public int getDisplayMode() { 1274 return mDisplayMode; 1275 } 1276 1277 public void setDisplayMode(int displayMode, boolean storeInPreference) { 1278 mDisplayMode = displayMode; 1279 if (storeInPreference) { 1280 mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply(); 1281 } 1282 applyDisplayMode(mTvView.getVideoWidth(), mTvView.getVideoHeight()); 1283 } 1284 1285 public void restoreDisplayMode() { 1286 setDisplayMode(mSharedPreferences.getInt(TvSettings.PREF_DISPLAY_MODE, 1287 DisplayMode.MODE_NORMAL), false); 1288 } 1289 1290 // Returns a constant defined in TvSettings. 1291 public int getPipLocation() { 1292 return mPipLocation; 1293 } 1294 1295 public void setPipLocation(int pipLocation, boolean storeInPreference) { 1296 mPipLocation = pipLocation; 1297 if (storeInPreference) { 1298 mSharedPreferences.edit().putInt(TvSettings.PREF_PIP_LOCATION, pipLocation).apply(); 1299 } 1300 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams(); 1301 if (mPipLocation == TvSettings.PIP_LOCATION_TOP_LEFT) { 1302 lp.gravity = Gravity.TOP | Gravity.LEFT; 1303 } else if (mPipLocation == TvSettings.PIP_LOCATION_TOP_RIGHT) { 1304 lp.gravity = Gravity.TOP | Gravity.RIGHT; 1305 } else if (mPipLocation == TvSettings.PIP_LOCATION_BOTTOM_LEFT) { 1306 lp.gravity = Gravity.BOTTOM | Gravity.LEFT; 1307 } else if (mPipLocation == TvSettings.PIP_LOCATION_BOTTOM_RIGHT) { 1308 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT; 1309 } else { 1310 throw new IllegalArgumentException("Invaild PIP location: " + pipLocation); 1311 } 1312 mPipView.setLayoutParams(lp); 1313 } 1314 1315 public void restorePipLocation() { 1316 setPipLocation(mSharedPreferences.getInt(TvSettings.PREF_PIP_LOCATION, 1317 TvSettings.PIP_LOCATION_BOTTOM_RIGHT), false); 1318 } 1319 1320 private class HideRunnable implements Runnable { 1321 private final View mView; 1322 private final long mWaitingTime; 1323 private boolean mOnHideAnimation; 1324 private final Runnable mPreShowListener; 1325 private final Runnable mPostHideListener; 1326 private boolean mHasFocusDuringHideAnimation; 1327 1328 private HideRunnable(View view, long waitingTime) { 1329 this(view, waitingTime, null, null); 1330 } 1331 1332 private HideRunnable(View view, long waitingTime, Runnable preShowListener, 1333 Runnable postHideListener) { 1334 mView = view; 1335 mWaitingTime = waitingTime; 1336 mPreShowListener = preShowListener; 1337 mPostHideListener = postHideListener; 1338 } 1339 1340 @Override 1341 public void run() { 1342 startHideAnimation(false); 1343 } 1344 1345 private boolean hasFocus() { 1346 return mView.getVisibility() == View.VISIBLE 1347 && (!mOnHideAnimation || mHasFocusDuringHideAnimation); 1348 } 1349 1350 private void startHideAnimation(boolean fastFadeOutRequired) { 1351 mOnHideAnimation = true; 1352 mHasFocusDuringHideAnimation = !fastFadeOutRequired; 1353 Animation anim = AnimationUtils.loadAnimation(TvActivity.this, 1354 android.R.anim.fade_out); 1355 anim.setInterpolator(AnimationUtils.loadInterpolator(TvActivity.this, 1356 android.R.interpolator.fast_out_linear_in)); 1357 if (fastFadeOutRequired) { 1358 anim.setDuration(mShortAnimationDuration); 1359 } 1360 anim.setAnimationListener(new Animation.AnimationListener() { 1361 @Override 1362 public void onAnimationStart(Animation animation) { 1363 } 1364 1365 @Override 1366 public void onAnimationRepeat(Animation animation) { 1367 } 1368 1369 @Override 1370 public void onAnimationEnd(Animation animation) { 1371 if (mOnHideAnimation) { 1372 hideView(); 1373 } 1374 } 1375 }); 1376 1377 mView.clearAnimation(); 1378 mView.startAnimation(anim); 1379 } 1380 1381 private void hideView() { 1382 mOnHideAnimation = false; 1383 mHasFocusDuringHideAnimation = false; 1384 mView.setVisibility(View.GONE); 1385 if (mPostHideListener != null) { 1386 mPostHideListener.run(); 1387 } 1388 } 1389 1390 private void hideImmediately(boolean withAnimation) { 1391 if (mView.getVisibility() != View.VISIBLE) { 1392 return; 1393 } 1394 if (!withAnimation) { 1395 mHandler.removeCallbacks(this); 1396 hideView(); 1397 mView.clearAnimation(); 1398 return; 1399 } 1400 if (!mOnHideAnimation) { 1401 mHandler.removeCallbacks(this); 1402 startHideAnimation(true); 1403 } 1404 } 1405 1406 private void showAndHide() { 1407 if (mView.getVisibility() != View.VISIBLE) { 1408 if (mPreShowListener != null) { 1409 mPreShowListener.run(); 1410 } 1411 mView.setVisibility(View.VISIBLE); 1412 Animation anim = AnimationUtils.loadAnimation(TvActivity.this, 1413 android.R.anim.fade_in); 1414 anim.setInterpolator(AnimationUtils.loadInterpolator(TvActivity.this, 1415 android.R.interpolator.linear_out_slow_in)); 1416 mView.clearAnimation(); 1417 mView.startAnimation(anim); 1418 } 1419 // Schedule the hide animation after a few seconds. 1420 mHandler.removeCallbacks(this); 1421 if (mOnHideAnimation) { 1422 mOnHideAnimation = false; 1423 mView.clearAnimation(); 1424 mView.setAlpha(1f); 1425 } 1426 mHandler.postDelayed(this, mWaitingTime); 1427 } 1428 } 1429 1430 private void setShynessMode(boolean shyMode) { 1431 mIsShy = shyMode; 1432 setMediaPlaying(!shyMode); 1433 } 1434 1435 private boolean isShyModeSet() { 1436 return mIsShy; 1437 } 1438 1439 @Override 1440 public void onStopMediaPlaying() { 1441 mHandler.removeMessages(MSG_START_TV_RETRY); 1442 stopTv(); 1443 stopPip(); 1444 super.onStopMediaPlaying(); 1445 } 1446} 1447