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