TvActivity.java revision 829ca34b57d208a84430b2ccecd8975598d086da
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 String LEANBACK_SET_SHYNESS_BROADCAST = 116 "com.android.mclauncher.action.SET_APP_SHYNESS"; 117 private static final String LEANBACK_SHY_MODE_EXTRA = "shyMode"; 118 119 private static final HashSet<String> AVAILABLE_DIALOG_TAGS = new HashSet<String>(); 120 121 private TvInputManager mTvInputManager; 122 private TunableTvView mTvView; 123 private LinearLayout mControlGuide; 124 private MainMenuView mMainMenuView; 125 private ChannelBannerView mChannelBanner; 126 private ChannelNumberView mChannelNumberView; 127 private SidePanelContainer mSidePanelContainer; 128 private HideRunnable mHideChannelBanner; 129 private HideRunnable mHideControlGuide; 130 private HideRunnable mHideMainMenu; 131 private HideRunnable mHideSideFragment; 132 private int mShortAnimationDuration; 133 private int mDisplayWidth; 134 private GestureDetector mGestureDetector; 135 private ChannelMap mChannelMap; 136 private long mInitChannelId; 137 private String mInitTvInputId; 138 private boolean mChildActivityCanceled; 139 140 private TvInput mTvInputForSetup; 141 private TvInputManagerHelper mTvInputManagerHelper; 142 private AudioManager mAudioManager; 143 private int mAudioFocusStatus; 144 private boolean mTunePendding; 145 private boolean mPipEnabled; 146 private long mPipChannelId; 147 private boolean mDebugNonFullSizeScreen; 148 private boolean mActivityResumed; 149 private boolean mSilenceRequired; 150 private boolean mUseKeycodeBlacklist; 151 private boolean mIsShy = true; 152 153 private boolean mIsClosedCaptionEnabled; 154 private int mDisplayMode; 155 private int mPipLocation; 156 private SharedPreferences mSharedPreferences; 157 158 private SimpleGuideFragment mSimpleGuideFragment; 159 160 static { 161 AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG); 162 AVAILABLE_DIALOG_TAGS.add(EditInputDialogFragment.DIALOG_TAG); 163 } 164 165 // PIP is used for debug/verification of multiple sessions rather than real PIP feature. 166 // When PIP is enabled, the same channel as mTvView is tuned. 167 private TunableTvView mPipView; 168 169 private final Handler mHandler = new Handler() { 170 @Override 171 public void handleMessage(Message msg) { 172 if (msg.what == MSG_START_TV_RETRY) { 173 Object[] arg = (Object[]) msg.obj; 174 TvInput input = (TvInput) arg[0]; 175 long channelId = (Long) arg[1]; 176 int retryCount = msg.arg1; 177 startTvIfAvailableOrRetry(input, channelId, retryCount); 178 } 179 } 180 }; 181 182 @Override 183 protected void onCreate(Bundle savedInstanceState) { 184 super.onCreate(savedInstanceState); 185 186 setContentView(R.layout.activity_tv); 187 mTvView = (TunableTvView) findViewById(R.id.tv_view); 188 mTvView.setOnUnhandledInputEventListener(new TunableTvView.OnUnhandledInputEventListener() { 189 @Override 190 public boolean onUnhandledInputEvent(InputEvent event) { 191 if (event instanceof KeyEvent) { 192 KeyEvent keyEvent = (KeyEvent) event; 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 mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); 302 mTvInputManagerHelper = new TvInputManagerHelper(mTvInputManager); 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 (!input.hasChannel(false)) { 468 mTvInputForSetup = null; 469 if (!startSetupActivity(input)) { 470 String message = String.format( 471 getString(R.string.empty_channel_tvinput_and_no_setup_activity), 472 input.getDisplayName()); 473 Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); 474 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 475 } 476 return; 477 } 478 479 stopTv(); 480 startTvWithLastWatchedChannel(input); 481 } 482 483 public TvInputManagerHelper getTvInputManagerHelper() { 484 return mTvInputManagerHelper; 485 } 486 487 public TvInput getSelectedTvInput() { 488 return mChannelMap == null ? null : mChannelMap.getTvInput(); 489 } 490 491 public void showEditChannelsFragment(int initiator) { 492 showSideFragment(new EditChannelsFragment(mChannelMap.getChannelList(false)), initiator); 493 } 494 495 public boolean startSetupActivity() { 496 if (getSelectedTvInput() == null) { 497 return false; 498 } 499 return startSetupActivity(getSelectedTvInput()); 500 } 501 502 public boolean startSetupActivity(TvInput input) { 503 Intent intent = input.getIntentForSetupActivity(); 504 if (intent == null) { 505 return false; 506 } 507 startActivityForResult(intent, REQUEST_START_SETUP_ACTIIVTY); 508 mTvInputForSetup = input; 509 mInitTvInputId = null; 510 stopTv(); 511 return true; 512 } 513 514 public boolean startSettingsActivity() { 515 TvInput input = getSelectedTvInput(); 516 if (input == null) { 517 Log.w(TAG, "There is no selected TV input during startSettingsActivity"); 518 return false; 519 } 520 Intent intent = input.getIntentForSettingsActivity(); 521 if (intent == null) { 522 return false; 523 } 524 startActivityForResult(intent, REQUEST_START_SETTINGS_ACTIIVTY); 525 return true; 526 } 527 528 public void showSimpleGuide(int initiator) { 529 mSimpleGuideFragment = new SimpleGuideFragment(this, mChannelMap); 530 showSideFragment(mSimpleGuideFragment, initiator); 531 } 532 533 public void showInputPicker(int initiator) { 534 showSideFragment(new InputPickerFragment(), initiator); 535 } 536 537 public void showDisplayModeOption(int initiator) { 538 showSideFragment(new DisplayModeOptionFragment(), initiator); 539 } 540 541 public void showClosedCaptionOption(int initiator) { 542 showSideFragment(new ClosedCaptionOptionFragment(), initiator); 543 } 544 545 public void showPipLocationOption(int initiator) { 546 showSideFragment(new PipLocationFragment(), initiator); 547 } 548 549 public void showSideFragment(Fragment f, int initiator) { 550 mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_SHOW); 551 mSidePanelContainer.setKeyDispatchable(true); 552 553 Bundle bundle = new Bundle(); 554 bundle.putInt(BaseSideFragment.KEY_INITIATOR, initiator); 555 f.setArguments(bundle); 556 FragmentTransaction ft = getFragmentManager().beginTransaction(); 557 ft.add(R.id.right_panel, f); 558 ft.addToBackStack(null); 559 ft.commit(); 560 561 mHideSideFragment.showAndHide(); 562 } 563 564 public void popFragmentBackStack() { 565 if (getFragmentManager().getBackStackEntryCount() > 1) { 566 getFragmentManager().popBackStack(); 567 } else if (getFragmentManager().getBackStackEntryCount() == 1 568 && (Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_RESET) { 569 if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_SHOW) { 570 mSidePanelContainer.setKeyDispatchable(false); 571 mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_HIDE); 572 mHideSideFragment.hideImmediately(true); 573 } else { 574 // It is during fade-out animation. 575 } 576 } else { 577 getFragmentManager().popBackStack(); 578 } 579 } 580 581 public void onSideFragmentCanceled(int initiator) { 582 if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_RESET) { 583 return; 584 } 585 if (initiator == BaseSideFragment.INITIATOR_MENU) { 586 displayMainMenu(false); 587 } 588 } 589 590 private void resetSideFragment() { 591 while (true) { 592 if (!getFragmentManager().popBackStackImmediate()) { 593 break; 594 } 595 } 596 mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_RESET); 597 } 598 599 @Override 600 public void onActivityResult(int requestCode, int resultCode, Intent data) { 601 if (resultCode == Activity.RESULT_OK) { 602 switch (requestCode) { 603 case REQUEST_START_SETUP_ACTIIVTY: 604 if (mTvInputForSetup != null) { 605 mInitTvInputId = mTvInputForSetup.getId(); 606 } 607 break; 608 609 default: 610 //TODO: Handle failure of setup. 611 } 612 } else { 613 mChildActivityCanceled = true; 614 } 615 mTvInputForSetup = null; 616 } 617 618 @Override 619 public boolean dispatchKeyEvent(KeyEvent event) { 620 if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); 621 if (mMainMenuView.isShown() || getFragmentManager().getBackStackEntryCount() > 0) { 622 return super.dispatchKeyEvent(event); 623 } 624 int eventKeyCode = event.getKeyCode(); 625 for (int keycode : KEYCODE_BLACKLIST) { 626 if (keycode == eventKeyCode) { 627 return super.dispatchKeyEvent(event); 628 } 629 } 630 return dispatchKeyEventToSession(event) || super.dispatchKeyEvent(event); 631 } 632 633 @Override 634 public void onAudioFocusChange(int focusChange) { 635 mAudioFocusStatus = focusChange; 636 setVolumeByAudioFocusStatus(); 637 } 638 639 private void setVolumeByAudioFocusStatus() { 640 if (mTvView.isPlaying()) { 641 switch (mAudioFocusStatus) { 642 case AudioManager.AUDIOFOCUS_GAIN: 643 if (!mSilenceRequired) { 644 mTvView.setStreamVolume(AUDIO_MAX_VOLUME); 645 if (isShyModeSet()) { 646 setShynessMode(false); 647 } 648 } else { 649 mTvView.setStreamVolume(AUDIO_MIN_VOLUME); 650 } 651 break; 652 case AudioManager.AUDIOFOCUS_LOSS: 653 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 654 mTvView.setStreamVolume(AUDIO_MIN_VOLUME); 655 if (!mActivityResumed) { 656 mSilenceRequired = true; 657 } 658 break; 659 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 660 if (!mSilenceRequired) { 661 mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME); 662 } else { 663 mTvView.setStreamVolume(AUDIO_MIN_VOLUME); 664 } 665 break; 666 } 667 } 668 // When the activity loses the audio focus, set the Shy mode regardless of the play status. 669 if (mAudioFocusStatus == AudioManager.AUDIOFOCUS_LOSS || 670 mAudioFocusStatus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { 671 if (!isShyModeSet()) { 672 setShynessMode(true); 673 } 674 } 675 } 676 677 private void startTvWithLastWatchedChannel(TvInput input) { 678 long channelId = Utils.getLastWatchedChannelId(TvActivity.this, input.getId()); 679 startTv(input, channelId); 680 } 681 682 private void startTv(TvInput input, long channelId) { 683 if (mChannelMap != null) { 684 // TODO: when this case occurs, we should remove the case. 685 Log.w(TAG, "The previous variables are not released in startTv"); 686 stopTv(); 687 } 688 689 mMainMenuView.setChannelMap(null); 690 int result = mAudioManager.requestAudioFocus(TvActivity.this, 691 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 692 mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ? 693 AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS; 694 695 // Prepare a new channel map for the current input. 696 mChannelMap = input.buildChannelMap(this, channelId, mOnChannelsLoadFinished); 697 mTvView.start(mTvInputManagerHelper); 698 setVolumeByAudioFocusStatus(); 699 tune(); 700 } 701 702 private void stopTv() { 703 if (mTvView.isPlaying()) { 704 mTvView.stop(); 705 mAudioManager.abandonAudioFocus(this); 706 } 707 if (mChannelMap != null) { 708 mMainMenuView.setChannelMap(null); 709 mChannelMap.close(); 710 mChannelMap = null; 711 } 712 mTunePendding = false; 713 714 if (!isShyModeSet()) { 715 setShynessMode(true); 716 } 717 } 718 719 private boolean isPlaying() { 720 return mTvView.isPlaying() && mTvView.getCurrentChannelId() != Channel.INVALID_ID; 721 } 722 723 private void startPip() { 724 if (mPipChannelId == Channel.INVALID_ID) { 725 Log.w(TAG, "PIP channel id is an invalid id."); 726 return; 727 } 728 if (DEBUG) Log.d(TAG, "startPip()"); 729 mPipView.start(mTvInputManagerHelper); 730 boolean success = mPipView.tuneTo(mPipChannelId, new OnTuneListener() { 731 @Override 732 public void onUnexpectedStop(long channelId) { 733 Log.w(TAG, "The PIP is Unexpectedly stopped"); 734 enablePipView(false); 735 } 736 737 @Override 738 public void onTuned(boolean success, long channelId) { 739 if (!success) { 740 Log.w(TAG, "Fail to start the PIP during channel tunning"); 741 enablePipView(false); 742 } else { 743 mPipView.setVisibility(View.VISIBLE); 744 } 745 } 746 747 @Override 748 public void onStreamInfoChanged(StreamInfo info) { 749 // Do nothing. 750 } 751 }); 752 if (!success) { 753 Log.w(TAG, "Fail to start the PIP"); 754 return; 755 } 756 mPipView.setStreamVolume(AUDIO_MIN_VOLUME); 757 } 758 759 private void stopPip() { 760 if (DEBUG) Log.d(TAG, "stopPip"); 761 if (mPipView.isPlaying()) { 762 mPipView.setVisibility(View.INVISIBLE); 763 mPipView.stop(); 764 } 765 } 766 767 private final Runnable mOnChannelsLoadFinished = new Runnable() { 768 @Override 769 public void run() { 770 if (mTunePendding) { 771 tune(); 772 } 773 mChannelNumberView.setChannels(mChannelMap.getChannelList(false)); 774 mMainMenuView.setChannelMap(mChannelMap); 775 } 776 }; 777 778 private void tune() { 779 if (DEBUG) Log.d(TAG, "tune()"); 780 // Prerequisites to be able to tune. 781 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 782 if (DEBUG) Log.d(TAG, "Channel map not ready"); 783 mTunePendding = true; 784 return; 785 } 786 mTunePendding = false; 787 long channelId = mChannelMap.getCurrentChannelId(); 788 final String inputId = mChannelMap.getTvInput().getId(); 789 if (channelId == Channel.INVALID_ID) { 790 stopTv(); 791 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 792 return; 793 } 794 795 mTvView.tuneTo(channelId, new OnTuneListener() { 796 @Override 797 public void onUnexpectedStop(long channelId) { 798 stopTv(); 799 startTv(Channel.INVALID_ID); 800 } 801 802 @Override 803 public void onTuned(boolean success, long channelId) { 804 if (!success) { 805 Log.w(TAG, "Failed to tune to channel " + channelId); 806 // TODO: show something to user about this error. 807 } else { 808 Utils.setLastWatchedChannelId(TvActivity.this, inputId, 809 mTvView.getCurrentTvInputInfo().getId(), channelId); 810 } 811 } 812 813 @Override 814 public void onStreamInfoChanged(StreamInfo info) { 815 updateChannelBanner(false); 816 applyDisplayMode(info.getVideoWidth(), info.getVideoHeight()); 817 } 818 }); 819 updateChannelBanner(true); 820 if (mChildActivityCanceled) { 821 displayMainMenu(false); 822 mChildActivityCanceled = false; 823 } 824 if (isShyModeSet()) { 825 setShynessMode(false); 826 // TODO: Set the shy mode to true when tune() fails. 827 } 828 } 829 830 public void hideOverlays(boolean hideMainMenu, boolean hideChannelBanner, 831 boolean hideSidePanel) { 832 hideOverlays(hideMainMenu, hideChannelBanner, hideSidePanel, true); 833 } 834 835 public void hideOverlays(boolean hideMainMenu, boolean hideChannelBanner, 836 boolean hideSidePanel, boolean withAnimation) { 837 if (hideMainMenu) { 838 mHideMainMenu.hideImmediately(withAnimation); 839 } 840 if (hideChannelBanner) { 841 mHideChannelBanner.hideImmediately(withAnimation); 842 } 843 if (hideSidePanel) { 844 if ((Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_SHOW) { 845 return; 846 } 847 mSidePanelContainer.setTag(SIDE_FRAGMENT_TAG_HIDE); 848 mHideSideFragment.hideImmediately(withAnimation); 849 } 850 } 851 852 private void applyDisplayMode(int videoWidth, int videoHeight) { 853 int decorViewWidth = getWindow().getDecorView().getWidth(); 854 int decorViewHeight = getWindow().getDecorView().getHeight(); 855 ViewGroup.LayoutParams layoutParams = mTvView.getLayoutParams(); 856 int displayMode = mDisplayMode; 857 double decorViewRatio = 0; 858 double videoRatio = 0; 859 if (decorViewWidth <= 0 || decorViewHeight <= 0 || videoWidth <= 0 || videoHeight <=0) { 860 displayMode = DisplayMode.MODE_FULL; 861 Log.w(TAG, "Some resolution info is missing during applyDisplayMode. (" 862 + "decorViewWidth=" + decorViewWidth + ", decorViewHeight=" + decorViewHeight 863 + ", width=" + videoWidth + ", height=" + videoHeight + ")"); 864 } else { 865 decorViewRatio = (double) decorViewWidth / decorViewHeight; 866 videoRatio = (double) videoWidth / videoHeight; 867 } 868 if (displayMode == DisplayMode.MODE_FULL) { 869 layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 870 layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 871 } else if (displayMode == DisplayMode.MODE_ZOOM) { 872 if (videoRatio < decorViewRatio) { 873 // Y axis will be clipped. 874 layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 875 layoutParams.height = (int) (decorViewWidth / videoRatio); 876 } else { 877 // X axis will be clipped. 878 layoutParams.width = (int) (decorViewHeight * videoRatio); 879 layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 880 } 881 } else { 882 // MODE_NORMAL (default value) 883 if (videoRatio < decorViewRatio) { 884 // X axis has black area. 885 layoutParams.width = (int) (decorViewHeight * videoRatio); 886 layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 887 } else { 888 // Y axis has black area. 889 layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; 890 layoutParams.height = (int) (decorViewWidth / videoRatio); 891 } 892 } 893 mTvView.setLayoutParams(layoutParams); 894 } 895 896 private void updateChannelBanner(final boolean showBanner) { 897 runOnUiThread(new Runnable() { 898 @Override 899 public void run() { 900 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 901 return; 902 } 903 904 mChannelBanner.updateViews(mChannelMap, mTvView); 905 if (showBanner) { 906 mHideChannelBanner.showAndHide(); 907 } 908 } 909 }); 910 } 911 912 private void displayMainMenu(final boolean resetSelectedItemPosition) { 913 runOnUiThread(new Runnable() { 914 @Override 915 public void run() { 916 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 917 return; 918 } 919 920 if (!mMainMenuView.isShown() && resetSelectedItemPosition) { 921 mMainMenuView.resetSelectedItemPosition(); 922 } 923 mHideMainMenu.showAndHide(); 924 } 925 }); 926 } 927 928 public void showRecentlyWatchedDialog() { 929 showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG, 930 new RecentlyWatchedDialogFragment()); 931 } 932 933 @Override 934 protected void onSaveInstanceState(Bundle outState) { 935 // Do not save instance state because restoring instance state when TV app died 936 // unexpectedly can cause some problems like initializing fragments duplicately and 937 // accessing resource before it is initialzed. 938 } 939 940 @Override 941 protected void onDestroy() { 942 if (DEBUG) Log.d(TAG, "onDestroy()"); 943 mTvInputManagerHelper.stop(); 944 super.onDestroy(); 945 } 946 947 @Override 948 public boolean onKeyDown(int keyCode, KeyEvent event) { 949 if (mMainMenuView.getVisibility() == View.VISIBLE 950 || getFragmentManager().getBackStackEntryCount() > 0) { 951 return super.onKeyDown(keyCode, event); 952 } 953 if (mChannelMap == null) { 954 return false; 955 } 956 switch (keyCode) { 957 case KeyEvent.KEYCODE_CHANNEL_UP: 958 case KeyEvent.KEYCODE_DPAD_UP: 959 channelUp(); 960 return true; 961 962 case KeyEvent.KEYCODE_CHANNEL_DOWN: 963 case KeyEvent.KEYCODE_DPAD_DOWN: 964 channelDown(); 965 return true; 966 } 967 return super.onKeyDown(keyCode, event); 968 } 969 970 @Override 971 public boolean onKeyUp(int keyCode, KeyEvent event) { 972 if (getFragmentManager().getBackStackEntryCount() > 0) { 973 if (keyCode == KeyEvent.KEYCODE_BACK) { 974 popFragmentBackStack(); 975 return true; 976 } 977 return super.onKeyUp(keyCode, event); 978 } 979 if (mMainMenuView.isShown() || mChannelBanner.isShown()) { 980 if (keyCode == KeyEvent.KEYCODE_BACK) { 981 hideOverlays(true, true, false); 982 return true; 983 } 984 if (mMainMenuView.isShown()) { 985 return super.onKeyUp(keyCode, event); 986 } 987 } 988 if (mChannelNumberView.isShown()) { 989 if (keyCode == KeyEvent.KEYCODE_BACK) { 990 mChannelNumberView.hide(); 991 return true; 992 } 993 return mChannelNumberView.onKeyUp(keyCode, event); 994 } 995 if (keyCode == KeyEvent.KEYCODE_BACK) { 996 // When the event is from onUnhandledInputEvent, onBackPressed is not automatically 997 // called. Therefore, we need to explicitly call onBackPressed(). 998 onBackPressed(); 999 return true; 1000 } 1001 1002 if (mHandler.hasMessages(MSG_START_TV_RETRY)) { 1003 // Ignore key events during startTv retry. 1004 return true; 1005 } 1006 if (mChannelMap == null) { 1007 switch (keyCode) { 1008 case KeyEvent.KEYCODE_H: 1009 showRecentlyWatchedDialog(); 1010 return true; 1011 case KeyEvent.KEYCODE_TV_INPUT: 1012 case KeyEvent.KEYCODE_I: 1013 case KeyEvent.KEYCODE_CHANNEL_UP: 1014 case KeyEvent.KEYCODE_DPAD_UP: 1015 case KeyEvent.KEYCODE_CHANNEL_DOWN: 1016 case KeyEvent.KEYCODE_DPAD_DOWN: 1017 case KeyEvent.KEYCODE_NUMPAD_ENTER: 1018 case KeyEvent.KEYCODE_DPAD_CENTER: 1019 case KeyEvent.KEYCODE_E: 1020 case KeyEvent.KEYCODE_MENU: 1021 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 1022 return true; 1023 } 1024 } else { 1025 if (ChannelNumberView.isChannelNumberKey(keyCode) 1026 && keyCode != KeyEvent.KEYCODE_MINUS) { 1027 mChannelNumberView.show(); 1028 mHideChannelBanner.hideImmediately(true); 1029 return mChannelNumberView.onKeyUp(keyCode, event); 1030 } 1031 switch (keyCode) { 1032 case KeyEvent.KEYCODE_H: 1033 showRecentlyWatchedDialog(); 1034 return true; 1035 1036 case KeyEvent.KEYCODE_TV_INPUT: 1037 case KeyEvent.KEYCODE_I: 1038 showInputPicker(BaseSideFragment.INITIATOR_UNKNOWN); 1039 return true; 1040 1041 case KeyEvent.KEYCODE_DPAD_LEFT: 1042 case KeyEvent.KEYCODE_DPAD_RIGHT: 1043 displayMainMenu(true); 1044 return true; 1045 1046 case KeyEvent.KEYCODE_ENTER: 1047 case KeyEvent.KEYCODE_NUMPAD_ENTER: 1048 case KeyEvent.KEYCODE_E: 1049 case KeyEvent.KEYCODE_DPAD_CENTER: 1050 case KeyEvent.KEYCODE_MENU: 1051 if (event.isCanceled()) { 1052 return true; 1053 } 1054 if (keyCode != KeyEvent.KEYCODE_MENU) { 1055 updateChannelBanner(true); 1056 } 1057 if (keyCode != KeyEvent.KEYCODE_E) { 1058 displayMainMenu(true); 1059 } 1060 return true; 1061 } 1062 } 1063 if (USE_DEBUG_KEYS) { 1064 switch (keyCode) { 1065 case KeyEvent.KEYCODE_W: { 1066 mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen; 1067 if (mDebugNonFullSizeScreen) { 1068 mTvView.layout(100, 100, 400, 300); 1069 } else { 1070 ViewGroup.LayoutParams params = mTvView.getLayoutParams(); 1071 params.width = ViewGroup.LayoutParams.MATCH_PARENT; 1072 params.height = ViewGroup.LayoutParams.MATCH_PARENT; 1073 mTvView.setLayoutParams(params); 1074 } 1075 return true; 1076 } 1077 case KeyEvent.KEYCODE_P: { 1078 togglePipView(); 1079 return true; 1080 } 1081 case KeyEvent.KEYCODE_CTRL_LEFT: 1082 case KeyEvent.KEYCODE_CTRL_RIGHT: { 1083 mUseKeycodeBlacklist = !mUseKeycodeBlacklist; 1084 return true; 1085 } 1086 case KeyEvent.KEYCODE_O: { 1087 showDisplayModeOption(BaseSideFragment.INITIATOR_SHORTCUT_KEY); 1088 return true; 1089 } 1090 } 1091 } 1092 return super.onKeyUp(keyCode, event); 1093 } 1094 1095 @Override 1096 public boolean onKeyLongPress(int keyCode, KeyEvent event) { 1097 if (DEBUG) Log.d(TAG, "onKeyLongPress(" + event); 1098 // Treat the BACK key long press as the normal press since we changed the behavior in 1099 // onBackPressed(). 1100 if (keyCode == KeyEvent.KEYCODE_BACK) { 1101 super.onBackPressed(); 1102 return true; 1103 } 1104 return false; 1105 } 1106 1107 @Override 1108 public void onBackPressed() { 1109 if (getFragmentManager().getBackStackEntryCount() <= 0 && isPlaying()) { 1110 // TODO: show the following toast message in the future. 1111// Toast.makeText(getApplicationContext(), getResources().getString( 1112// R.string.long_press_back), Toast.LENGTH_SHORT).show(); 1113 1114 // If back key would exit TV app, 1115 // show McLauncher instead so we can get benefit of McLauncher's shyMode. 1116 Intent startMain = new Intent(Intent.ACTION_MAIN); 1117 startMain.addCategory(Intent.CATEGORY_HOME); 1118 startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1119 startActivity(startMain); 1120 } else { 1121 super.onBackPressed(); 1122 } 1123 } 1124 1125 @Override 1126 public void onUserInteraction() { 1127 super.onUserInteraction(); 1128 if (mHideMainMenu.hasFocus() 1129 && (Integer) mSidePanelContainer.getTag() != SIDE_FRAGMENT_TAG_SHOW) { 1130 mHideMainMenu.showAndHide(); 1131 } 1132 if ((Integer) mSidePanelContainer.getTag() == SIDE_FRAGMENT_TAG_SHOW) { 1133 mHideSideFragment.showAndHide(); 1134 } 1135 } 1136 1137 @Override 1138 public boolean onTouchEvent(MotionEvent event) { 1139 if (mMainMenuView.getVisibility() != View.VISIBLE) { 1140 mGestureDetector.onTouchEvent(event); 1141 } 1142 return super.onTouchEvent(event); 1143 } 1144 1145 public void togglePipView() { 1146 enablePipView(!mPipEnabled); 1147 } 1148 1149 public void enablePipView(boolean enable) { 1150 if (enable == mPipEnabled) { 1151 return; 1152 } 1153 if (enable) { 1154 long pipChannelId = mTvView.getCurrentChannelId(); 1155 if (pipChannelId != Channel.INVALID_ID) { 1156 mPipEnabled = true; 1157 mPipChannelId = pipChannelId; 1158 startPip(); 1159 } 1160 } else { 1161 mPipEnabled = false; 1162 mPipChannelId = Channel.INVALID_ID; 1163 stopPip(); 1164 } 1165 } 1166 1167 private boolean dispatchKeyEventToSession(final KeyEvent event) { 1168 if (DEBUG) Log.d(TAG, "dispatchKeyEventToSession(" + event + ")"); 1169 if (mTvView != null) { 1170 return mTvView.dispatchKeyEvent(event); 1171 } 1172 return false; 1173 } 1174 1175 public void moveToChannel(long id) { 1176 if (mChannelMap != null && mChannelMap.isLoadFinished() 1177 && id != mChannelMap.getCurrentChannelId()) { 1178 if (mChannelMap.moveToChannel(id)) { 1179 tune(); 1180 } else if (!TextUtils.isEmpty(Utils.getInputIdForChannel(this, id))) { 1181 startTv(id); 1182 } else { 1183 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 1184 } 1185 } 1186 } 1187 1188 private void channelUp() { 1189 if (mChannelMap != null && mChannelMap.isLoadFinished()) { 1190 if (mChannelMap.moveToNextChannel()) { 1191 tune(); 1192 } else { 1193 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 1194 } 1195 } 1196 } 1197 1198 private void channelDown() { 1199 if (mChannelMap != null && mChannelMap.isLoadFinished()) { 1200 if (mChannelMap.moveToPreviousChannel()) { 1201 tune(); 1202 } else { 1203 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 1204 } 1205 } 1206 } 1207 1208 public void showDialogFragment(final String tag, final DialogFragment dialog) { 1209 // A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV. 1210 if (!AVAILABLE_DIALOG_TAGS.contains(tag)) { 1211 return; 1212 } 1213 mHandler.post(new Runnable() { 1214 @Override 1215 public void run() { 1216 FragmentManager fm = getFragmentManager(); 1217 fm.executePendingTransactions(); 1218 1219 for (String availableTag : AVAILABLE_DIALOG_TAGS) { 1220 if (fm.findFragmentByTag(availableTag) != null) { 1221 return; 1222 } 1223 } 1224 1225 FragmentTransaction ft = getFragmentManager().beginTransaction(); 1226 ft.addToBackStack(null); 1227 dialog.show(ft, tag); 1228 } 1229 }); 1230 } 1231 1232 public boolean isClosedCaptionEnabled() { 1233 return mIsClosedCaptionEnabled; 1234 } 1235 1236 public void setClosedCaptionEnabled(boolean enable, boolean storeInPreference) { 1237 mIsClosedCaptionEnabled = enable; 1238 if (storeInPreference) { 1239 mSharedPreferences.edit().putBoolean(TvSettings.PREF_CLOSED_CAPTION_ENABLED, enable) 1240 .apply(); 1241 } 1242 // TODO: send the change to TIS 1243 } 1244 1245 public void restoreClosedCaptionEnabled() { 1246 setClosedCaptionEnabled(mSharedPreferences.getBoolean( 1247 TvSettings.PREF_CLOSED_CAPTION_ENABLED, false), false); 1248 } 1249 1250 // Returns a constant defined in DisplayMode. 1251 public int getDisplayMode() { 1252 return mDisplayMode; 1253 } 1254 1255 public void setDisplayMode(int displayMode, boolean storeInPreference) { 1256 mDisplayMode = displayMode; 1257 if (storeInPreference) { 1258 mSharedPreferences.edit().putInt(TvSettings.PREF_DISPLAY_MODE, displayMode).apply(); 1259 } 1260 applyDisplayMode(mTvView.getVideoWidth(), mTvView.getVideoHeight()); 1261 } 1262 1263 public void restoreDisplayMode() { 1264 setDisplayMode(mSharedPreferences.getInt(TvSettings.PREF_DISPLAY_MODE, 1265 DisplayMode.MODE_NORMAL), false); 1266 } 1267 1268 // Returns a constant defined in TvSettings. 1269 public int getPipLocation() { 1270 return mPipLocation; 1271 } 1272 1273 public void setPipLocation(int pipLocation, boolean storeInPreference) { 1274 mPipLocation = pipLocation; 1275 if (storeInPreference) { 1276 mSharedPreferences.edit().putInt(TvSettings.PREF_PIP_LOCATION, pipLocation).apply(); 1277 } 1278 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams(); 1279 if (mPipLocation == TvSettings.PIP_LOCATION_TOP_LEFT) { 1280 lp.gravity = Gravity.TOP | Gravity.LEFT; 1281 } else if (mPipLocation == TvSettings.PIP_LOCATION_TOP_RIGHT) { 1282 lp.gravity = Gravity.TOP | Gravity.RIGHT; 1283 } else if (mPipLocation == TvSettings.PIP_LOCATION_BOTTOM_LEFT) { 1284 lp.gravity = Gravity.BOTTOM | Gravity.LEFT; 1285 } else if (mPipLocation == TvSettings.PIP_LOCATION_BOTTOM_RIGHT) { 1286 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT; 1287 } else { 1288 throw new IllegalArgumentException("Invaild PIP location: " + pipLocation); 1289 } 1290 mPipView.setLayoutParams(lp); 1291 } 1292 1293 public void restorePipLocation() { 1294 setDisplayMode(mSharedPreferences.getInt(TvSettings.PREF_PIP_LOCATION, 1295 TvSettings.PIP_LOCATION_BOTTOM_RIGHT), false); 1296 } 1297 1298 private class HideRunnable implements Runnable { 1299 private final View mView; 1300 private final long mWaitingTime; 1301 private boolean mOnHideAnimation; 1302 private final Runnable mPreShowListener; 1303 private final Runnable mPostHideListener; 1304 private boolean mHasFocusDuringHideAnimation; 1305 1306 private HideRunnable(View view, long waitingTime) { 1307 this(view, waitingTime, null, null); 1308 } 1309 1310 private HideRunnable(View view, long waitingTime, Runnable preShowListener, 1311 Runnable postHideListener) { 1312 mView = view; 1313 mWaitingTime = waitingTime; 1314 mPreShowListener = preShowListener; 1315 mPostHideListener = postHideListener; 1316 } 1317 1318 @Override 1319 public void run() { 1320 startHideAnimation(false); 1321 } 1322 1323 private boolean hasFocus() { 1324 return mView.getVisibility() == View.VISIBLE 1325 && (!mOnHideAnimation || mHasFocusDuringHideAnimation); 1326 } 1327 1328 private void startHideAnimation(boolean fastFadeOutRequired) { 1329 mOnHideAnimation = true; 1330 mHasFocusDuringHideAnimation = !fastFadeOutRequired; 1331 Animation anim = AnimationUtils.loadAnimation(TvActivity.this, 1332 android.R.anim.fade_out); 1333 anim.setInterpolator(AnimationUtils.loadInterpolator(TvActivity.this, 1334 android.R.interpolator.fast_out_linear_in)); 1335 if (fastFadeOutRequired) { 1336 anim.setDuration(mShortAnimationDuration); 1337 } 1338 anim.setAnimationListener(new Animation.AnimationListener() { 1339 @Override 1340 public void onAnimationStart(Animation animation) { 1341 } 1342 1343 @Override 1344 public void onAnimationRepeat(Animation animation) { 1345 } 1346 1347 @Override 1348 public void onAnimationEnd(Animation animation) { 1349 if (mOnHideAnimation) { 1350 hideView(); 1351 } 1352 } 1353 }); 1354 1355 mView.clearAnimation(); 1356 mView.startAnimation(anim); 1357 } 1358 1359 private void hideView() { 1360 mOnHideAnimation = false; 1361 mHasFocusDuringHideAnimation = false; 1362 mView.setVisibility(View.GONE); 1363 if (mPostHideListener != null) { 1364 mPostHideListener.run(); 1365 } 1366 } 1367 1368 private void hideImmediately(boolean withAnimation) { 1369 if (mView.getVisibility() != View.VISIBLE) { 1370 return; 1371 } 1372 if (!withAnimation) { 1373 mHandler.removeCallbacks(this); 1374 hideView(); 1375 mView.clearAnimation(); 1376 return; 1377 } 1378 if (!mOnHideAnimation) { 1379 mHandler.removeCallbacks(this); 1380 startHideAnimation(true); 1381 } 1382 } 1383 1384 private void showAndHide() { 1385 if (mView.getVisibility() != View.VISIBLE) { 1386 if (mPreShowListener != null) { 1387 mPreShowListener.run(); 1388 } 1389 mView.setVisibility(View.VISIBLE); 1390 Animation anim = AnimationUtils.loadAnimation(TvActivity.this, 1391 android.R.anim.fade_in); 1392 anim.setInterpolator(AnimationUtils.loadInterpolator(TvActivity.this, 1393 android.R.interpolator.linear_out_slow_in)); 1394 mView.clearAnimation(); 1395 mView.startAnimation(anim); 1396 } 1397 // Schedule the hide animation after a few seconds. 1398 mHandler.removeCallbacks(this); 1399 if (mOnHideAnimation) { 1400 mOnHideAnimation = false; 1401 mView.clearAnimation(); 1402 mView.setAlpha(1f); 1403 } 1404 mHandler.postDelayed(this, mWaitingTime); 1405 } 1406 } 1407 1408 private void setShynessMode(boolean shyMode) { 1409 mIsShy = shyMode; 1410 Intent intent = new Intent(LEANBACK_SET_SHYNESS_BROADCAST); 1411 intent.putExtra(LEANBACK_SHY_MODE_EXTRA, shyMode); 1412 sendBroadcast(intent); 1413 } 1414 1415 private boolean isShyModeSet() { 1416 return mIsShy; 1417 } 1418} 1419