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