TvActivity.java revision d6903b32bf7c313e0395c492dd374bfb5b9909c8
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.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.app.Activity; 22import android.app.AlertDialog; 23import android.app.DialogFragment; 24import android.app.FragmentManager; 25import android.app.FragmentTransaction; 26import android.content.ContentUris; 27import android.content.Context; 28import android.content.Intent; 29import android.graphics.Point; 30import android.media.AudioManager; 31import android.net.Uri; 32import android.os.Bundle; 33import android.os.Handler; 34import android.os.Message; 35import android.text.TextUtils; 36import android.tv.TvInputInfo; 37import android.tv.TvInputManager; 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.widget.LinearLayout; 48import android.widget.Toast; 49 50import com.android.tv.data.Channel; 51import com.android.tv.data.ChannelMap; 52import com.android.tv.data.StreamInfo; 53import com.android.tv.dialog.EditChannelsDialogFragment; 54import com.android.tv.dialog.EditInputDialogFragment; 55import com.android.tv.dialog.InputPickerDialogFragment; 56import com.android.tv.dialog.PrivacySettingDialogFragment; 57import com.android.tv.dialog.RecentlyWatchedDialogFragment; 58import com.android.tv.input.TisTvInput; 59import com.android.tv.input.TvInput; 60import com.android.tv.input.UnifiedTvInput; 61import com.android.tv.ui.AspectRatioOptionFragment; 62import com.android.tv.ui.ChannelBannerView; 63import com.android.tv.ui.ClosedCaptionOptionFragment; 64import com.android.tv.ui.MainMenuView; 65import com.android.tv.ui.TunableTvView; 66import com.android.tv.ui.TunableTvView.OnTuneListener; 67import com.android.tv.util.TvInputManagerHelper; 68import com.android.tv.util.Utils; 69 70import java.util.HashSet; 71 72/** 73 * The main activity for demonstrating TV app. 74 */ 75public class TvActivity extends Activity implements 76 InputPickerDialogFragment.InputPickerDialogListener, 77 AudioManager.OnAudioFocusChangeListener { 78 // STOPSHIP: Turn debugging off 79 private static final boolean DEBUG = true; 80 private static final String TAG = "TvActivity"; 81 82 private static final int MSG_START_TV_RETRY = 1; 83 84 private static final int DURATION_SHOW_CHANNEL_BANNER = 2000; 85 private static final int DURATION_SHOW_CONTROL_GUIDE = 1000; 86 private static final int DURATION_SHOW_MAIN_MENU = DURATION_SHOW_CHANNEL_BANNER; 87 private static final float AUDIO_MAX_VOLUME = 1.0f; 88 private static final float AUDIO_MIN_VOLUME = 0.0f; 89 private static final float AUDIO_DUCKING_VOLUME = 0.3f; 90 private static final int START_TV_MAX_RETRY = 4; 91 private static final int START_TV_RETRY_INTERVAL = 250; 92 93 // TODO: add more KEYCODEs to the white list. 94 private static final int[] KEYCODE_WHITELIST = { 95 KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3, 96 KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_7, 97 KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_STAR, KeyEvent.KEYCODE_POUND, 98 KeyEvent.KEYCODE_M, 99 }; 100 // TODO: this value should be able to be toggled in menu. 101 private static final boolean USE_KEYCODE_BLACKLIST = false; 102 private static final int[] KEYCODE_BLACKLIST = { 103 KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_CHANNEL_UP, KeyEvent.KEYCODE_CHANNEL_DOWN, 104 KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT 105 }; 106 // STOPSHIP: debug keys are used only for testing. 107 private static final boolean USE_DEBUG_KEYS = true; 108 109 private static final int REQUEST_START_SETUP_ACTIIVTY = 0; 110 111 private static final String LEANBACK_SET_SHYNESS_BROADCAST = 112 "com.android.mclauncher.action.SET_APP_SHYNESS"; 113 private static final String LEANBACK_SHY_MODE_EXTRA = "shyMode"; 114 115 private static final HashSet<String> AVAILABLE_DIALOG_TAGS = new HashSet<String>(); 116 117 private TvInputManager mTvInputManager; 118 private TunableTvView mTvView; 119 private LinearLayout mControlGuide; 120 private MainMenuView mMainMenuView; 121 private ChannelBannerView mChannelBanner; 122 private HideRunnable mHideChannelBanner; 123 private HideRunnable mHideControlGuide; 124 private HideRunnable mHideMainMenu; 125 private int mShortAnimationDuration; 126 private int mDisplayWidth; 127 private GestureDetector mGestureDetector; 128 private ChannelMap mChannelMap; 129 private long mInitChannelId; 130 131 private TvInput mTvInputForSetup; 132 private TvInputManagerHelper mTvInputManagerHelper; 133 private AudioManager mAudioManager; 134 private int mAudioFocusStatus; 135 private boolean mTunePendding; 136 private boolean mPipShowing; 137 private boolean mDebugNonFullSizeScreen; 138 private boolean mUseKeycodeBlacklist = USE_KEYCODE_BLACKLIST; 139 private boolean mIsShy = true; 140 141 static { 142 AVAILABLE_DIALOG_TAGS.add(InputPickerDialogFragment.DIALOG_TAG); 143 AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG); 144 AVAILABLE_DIALOG_TAGS.add(EditChannelsDialogFragment.DIALOG_TAG); 145 AVAILABLE_DIALOG_TAGS.add(EditInputDialogFragment.DIALOG_TAG); 146 AVAILABLE_DIALOG_TAGS.add(PrivacySettingDialogFragment.DIALOG_TAG); 147 } 148 149 // PIP is used for debug/verification of multiple sessions rather than real PIP feature. 150 // When PIP is enabled, the same channel as mTvView is tuned. 151 private TunableTvView mPipView; 152 153 private final Handler mHandler = new Handler() { 154 @Override 155 public void handleMessage(Message msg) { 156 if (msg.what == MSG_START_TV_RETRY) { 157 Object[] arg = (Object[]) msg.obj; 158 TvInput input = (TvInput) arg[0]; 159 long channelId = (Long) arg[1]; 160 int retryCount = msg.arg1; 161 startTvIfAvailableOrRetry(input, channelId, retryCount); 162 } 163 } 164 }; 165 166 @Override 167 protected void onCreate(Bundle savedInstanceState) { 168 super.onCreate(savedInstanceState); 169 170 setContentView(R.layout.activity_tv); 171 mTvView = (TunableTvView) findViewById(R.id.tv_view); 172 mTvView.setOnUnhandledInputEventListener(new TunableTvView.OnUnhandledInputEventListener() { 173 @Override 174 public boolean onUnhandledInputEvent(InputEvent event) { 175 if (event instanceof KeyEvent) { 176 KeyEvent keyEvent = (KeyEvent) event; 177 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 178 return onKeyUp(keyEvent.getKeyCode(), keyEvent); 179 } 180 } else if (event instanceof MotionEvent) { 181 MotionEvent motionEvent = (MotionEvent) event; 182 if (motionEvent.isTouchEvent()) { 183 return onTouchEvent(motionEvent); 184 } 185 } 186 return false; 187 } 188 }); 189 mPipView = (TunableTvView) findViewById(R.id.pip_view); 190 mPipView.setZOrderMediaOverlay(true); 191 192 mControlGuide = (LinearLayout) findViewById(R.id.control_guide); 193 mChannelBanner = (ChannelBannerView) findViewById(R.id.channel_banner); 194 mMainMenuView = (MainMenuView) findViewById(R.id.main_menu); 195 mMainMenuView.setTvActivity(this); 196 197 // Initially hide the channel banner and the control guide. 198 mChannelBanner.setVisibility(View.GONE); 199 mMainMenuView.setVisibility(View.GONE); 200 mControlGuide.setVisibility(View.GONE); 201 202 mHideControlGuide = new HideRunnable(mControlGuide, DURATION_SHOW_CONTROL_GUIDE); 203 mHideChannelBanner = new HideRunnable(mChannelBanner, DURATION_SHOW_CHANNEL_BANNER); 204 mHideMainMenu = new HideRunnable(mMainMenuView, DURATION_SHOW_MAIN_MENU); 205 206 mShortAnimationDuration = getResources().getInteger( 207 android.R.integer.config_shortAnimTime); 208 209 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 210 mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; 211 Display display = getWindowManager().getDefaultDisplay(); 212 Point size = new Point(); 213 display.getSize(size); 214 mDisplayWidth = size.x; 215 216 mGestureDetector = new GestureDetector(this, new SimpleOnGestureListener() { 217 static final float CONTROL_MARGIN = 0.2f; 218 final float mLeftMargin = mDisplayWidth * CONTROL_MARGIN; 219 final float mRightMargin = mDisplayWidth * (1 - CONTROL_MARGIN); 220 221 @Override 222 public boolean onDown(MotionEvent event) { 223 if (DEBUG) Log.d(TAG, "onDown: " + event.toString()); 224 if (mChannelMap == null) { 225 return false; 226 } 227 228 mHideControlGuide.showAndHide(); 229 230 if (event.getX() <= mLeftMargin) { 231 channelDown(); 232 return true; 233 } else if (event.getX() >= mRightMargin) { 234 channelUp(); 235 return true; 236 } 237 return false; 238 } 239 240 @Override 241 public boolean onSingleTapUp(MotionEvent event) { 242 if (mChannelMap == null) { 243 showInputPickerDialog(); 244 return true; 245 } 246 247 if (event.getX() > mLeftMargin && event.getX() < mRightMargin) { 248 displayMainMenu(); 249 return true; 250 } 251 return false; 252 } 253 }); 254 255 mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); 256 mTvInputManagerHelper = new TvInputManagerHelper(mTvInputManager); 257 onNewIntent(getIntent()); 258 } 259 260 @Override 261 protected void onNewIntent(Intent intent) { 262 // Handle the passed key press, if any. Note that only the key codes that are currently 263 // handled in the TV app will be handled via Intent. 264 // TODO: Consider defining a separate intent filter as passing data of mime type 265 // vnd.android.cursor.item/vnd.com.android.tv.channels isn't really necessary here. 266 int keyCode = intent.getIntExtra(Utils.EXTRA_KEYCODE, KeyEvent.KEYCODE_UNKNOWN); 267 if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { 268 if (DEBUG) Log.d(TAG, "Got an intent with keycode: " + keyCode); 269 KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keyCode); 270 onKeyUp(keyCode, event); 271 return; 272 } 273 274 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 275 // In case the channel is given explicitly, use it. 276 mInitChannelId = ContentUris.parseId(intent.getData()); 277 } else { 278 mInitChannelId = Channel.INVALID_ID; 279 } 280 } 281 282 @Override 283 protected void onStart() { 284 super.onStart(); 285 mTvInputManagerHelper.start(); 286 } 287 288 @Override 289 protected void onResume() { 290 super.onResume(); 291 mTvInputManagerHelper.update(); 292 if (mTvInputManagerHelper.getTvInputSize() == 0) { 293 Toast.makeText(this, R.string.no_input_device_found, Toast.LENGTH_SHORT).show(); 294 // TODO: Direct the user to a Play Store landing page for TvInputService apps. 295 return; 296 } 297 startTv(mInitChannelId); 298 mInitChannelId = Channel.INVALID_ID; 299 } 300 301 private void startTv(long channelId) { 302 if (mTvView.isPlaying()) { 303 // TV has already started. 304 if (channelId == Channel.INVALID_ID) { 305 // Simply adjust the volume without tune. 306 setVolumeByAudioFocusStatus(); 307 return; 308 } 309 Uri channelUri = mChannelMap.getCurrentChannelUri(); 310 if (channelUri != null && ContentUris.parseId(channelUri) == channelId) { 311 // The requested channel is already tuned. 312 setVolumeByAudioFocusStatus(); 313 return; 314 } 315 stopTv(); 316 } 317 318 if (channelId == Channel.INVALID_ID) { 319 // If any initial channel id is not given, remember the last channel the user watched. 320 channelId = Utils.getLastWatchedChannelId(this); 321 } 322 if (channelId == Channel.INVALID_ID) { 323 // If failed to pick a channel, try a different input. 324 showInputPickerDialog(); 325 return; 326 } 327 String inputId = Utils.getInputIdForChannel(this, channelId); 328 if (TextUtils.isEmpty(inputId)) { 329 // If failed to determine the input for that channel, try a different input. 330 showInputPickerDialog(); 331 return; 332 } 333 TvInputInfo inputInfo = mTvInputManagerHelper.getTvInputInfo(inputId); 334 if (inputInfo == null) { 335 // TODO: if the last selected TV input is uninstalled, getLastWatchedChannelId 336 // should return Channel.INVALID_ID. 337 Log.w(TAG, "Input (id=" + inputId + ") doesn't exist"); 338 showInputPickerDialog(); 339 return; 340 } 341 String lastSelectedInputId = Utils.getLastSelectedInputId(this); 342 TvInput input; 343 if (UnifiedTvInput.ID.equals(lastSelectedInputId)) { 344 input = new UnifiedTvInput(mTvInputManagerHelper, this); 345 } else { 346 input = new TisTvInput(mTvInputManagerHelper, inputInfo, this); 347 } 348 startTvIfAvailableOrRetry(input, channelId, 0); 349 } 350 351 private void startTvIfAvailableOrRetry(TvInput input, long channelId, int retryCount) { 352 if (!input.isAvailable()) { 353 if (retryCount >= START_TV_MAX_RETRY) { 354 showInputPickerDialog(); 355 return; 356 } 357 if (DEBUG) Log.d(TAG, "Retry start TV (retryCount=" + retryCount + ")"); 358 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TV_RETRY, 359 retryCount + 1, 0, new Object[]{input, channelId}), 360 START_TV_RETRY_INTERVAL); 361 return; 362 } 363 startTv(input, channelId); 364 } 365 366 @Override 367 protected void onStop() { 368 if (DEBUG) Log.d(TAG, "onStop()"); 369 mHandler.removeMessages(MSG_START_TV_RETRY); 370 stopTv(); 371 stopPip(); 372 if (!isShyModeSet()) { 373 setShynessMode(true); 374 } 375 mTvInputManagerHelper.stop(); 376 super.onStop(); 377 } 378 379 @Override 380 public void onInputPicked(TvInput input) { 381 if (input.equals(getSelectedTvInput())) { 382 // Nothing has changed thus nothing to do. 383 return; 384 } 385 if (!input.hasChannel(false)) { 386 mTvInputForSetup = null; 387 if (input.hasActivity(Utils.ACTION_SETUP)) { 388 startSetupActivity(input); 389 } else { 390 Toast.makeText(this, R.string.empty_channel_tvinput, Toast.LENGTH_SHORT).show(); 391 showInputPickerDialog(); 392 } 393 return; 394 } 395 396 stopTv(); 397 startTvWithLastWatchedChannel(input); 398 } 399 400 public TvInputManagerHelper getTvInputManagerHelper() { 401 return mTvInputManagerHelper; 402 } 403 404 public TvInput getSelectedTvInput() { 405 return mChannelMap == null ? null : mChannelMap.getTvInput(); 406 } 407 408 public void showEditChannelsDialog() { 409 if (getSelectedTvInput() == null) { 410 return; 411 } 412 413 showDialogFragment(EditChannelsDialogFragment.DIALOG_TAG, new EditChannelsDialogFragment()); 414 } 415 416 public void showInputPickerDialog() { 417 showDialogFragment(InputPickerDialogFragment.DIALOG_TAG, new InputPickerDialogFragment()); 418 } 419 420 public void startSettingsActivity() { 421 if (getSelectedTvInput() == null) { 422 Log.w(TAG, "There is no selected TV input during startSettingsActivity"); 423 return; 424 } 425 getSelectedTvInput().startActivity(Utils.ACTION_SETTINGS); 426 } 427 428 public void startSetupActivity() { 429 if (getSelectedTvInput() != null) { 430 startSetupActivity(getSelectedTvInput()); 431 } 432 } 433 434 public void startSetupActivity(TvInput input) { 435 if (input.startActivityForResult(this, Utils.ACTION_SETUP, REQUEST_START_SETUP_ACTIIVTY)) { 436 mTvInputForSetup = input; 437 stopTv(); 438 } else { 439 String displayName = input.getDisplayName(); 440 String message = String.format(getString( 441 R.string.input_setup_activity_not_found), displayName); 442 new AlertDialog.Builder(this) 443 .setMessage(message) 444 .setPositiveButton(R.string.OK, null) 445 .show(); 446 } 447 } 448 449 public void showAspectRatioOption() { 450 AspectRatioOptionFragment f = new AspectRatioOptionFragment(); 451 FragmentTransaction ft = getFragmentManager().beginTransaction(); 452 ft.add(R.id.right_panel, f); 453 ft.addToBackStack(null); 454 // TODO: add an animation. 455 ft.commit(); 456 } 457 458 public void showClosedCaptionOption() { 459 ClosedCaptionOptionFragment f = new ClosedCaptionOptionFragment(); 460 FragmentTransaction ft = getFragmentManager().beginTransaction(); 461 ft.add(R.id.right_panel, f); 462 ft.addToBackStack(null); 463 // TODO: add an animation. 464 ft.commit(); 465 } 466 467 @Override 468 public void onActivityResult(int requestCode, int resultCode, Intent data) { 469 switch (requestCode) { 470 case REQUEST_START_SETUP_ACTIIVTY: 471 if (resultCode == Activity.RESULT_OK && mTvInputForSetup != null) { 472 startTvWithLastWatchedChannel(mTvInputForSetup); 473 } 474 break; 475 476 default: 477 //TODO: Handle failure of setup. 478 } 479 mTvInputForSetup = null; 480 } 481 482 @Override 483 public boolean dispatchKeyEvent(KeyEvent event) { 484 if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); 485 int eventKeyCode = event.getKeyCode(); 486 if (mUseKeycodeBlacklist) { 487 for (int keycode : KEYCODE_BLACKLIST) { 488 if (keycode == eventKeyCode) { 489 return super.dispatchKeyEvent(event); 490 } 491 } 492 return dispatchKeyEventToSession(event); 493 } else { 494 for (int keycode : KEYCODE_WHITELIST) { 495 if (keycode == eventKeyCode) { 496 return dispatchKeyEventToSession(event); 497 } 498 } 499 return super.dispatchKeyEvent(event); 500 } 501 } 502 503 @Override 504 public void onAudioFocusChange(int focusChange) { 505 mAudioFocusStatus = focusChange; 506 setVolumeByAudioFocusStatus(); 507 } 508 509 private void setVolumeByAudioFocusStatus() { 510 if (mTvView.isPlaying()) { 511 switch (mAudioFocusStatus) { 512 case AudioManager.AUDIOFOCUS_GAIN: 513 mTvView.setVolume(AUDIO_MAX_VOLUME); 514 break; 515 case AudioManager.AUDIOFOCUS_LOSS: 516 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 517 mTvView.setVolume(AUDIO_MIN_VOLUME); 518 break; 519 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 520 mTvView.setVolume(AUDIO_DUCKING_VOLUME); 521 break; 522 } 523 } 524 } 525 526 private void startTvWithLastWatchedChannel(TvInput input) { 527 long channelId = Utils.getLastWatchedChannelId(TvActivity.this, input.getId()); 528 startTv(input, channelId); 529 } 530 531 private void startTv(TvInput input, long channelId) { 532 if (mChannelMap != null) { 533 // TODO: when this case occurs, we should remove the case. 534 Log.w(TAG, "The previous variables are not released in startTv"); 535 stopTv(); 536 } 537 538 mMainMenuView.setChannelMap(null); 539 int result = mAudioManager.requestAudioFocus(TvActivity.this, 540 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 541 mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ? 542 AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS; 543 544 // Prepare a new channel map for the current input. 545 mChannelMap = input.buildChannelMap(this, channelId, mOnChannelsLoadFinished); 546 mTvView.start(mTvInputManagerHelper); 547 setVolumeByAudioFocusStatus(); 548 tune(); 549 } 550 551 private void stopTv() { 552 if (mTvView.isPlaying()) { 553 mTvView.stop(); 554 mAudioManager.abandonAudioFocus(this); 555 } 556 if (mChannelMap != null) { 557 mMainMenuView.setChannelMap(null); 558 mChannelMap.close(); 559 mChannelMap = null; 560 } 561 mTunePendding = false; 562 } 563 564 private boolean isPlaying() { 565 return mTvView.isPlaying() && mTvView.getCurrentChannelId() != Channel.INVALID_ID; 566 } 567 568 private void startPip() { 569 if (!isPlaying()) { 570 Log.w(TAG, "TV content should be playing"); 571 return; 572 } 573 if (DEBUG) Log.d(TAG, "startPip()"); 574 mPipView.start(mTvInputManagerHelper); 575 boolean success = mPipView.tuneTo(mTvView.getCurrentChannelId(), new OnTuneListener() { 576 @Override 577 public void onUnexpectedStop(long channelId) { 578 Log.w(TAG, "The PIP is Unexpectedly stopped"); 579 stopPip(); 580 } 581 582 @Override 583 public void onTuned(boolean success, long channelId) { 584 if (!success) { 585 Log.w(TAG, "Fail to start the PIP during channel tunning"); 586 stopPip(); 587 } else { 588 mPipView.setVisibility(View.VISIBLE); 589 } 590 } 591 592 @Override 593 public void onStreamInfoChanged(StreamInfo info) { 594 // Do nothing. 595 } 596 }); 597 if (!success) { 598 Log.w(TAG, "Fail to start the PIP"); 599 return; 600 } 601 mPipView.setVolume(AUDIO_MIN_VOLUME); 602 mPipShowing = true; 603 } 604 605 private void stopPip() { 606 if (DEBUG) Log.d(TAG, "stopPip"); 607 if (mPipView.isPlaying()) { 608 mPipView.setVisibility(View.INVISIBLE); 609 mPipView.stop(); 610 } 611 mPipShowing = false; 612 } 613 614 private final Runnable mOnChannelsLoadFinished = new Runnable() { 615 @Override 616 public void run() { 617 if (mTunePendding) { 618 tune(); 619 } 620 mMainMenuView.setChannelMap(mChannelMap); 621 } 622 }; 623 624 private void tune() { 625 if (DEBUG) Log.d(TAG, "tune()"); 626 // Prerequisites to be able to tune. 627 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 628 if (DEBUG) Log.d(TAG, "Channel map not ready"); 629 mTunePendding = true; 630 return; 631 } 632 mTunePendding = false; 633 long channelId = mChannelMap.getCurrentChannelId(); 634 final String inputId = mChannelMap.getTvInput().getId(); 635 if (channelId == Channel.INVALID_ID) { 636 stopTv(); 637 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 638 return; 639 } 640 641 mTvView.tuneTo(channelId, new OnTuneListener() { 642 @Override 643 public void onUnexpectedStop(long channelId) { 644 stopTv(); 645 startTv(Channel.INVALID_ID); 646 } 647 648 @Override 649 public void onTuned(boolean success, long channelId) { 650 if (!success) { 651 Log.w(TAG, "Failed to tune to channel " + channelId); 652 // TODO: show something to user about this error. 653 } else { 654 Utils.setLastWatchedChannelId(TvActivity.this, inputId, 655 channelId); 656 } 657 } 658 659 @Override 660 public void onStreamInfoChanged(StreamInfo info) { 661 updateChannelBanner(false); 662 } 663 }); 664 updateChannelBanner(true); 665 if (isShyModeSet()) { 666 setShynessMode(false); 667 // TODO: Set the shy mode to true when tune() fails. 668 } 669 } 670 671 private void updateChannelBanner(final boolean showBanner) { 672 runOnUiThread(new Runnable() { 673 @Override 674 public void run() { 675 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 676 return; 677 } 678 679 mChannelBanner.updateViews(mChannelMap, mTvView); 680 if (showBanner) { 681 mHideChannelBanner.showAndHide(); 682 } 683 } 684 }); 685 } 686 687 private void displayMainMenu() { 688 runOnUiThread(new Runnable() { 689 @Override 690 public void run() { 691 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 692 return; 693 } 694 695 mHideMainMenu.showAndHide(); 696 } 697 }); 698 } 699 700 public void showRecentlyWatchedDialog() { 701 showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG, 702 new RecentlyWatchedDialogFragment()); 703 } 704 705 @Override 706 protected void onSaveInstanceState(Bundle outState) { 707 // Do not save instance state because restoring instance state when TV app died 708 // unexpectedly can cause some problems like initializing fragments duplicately and 709 // accessing resource before it is initialzed. 710 } 711 712 @Override 713 protected void onDestroy() { 714 if (DEBUG) Log.d(TAG, "onDestroy()"); 715 super.onDestroy(); 716 } 717 718 @Override 719 public boolean onKeyUp(int keyCode, KeyEvent event) { 720 if (getFragmentManager().getBackStackEntryCount() > 0) { 721 if (keyCode == KeyEvent.KEYCODE_BACK) { 722 getFragmentManager().popBackStack(); 723 return true; 724 } 725 return super.onKeyUp(keyCode, event); 726 } 727 if (mMainMenuView.getVisibility() == View.VISIBLE) { 728 if (keyCode == KeyEvent.KEYCODE_BACK) { 729 mMainMenuView.setVisibility(View.GONE); 730 return true; 731 } 732 return super.onKeyUp(keyCode, event); 733 } 734 735 if (mHandler.hasMessages(MSG_START_TV_RETRY)) { 736 // Ignore key events during startTv retry. 737 return true; 738 } 739 if (mChannelMap == null) { 740 switch (keyCode) { 741 case KeyEvent.KEYCODE_H: 742 showRecentlyWatchedDialog(); 743 return true; 744 case KeyEvent.KEYCODE_TV_INPUT: 745 case KeyEvent.KEYCODE_I: 746 case KeyEvent.KEYCODE_CHANNEL_UP: 747 case KeyEvent.KEYCODE_DPAD_UP: 748 case KeyEvent.KEYCODE_CHANNEL_DOWN: 749 case KeyEvent.KEYCODE_DPAD_DOWN: 750 case KeyEvent.KEYCODE_NUMPAD_ENTER: 751 case KeyEvent.KEYCODE_DPAD_CENTER: 752 case KeyEvent.KEYCODE_E: 753 case KeyEvent.KEYCODE_MENU: 754 showInputPickerDialog(); 755 return true; 756 } 757 } else { 758 switch (keyCode) { 759 case KeyEvent.KEYCODE_H: 760 showRecentlyWatchedDialog(); 761 return true; 762 763 case KeyEvent.KEYCODE_TV_INPUT: 764 case KeyEvent.KEYCODE_I: 765 showInputPickerDialog(); 766 return true; 767 768 case KeyEvent.KEYCODE_CHANNEL_UP: 769 case KeyEvent.KEYCODE_DPAD_UP: 770 channelUp(); 771 return true; 772 773 case KeyEvent.KEYCODE_CHANNEL_DOWN: 774 case KeyEvent.KEYCODE_DPAD_DOWN: 775 channelDown(); 776 return true; 777 778 case KeyEvent.KEYCODE_DPAD_LEFT: 779 case KeyEvent.KEYCODE_DPAD_RIGHT: 780 displayMainMenu(); 781 return true; 782 783 case KeyEvent.KEYCODE_ENTER: 784 case KeyEvent.KEYCODE_NUMPAD_ENTER: 785 case KeyEvent.KEYCODE_E: 786 case KeyEvent.KEYCODE_DPAD_CENTER: 787 case KeyEvent.KEYCODE_MENU: 788 if (event.isCanceled()) { 789 return true; 790 } 791 if (keyCode != KeyEvent.KEYCODE_MENU) { 792 updateChannelBanner(true); 793 } 794 if (keyCode != KeyEvent.KEYCODE_E) { 795 displayMainMenu(); 796 } 797 return true; 798 } 799 } 800 if (USE_DEBUG_KEYS) { 801 switch (keyCode) { 802 case KeyEvent.KEYCODE_W: { 803 mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen; 804 if (mDebugNonFullSizeScreen) { 805 mTvView.layout(100, 100, 400, 300); 806 } else { 807 ViewGroup.LayoutParams params = mTvView.getLayoutParams(); 808 params.width = ViewGroup.LayoutParams.MATCH_PARENT; 809 params.height = ViewGroup.LayoutParams.MATCH_PARENT; 810 mTvView.setLayoutParams(params); 811 } 812 return true; 813 } 814 case KeyEvent.KEYCODE_P: { 815 togglePipView(); 816 return true; 817 } 818 case KeyEvent.KEYCODE_CTRL_LEFT: 819 case KeyEvent.KEYCODE_CTRL_RIGHT: { 820 mUseKeycodeBlacklist = !mUseKeycodeBlacklist; 821 return true; 822 } 823 case KeyEvent.KEYCODE_O: { 824 showAspectRatioOption(); 825 return true; 826 } 827 } 828 } 829 return super.onKeyUp(keyCode, event); 830 } 831 832 @Override 833 public boolean onKeyLongPress(int keyCode, KeyEvent event) { 834 if (DEBUG) Log.d(TAG, "onKeyLongPress(" + event); 835 // Treat the BACK key long press as the normal press since we changed the behavior in 836 // onBackPressed(). 837 if (keyCode == KeyEvent.KEYCODE_BACK) { 838 super.onBackPressed(); 839 return true; 840 } 841 return false; 842 } 843 844 @Override 845 public void onBackPressed() { 846 if (getFragmentManager().getBackStackEntryCount() <= 0 && isPlaying()) { 847 // TODO: show the following toast message in the future. 848// Toast.makeText(getApplicationContext(), getResources().getString( 849// R.string.long_press_back), Toast.LENGTH_SHORT).show(); 850 851 // If back key would exit TV app, 852 // show McLauncher instead so we can get benefit of McLauncher's shyMode. 853 Intent startMain = new Intent(Intent.ACTION_MAIN); 854 startMain.addCategory(Intent.CATEGORY_HOME); 855 startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 856 startActivity(startMain); 857 } else { 858 super.onBackPressed(); 859 } 860 } 861 862 @Override 863 public void onUserInteraction() { 864 super.onUserInteraction(); 865 if (mMainMenuView.getVisibility() == View.VISIBLE) { 866 mHideMainMenu.showAndHide(); 867 } 868 } 869 870 @Override 871 public boolean onTouchEvent(MotionEvent event) { 872 if (mMainMenuView.getVisibility() != View.VISIBLE) { 873 mGestureDetector.onTouchEvent(event); 874 } 875 return super.onTouchEvent(event); 876 } 877 878 public void togglePipView() { 879 if (mPipShowing) { 880 stopPip(); 881 } else { 882 startPip(); 883 } 884 } 885 886 private boolean dispatchKeyEventToSession(final KeyEvent event) { 887 if (DEBUG) Log.d(TAG, "dispatchKeyEventToSession(" + event + ")"); 888 if (mTvView != null) { 889 return mTvView.dispatchKeyEvent(event); 890 } 891 return false; 892 } 893 894 public void moveToChannel(long id) { 895 if (mChannelMap != null && mChannelMap.isLoadFinished() 896 && id != mChannelMap.getCurrentChannelId()) { 897 if (mChannelMap.moveToChannel(id)) { 898 tune(); 899 } else if (!TextUtils.isEmpty(Utils.getInputIdForChannel(this, id))) { 900 startTv(id); 901 } else { 902 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 903 } 904 } 905 } 906 907 private void channelUp() { 908 if (mChannelMap != null && mChannelMap.isLoadFinished()) { 909 if (mChannelMap.moveToNextChannel()) { 910 tune(); 911 } else { 912 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 913 } 914 } 915 } 916 917 private void channelDown() { 918 if (mChannelMap != null && mChannelMap.isLoadFinished()) { 919 if (mChannelMap.moveToPreviousChannel()) { 920 tune(); 921 } else { 922 Toast.makeText(this, R.string.input_is_not_available, Toast.LENGTH_SHORT).show(); 923 } 924 } 925 } 926 927 public void showDialogFragment(final String tag, final DialogFragment dialog) { 928 // A tag for dialog must be added to AVAILABLE_DIALOG_TAGS to make it launchable from TV. 929 if (!AVAILABLE_DIALOG_TAGS.contains(tag)) { 930 return; 931 } 932 mHandler.post(new Runnable() { 933 @Override 934 public void run() { 935 FragmentManager fm = getFragmentManager(); 936 fm.executePendingTransactions(); 937 938 for (String availableTag : AVAILABLE_DIALOG_TAGS) { 939 if (fm.findFragmentByTag(availableTag) != null) { 940 return; 941 } 942 } 943 944 FragmentTransaction ft = getFragmentManager().beginTransaction(); 945 ft.addToBackStack(null); 946 dialog.show(ft, tag); 947 } 948 }); 949 } 950 951 private class HideRunnable implements Runnable { 952 private final View mView; 953 private final long mWaitingTime; 954 private boolean mOnHideAnimation; 955 956 public HideRunnable(View view, long waitingTime) { 957 mView = view; 958 mWaitingTime = waitingTime; 959 } 960 961 @Override 962 public void run() { 963 mOnHideAnimation = true; 964 mView.animate() 965 .alpha(0f) 966 .setDuration(mShortAnimationDuration) 967 .setListener(new AnimatorListenerAdapter() { 968 @Override 969 public void onAnimationEnd(Animator animation) { 970 mOnHideAnimation = false; 971 mView.setVisibility(View.GONE); 972 } 973 }); 974 } 975 976 private void showAndHide() { 977 if (mView.getVisibility() != View.VISIBLE) { 978 mView.setAlpha(0f); 979 mView.setVisibility(View.VISIBLE); 980 mView.animate() 981 .alpha(1f) 982 .setDuration(mShortAnimationDuration) 983 .setListener(new AnimatorListenerAdapter() { 984 @Override 985 public void onAnimationEnd(Animator animation) { 986 // Currently the target alpha isn't kept, but it was before. 987 // TODO: Remove this if frameworks keeps the target value again. 988 mView.setAlpha(1f); 989 } 990 }); 991 } 992 // Schedule the hide animation after a few seconds. 993 mHandler.removeCallbacks(this); 994 if (mOnHideAnimation) { 995 mView.clearAnimation(); 996 mView.setAlpha(1f); 997 mOnHideAnimation = false; 998 } 999 mHandler.postDelayed(this, mWaitingTime); 1000 } 1001 } 1002 1003 private void setShynessMode(boolean shyMode) { 1004 mIsShy = shyMode; 1005 Intent intent = new Intent(LEANBACK_SET_SHYNESS_BROADCAST); 1006 intent.putExtra(LEANBACK_SHY_MODE_EXTRA, shyMode); 1007 sendBroadcast(intent); 1008 } 1009 1010 private boolean isShyModeSet() { 1011 return mIsShy; 1012 } 1013} 1014