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